= configtoolkit
== DESCRIPTION:
This package makes sourcing information from (parsing) configuration files
robust and easy! It:
- Allows programmers to specify the type of data that should be loaded
from a configuration file. The toolkit automatically will validate
the file's data against this specification when loading the file, ensuring
that the specification always is obeyed and saving the programmer the
tedious chore of writing validation code.
- Automagically generates parameter accessor methods (getters, setters, and
predicates to test for presence), an equality operator, and a +to_s+ method
from the configuration's specification.
- Allows programmers to create configuration files,
easily and programatically.
- Provides a class that can load (parse) Ruby configuration files (allowing the
full power of Ruby to be used within configuration files).
- Provides classes that can load from (parse) and dump to YAML and key-value
configuration files.
- Provides classes that can load from and dump to Hashes.
- Provides a class that allows the contents of one configuration source to
override the contents of another (this works with configuration files of any
format or Hashes).
- Is very extensible, allowing the engine to be used with custom format
configuration files and with custom data validation rules.
== FEATURES:
- The ConfigToolkit allows programmers to define a new configuration class by
specifying the parameters that are included in the configuration. A
parameter specification consists of the class of the parameter's values,
whether or not the paramater is required, and a default value if the
parameter is not required.
- Getter, setter, and predicate methods automatically are generated for a new
configuration class for each specified parameter to get the parameter's
value, set the parameter's value, and test whether the parameter has a value.
- A method to clear a parameter's value automatically is generated for
optional parameters.
- A block can be passed to the +new+ method of a configuration class in order
to initialize the instance (see ConfigToolkit::BaseConfig.new).
- The ConfigToolkit (by default) will print warnings when it encounters
unknown configuration parameters, allowing errors in configuration files to
be discovered quickly.
- The behavior of the ConfigToolkit for a particular ConfigToolkit::BaseConfig
child class or for an instance of a ConfigToolkit::BaseConfig child class
can be configured through specifying a ConfigToolkit::ConfigToolkitConfig.
Right now, the ConfigToolkit's behavior when it encounters an unknown
parameter can be configured (to ignore the unknown parameter, to warn
about the unknown parameter, or to raise an error about the unknown
parameter).
- +rdoc+ can detect and generate documentation for each parameter in a
configuration class (see FAQ.txt for how to enable this).
- An equality operator exists for each configuration class that
determines equality based on whether all parameter values are equal.
- A +to_s+ method that produces very pretty output exists for each
configuration class (ConfigToolkit::BaseConfig#to_s, leveraging the
ConfigToolkit::PrettyPrintWriter)
- Programmers can specify custom validation blocks for each parameter, in
order to enforce specifications not directly supported by the engine.
- Programmers can define a method in a configuration class that will be
called in order to enforce relationships between the values of different
parameters (ConfigToolkit::BaseConfig#validate_all_values)
- Programmers can create custom reader and writer classes in order to
load from and dump to (respectively) configuration file formats not
directly supported by the ConfigToolkit.
- Configuration classes can be nested to any depth within each other.
- Configuration classes have first class support for Array configuration
parameters. Constraints can be specified for a given Array parameter that
will ensure that all elements are of a specified class and that there
are a specified number of elements present (see
ConfigToolkit::ConstrainedArray)
- The ConfigToolkit supports multiple configurations stored
in a single file; it is able to distinguish that different configurations
within a file belong to different configuration objects. For example,
"production" and "test" configuration information can live within the
same configuration file and can be loaded into separate configuration
instances (by specifying a different "containing object name" for each
ConfigToolkit::BaseConfig#load call)
- A reader class to read (parse) Ruby configuration files
(ConfigToolkit::RubyReader)
- A reader class to read (parse) YAML configuration files
(ConfigToolkit::YAMLReader)
- A writer class to dump YAML configuration files
(ConfigToolkit::YAMLWriter)
- A reader class to load configuration directly from a Hash
(ConfigToolkit::HashReader)
- A writer class do dump configuration directly to a Hash
(ConfigToolkit::HashWriter)
- A reader class to read (parse) key-value configuration files
(ConfigToolkit::KeyValueReader)
- A writer class to dump key-value configuration files
(ConfigToolkit::KeyValueWriter)
- The ConfigToolkit::KeyValueReader and ConfigToolkit::KeyValueWriter classes
can be configured to work with many different formats of key-value
configuration files (via ConfigToolkit::KeyValueConfig).
- A reader class to source one configuration from multiple configuration files,
allowing one configuration file to override another
(ConfigToolkit::OverrideReader)
- The ConfigToolkit includes a full unit testing suite.
- The ConfigToolkit code has detailed comments.
- The ConfigToolkit code has many example programs (in the +examples+
subdirectory).
- The ConfigToolkit package includes extensive documentation, including
FAQ.txt and documentation for the support file formats.
== PROBLEMS:
None (known).
== SYNOPSIS:
Here is a sample configuration class:
======examples/machineconfig.rb:
require 'rubygems'
require 'configtoolkit'
require 'uri'
This configuration class is nested within MachineConfig (below).
All configuration classes must descend from ConfigToolkit::BaseConfig.
class OSConfig < ConfigToolkit::BaseConfig
add_required_param(:name, String)
add_required_param(:version, Float)
end
This configuration class is used in all of the example programs.
Since it is a configuration class, it descends from
ConfigToolkit::BaseConfig.
class MachineConfig < ConfigToolkit::BaseConfig
(that :num_cpus > 0 must be true)
add_required_param(:num_cpus, Integer) do |value|
if(value <= 0)
raise_error("num_cpus must be greater than zero")
end
end
This required parameter is itself a BaseConfig instance; BaseConfig
instances can be nested within each other.
add_required_param(:os, OSConfig)
This is a required boolean parameter. Note that Ruby does not
have a boolean type (it has a TrueClass and a FalseClass), so use
a marker class (ConfigToolkit::Boolean) to indicate that
the parameter will have boolean values (true and false).
Boolean values can be written as "true"/"false" or as
"yes"/"no" in configuration files.
add_required_param(:contains_sensitive_data, ConfigToolkit::Boolean)
The behind_firewall parameter is optional and has a default value of
true, which means that it will be set to the true if not explicitly set
by a configuration file.
add_optional_param(:behind_firewall, ConfigToolkit::Boolean, true)
The primary_contact parameter is optional and has no default value, which
means that it will be absent if not explicitly set by a configuration file.
add_optional_param(:primary_contact, String)
This parameter's values are guaranteed to be Arrays with URI elements.
add_required_param(:addresses, ConfigToolkit::ConstrainedArray.new(URI))
This method is called by load() after loading values for
all parameters from a specified configuration and can enforce
constraints between different parameters. In this case, this method
ensures that all machines containing sensitive data are behind
the firewall.
def validate_all_values
if(contains_sensitive_data && !behind_firewall)
raise_error("only machines behind firewalls can contain sensitive data")
end
end
end
This code creates a configuration class for a company's servers by subclassing
ConfigToolkit::BaseConfig. Each of the parameters is specified (the
parameter's class, whether the parameter is required, and a default value
if the parameter is optional) with private class methods of
ConfigToolkit::BaseConfig (ConfigToolkit::BaseConfig.add_required_param and
ConfigToolkit::BaseConfig.add_optional_param).
A +validate_all_values+ method also is defined in the +MachineConfig+
class that enforces an invariant between multiple parameters. This will be
called at the end of ConfigToolkit::BaseConfig#load or at the start of
ConfigToolkit::BaseConfig#dump (see
ConfigToolkit::BaseConfig#validate_all_values for more details).
A custom validation block is specified for the +num_cpus+ parameter;
the ConfigToolkit engine does not have direct support for
checking specifications like "the value must be greater than zero", but these
conditions easily can be added by the programmer. Such blocks are
called by the engine before the configuration parameter is set to the new
value and are executed in the context of the configuration class instance.
If there is a problem with the new value, the block should call
ConfigToolkit::BaseConfig#raise_error.
A ConfigToolkit::Boolean class is specified for the
+behind_firewall+ and +contains_sensitive_data+
parameters. Ruby does not have a standard boolean
class (although it does have TrueClass and FalseClass, for boolean values), and
so the ConfigToolkit provides a marker class for boolean values. The value
of a parameter specified with ConfigToolkit::Boolean either is +true+ or
+false+.
A ConfigToolkit::ConstrainedArray.new(URI) class is
specified for the +addresses+ parameter. A
ConfigToolkit::ConstrainedArray class simply specifies that the
parameter value will be an Array that obeys the
constraints contained in the ConfigToolkit::ConstrainedArray. In this
case, the constraints ensure that all Array elements are of class URI,
that the Array has at least two elements. Additional constraints on the
number of elements in the Array are possible
The following program manipulates a MachineConfig instance.
======examples/usage_example.rb:
#!/usr/bin/env ruby
These example programs use the 'relative' gem in order to
express paths relative to FILE cleanly, allowing them to be run from
any directory. In particular, they use the require_relative
method to load examples/machineconfig.rb and
File.expand_path_relative_to_caller in order to refer to
configuration files within the examples directory.
require 'rubygems'
require 'relative'
require_relative 'machineconfig'
If new() is passed a block, the block must initialize all
required parameters (otherwise a ConfigToolkit::Error will
be thrown). Optional parameters to not need to be
explicitly initialized, however. If a default value was
specified for an uninitialized optional parameter, the
optional parameter will be set to the default value after
the block (the behind_firewall parameter is an example of this below);
otherwise, the default value will have no value
(the primary_contact parameter is an example of this below).
config = MachineConfig.new() do |config|
config.num_cpus = 8
config.os = OSConfig.new() do |os_config|
os_config.name = "Solaris"
os_config.version = 10.0
end
config.contains_sensitive_data = true
config.addresses = [URI("http://www.designingpatterns.com"),
URI("http://jackfruit.designingpatterns.com")]
end
Note in the output that the behind_firewall parameter is true, its
default value, because it was not initialized in the block above.
Also note that the primary_contact parameter is not present.
print "Config after initialization:\n#{config}\n"
Each parameter has a predicate method (automatically generated by
add_required_param or add_optional_param) that returns whether or not
the parameter has a value.
if(!config.primary_contact?)
This will be printed...
print "Optional primary_contact parameter is NOT set!\n"
end
Each parameter has getter and setter accessor methods (automatically
generated by add_required_param or add_optional_param), just like
"normal" attributes.
config.primary_contact = "Tony"
print("primary_contact parameter set to: #{config.primary_contact}\n")
if(config.primary_contact?)
This will be printed...
print "Optional primary_contact parameter is now IS set!\n\n"
end
Note in the output that the primary_contact parameter now is present.
print "Config after setting optional primary_contact parameter:\n#{config}\n"
Each optional parameter has a clear method (automatically generated by
add_optional_param) that deletes the parameter's value.
config.clear_primary_contact()
if(!config.primary_contact?)
This will be printed...
print "Optional primary_contact parameter has been deleted!\n\n"
end
Note in the output that the primary_contact parameter once again
is absent.
print "Config after deleting the primary_contact parameter:\n#{config}\n"
This example uses the accessor methods (a getter, a setter, and a predicate)
that are generated automatically for each configuration parameter. These
allow configuration instances to be manipulated in the same way that
instances of a hand-written configuration class could be manipulated. Note,
however, that the same strict validation is performed for values specified
programatically through setters as is done for values sourced from
configuration files. Also note that setters copy the values of their
arguments (with the +dup+ method) not references to their arguments, unlike
the setters created by the +attr_accessor+ method.
When run, it produces:
======The output of examples/usage_example.rb
:
Config after initialization:
{
contains_sensitive_data: true
num_cpus : 8
addresses : [
http://www.designingpatterns.com,
http://jackfruit.designingpatterns.com
]
os : {
version: 10.0
name : Solaris
}
behind_firewall : true
}
Optional primary_contact parameter is NOT set!
primary_contact parameter set to: Tony
Optional primary_contact parameter is now IS set!
Config after setting optional primary_contact parameter:
{
contains_sensitive_data: true
num_cpus : 8
addresses : [
http://www.designingpatterns.com,
http://jackfruit.designingpatterns.com
]
os : {
version: 10.0
name : Solaris
}
behind_firewall : true
primary_contact : Tony
}
Optional primary_contact parameter has been deleted!
Config after deleting the primary_contact parameter:
{
contains_sensitive_data: true
num_cpus : 8
addresses : [
http://www.designingpatterns.com,
http://jackfruit.designingpatterns.com
]
os : {
version: 10.0
name : Solaris
}
behind_firewall : true
}
Of course, the most common use case will be setting configuration parameters
with the contents of configuration files, not programatically. The following YAML configuration file contains two MachineConfig configurations:
======examples/load_example.yaml:
This file contains MachineConfig configurations
(see examples/machineconfig.rb for the configuration's specification).
############################################################
First configuration
num_cpus: 32
os:
name: AIX
version: 5.3
behind_firewall: no
contains_sensitive_data: no
addresses:
production is a containing object
production:
production.www is a containing object
www:
############################################################
Second configuration (nested in the production.www
containing object)
num_cpus: 64
os:
name: Solaris
version: 10.0
contains_sensitive_data: yes
addresses:
- http://www.designingpatterns.com
- http://tokyo.designingpatterns.com
############################################################
The following program loads and prints the two configurations from
+examples/load_example.yaml+.
======examples/load_example.rb:
#!/usr/bin/env ruby
These example programs use the 'relative' gem in order to
express paths relative to FILE cleanly, allowing them to be run from
any directory. In particular, they use the require_relative
method to load examples/machineconfig.rb and
File.expand_path_relative_to_caller in order to refer to
configuration files within the examples directory.
require 'rubygems'
require 'relative'
require_relative 'machineconfig'
require 'configtoolkit/yamlreader'
CONFIGURATION_FILE = File.expand_path_relative_to_caller("load_example.yaml")
The first configuration has no containing object name.
reader = ConfigToolkit::YAMLReader.new(CONFIGURATION_FILE)
first_config = MachineConfig.load(reader)
print("The first config:\n#{first_config}\n")
The second configuration has "production.www" as a containing
object name.
reader = ConfigToolkit::YAMLReader.new(CONFIGURATION_FILE)
second_config = MachineConfig.load(reader, "production.www")
print("The second config:\n#{second_config}\n")
Note how the program changes the containing object name passed to the +load+
method (first an empty string, next +production.www+, and finally
+production.mail+) in order to refer to different configurations within
the YAML file. The ConfigToolkit's support of containing objects allows it
to accomodate multiple configurations living in the same file, with each
configuration being accessible separately. When run, it produces:
======The output of examples/load_example.rb
:
The first config:
{
addresses : [
http://default.designingpatterns.com,
http://apple.designingpatterns.com
]
contains_sensitive_data: false
os : {
version: 5.3
name : AIX
}
behind_firewall : false
num_cpus : 32
}
The second config:
production.www: {
addresses : [
http://www.designingpatterns.com,
http://tokyo.designingpatterns.com
]
contains_sensitive_data: true
os : {
version: 10.0
name : Solaris
}
behind_firewall : true
num_cpus : 64
}
A group of configurations also can be loaded with the
ConfigToolkit::BaseConfig.load_group method. For instance, given
+examples/load_group_example.yaml+:
======examples/load_group_example.yaml:
This file contains MachineConfig configurations
(see examples/machineconfig.rb for the configuration's specification).
db_cluster:
db1:
num_cpus: 16
os:
name: Solaris
version: 10.0
contains_sensitive_data: yes
behind_firewall: yes
addresses:
- http://db1.designingpatterns.com
db2:
num_cpus: 12
os:
name: AIX
version: 5.3
contains_sensitive_data: yes
behind_firewall: yes
addresses:
- http://db2.designingpatterns.com
db3:
num_cpus: 24
os:
name: Solaris
version: 10.0
contains_sensitive_data: yes
behind_firewall: yes
addresses:
- http://db3.designingpatterns.com
the following code will load the file's three configurations into a +Hash+
mapping containing object names to configurations:
======examples/load_group_example.rb:
#!/usr/bin/env ruby
These example programs use the 'relative' gem in order to
express paths relative to FILE cleanly, allowing them to be run from
any directory. In particular, they use the require_relative
method to load examples/machineconfig.rb and
File.expand_path_relative_to_caller in order to refer to
configuration files within the examples directory.
require 'rubygems'
require 'relative'
require_relative 'machineconfig'
require 'configtoolkit/yamlreader'
CONFIGURATION_FILE =
File.expand_path_relative_to_caller("load_group_example.yaml")
The group of configurations is under the db_cluster containing
object name.
reader = ConfigToolkit::YAMLReader.new(CONFIGURATION_FILE)
configs = MachineConfig.load_group(reader, "db_cluster")
configs.each do |name, config|
print "The #{name} configuration:\n#{config}\n"
end
When run, it produces:
======The output of examples/load_group_example.rb
:
The db3 configuration:
db_cluster.db3: {
addresses : [
http://db3.designingpatterns.com
]
contains_sensitive_data: true
os : {
name : Solaris
version: 10.0
}
behind_firewall : true
num_cpus : 24
}
The db1 configuration:
db_cluster.db1: {
addresses : [
http://db1.designingpatterns.com
]
contains_sensitive_data: true
os : {
name : Solaris
version: 10.0
}
behind_firewall : true
num_cpus : 16
}
The db2 configuration:
db_cluster.db2: {
addresses : [
http://db2.designingpatterns.com
]
contains_sensitive_data: true
os : {
name : AIX
version: 5.3
}
behind_firewall : true
num_cpus : 12
}
== CONFIGURING THE CONFIGTOOLKIT
As of version 2.3.0, the ConfigToolkit itself can be configured.
Consider the following simple YAML configuration file:
======examples/config_toolkit_config_example.yaml:
name: Tony
'country' has been misspelled
county: USA
The following code configures the +SimpleConfig+ class and specific instances
of the +SimpleConfig+ class to behave differently than the ConfigToolkit
default when unknown parameters are encountered when loading a configuration
file.
======examples/config_toolkit_config_example.rb:
#!/usr/bin/env ruby
require 'rubygems'
require 'configtoolkit'
require 'configtoolkit/yamlreader'
require 'relative'
class SimpleConfig < ConfigToolkit::BaseConfig
Set SimpleConfig's default ConfigToolkit::ConfigToolkitConfig via
the default_config_toolkit_config accessor method.
self.default_config_toolkit_config = ConfigToolkit::ConfigToolkitConfig.new() do |instance|
#
# This will suppress warnings when an unknown config param
# is encountered when loading a configuration file.
#
instance.unknown_config_param_behavior =
ConfigToolkit::ConfigToolkitConfig::UnknownConfigParamBehavior::IGNORE
end
add_required_param(:name, String)
add_optional_param(:country, String, NO_DEFAULT_VALUE)
end
CONFIGURATION_FILE = File.expand_path_relative_to_caller("config_toolkit_config_example.yaml")
Even though there is a bogus parameter in the configuration file,
SimpleConfig.load silently will ignore it due to SimpleConfig's
default ConfigToolkit::ConfigToolkitConfig.
reader = ConfigToolkit::YAMLReader.new(CONFIGURATION_FILE)
config = SimpleConfig.load(reader)
puts "Unknown parameters silently were ignored when loading this: #{config}\n"
Note that a ConfigToolkit::ConfigToolkitConfig is passed
into SimpleConfig.load below. This will override SimpleConfig's default
ConfigToolkit::ConfigToolkitConfig and cause an error to be raised when
the unknown parameter is encountered.
config_toolkit_config = ConfigToolkit::ConfigToolkitConfig.new() do |instance|
This will cause an error to be raised when an unknown config param
is encountered when loading a configuration file.
instance.unknown_config_param_behavior =
ConfigToolkit::ConfigToolkitConfig::UnknownConfigParamBehavior::ERROR
end
reader = ConfigToolkit::YAMLReader.new(CONFIGURATION_FILE)
begin
config = SimpleConfig.load(reader, "", config_toolkit_config)
rescue ConfigToolkit::Error => e
puts e
end
When run, it produces:
======The output of examples/config_toolkit_config_example.rb
:
Unknown parameters silently were ignored when loading this: {
name: Tony
}
error setting county with value USA: no parameter county exists for configuration class SimpleConfig.
=== CONFIGTOOLKIT CONFIGURATION PARAMETERS
+unknown_config_param_behavior+: the behavior when encountering an unknown parameter. Its possible values:
+ignore+:: The unknown parameter will be ignored (+ignore+). This can be
useful in order to maintain compatibility with older versions of
software when adding new fields to a configuration file.
+warn+:: The unknown parameter will cause a warning to be issued (+warn+).
This is the default and can allow errors in configuration files to
be detected more easily. It does NOT prevent the configuration
file from being loaded, however.
+error+:: The unknown parameter will cause an error to be raised, preventing
the configuration file from being loaded.
== CONFIGURATION FILE FORMATS:
=== EXISTING FORMATS:
- YAML.txt describes working with YAML configuration files.
- KeyValue.txt describes working with key-value configuration files.
- Ruby.txt describes working with Ruby configuration files.
- Hash.txt describes loading configurations from Hashes and dumping
configurations to Hashes.
- Override.txt describes loading configurations from multiple sources (files),
allowing the configuration from one source to override that from another
source.
=== NEW FORMATS:
The ConfigToolkit easily can be extended to support new file formats. A new
reader class can be created to load from a new format, and a new writer class
can be created to write out a new format.
The reader interface is documented by the ConfigToolkit::Reader class.
Basically, a reader needs to have a read method that reads data from some
underlying stream and returns a Hash that represents the configuration (see
ConfigToolkit::Reader#read for the specifications;
ConfigToolkit::YAMLReader#read is a simple example of an implementation).
ConfigToolkit::HashReader is a no-op reader, so the Hash that its constructor
accepts is the same that a new reader class should return from its
+read+ method.
The writer interface is documented by the ConfigToolkit::Writer class.
Basically, a writer needs to have a write method that accepts a Hash
representing a configuration and writes it to some underlying stream (see
ConfigToolkit::Writer#write for the specifications;
ConfigToolkit::YAMLWriter#write is a relatively simple example of an
implementation). ConfigToolkit::HashWriter is a no-op writer, so the Hash
stored in its +config_hash+ attribute after a ConfigToolkit::BaseConfig#dump
call is the same Hash passed to a writer's +write+ method.
While writing a new reader or writer class, it may be necessary to iterate
over a Hash that contains arbitrarily deeply nested Hashes and Arrays. The
ConfigToolkit::HashArrayVisitor can help with this task by allowing hook
functions to be written that will be called when a particular type of node
is visited. ConfigToolkit::YAMLWriter#write,
ConfigToolkit::KeyValueWriter#write and ConfigToolkit::PrettyPrintWriter#write
all use such visitors.
Feel free also to request that we add support for a new file format to the
distribution, and we will try our best to accomodate! See below for some
ideas that we have had for new readers and writers.
== CONFIGURATION CLASS DOCUMENTATION:
Configuration classes can be documented properly by +rdoc+, although not by
default. See FAQ.txt for full details.
== REQUIREMENTS:
Hoe, assertions, and relative are required, but only for running the tests.
Relative also is required for running the examples.
== INSTALL:
sudo gem install configtoolkit
== POSSIBLE FUTURE TECHNICAL DIRECTIONS:
Here are some possible ideas:
- Modify the ConfigToolkit and/or +rdoc+ so that +rdoc+ produces good
documentation for configuration classes (perhaps showing parameter value
classes and any default values). This enhancement should eliminate
any need to patch +rdoc+ (see FAQ.txt for the current procedure).
- Provide support for sourcing configuration from XML files
(+XMLReader+ and +XMLWriter+ classes)
- Provide support for sourcing configuration from INI files
(+INIReader+ and +INIWriter+ classes)
- Provide support for sourcing configuration from command-line parameters
(+CommandLineReader+ and +CommandLineWriter+ classes)
- Provide support for sourcing configuration from environment variables
(+EnvVarReader+ and +EnvVarWriter+ classes)
- Provide support for sourcing configuration from a database
(+DatabaseReader+ and +DatabaseWriter+ classes)
- Provide support for sourcing configuration from a configuration file (of
any format) containing embedded Ruby (+EmbeddedRubyReader+ class, which
would process the embedded Ruby and then hand the resulting file off
to another reader class)
- Allow more powerful parameter specifications to be specified. For instance,
it would be nice if users could specify that the parameter is a Pathname
to a readable file. Possibly the ConfigToolkit should provide a Rules
interface to create new parameter rules.
- Allow user-specified conversions from Strings to arbitrary types for
configuration file formats that only have a limited type palette (key-value
files, for instance).
- Allow other languages to use the ConfigToolkit (possible targets could
be C++, Python, or Java)
- Provide a ConfigToolkit::BaseConfig class method "constructor" that would
allocate any nested config instances and Arrays (recursively, so that
and multiply-nested config instances and Arrays also would be initialized).
This would make programatically setting the configuration parameters
very easy, since no explicit +new+ calls would be necessary for the
nested config object and Array parameters.
We would love to receive input from users about which new features are most
desirable!
== AUTHORS:
=== Designing Patterns
== SUPPORT:
Please post questions, concerns, or requests for enhancement to the forums on
the project page. Alternatively, direct contact information for
Designing Patterns can be found on the project page for this gem.
== ENHANCEMENTS:
Please feel free to contact us with any ideas; we will try our best to
enhance the software and respond to user requests. Of course, we are more
likely to work on a particular enhancement if we know that there are users
who want it. Designing Patterns provides contracting and consulting services,
so if there is an enhancement that must get done (and in a specified time
frame), please inquire about retaining our services!
== LICENSE:
The license text can be found in the +LICENSE+ file at the root of the
distribution.
This package is licensed with an MIT license:
Copyright (c) 2008-2009 Designing Patterns
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
== SHARE AND ENJOY!