config_object 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +214 -0
- data/Rakefile +47 -0
- data/lib/config_object.rb +319 -0
- data/spec/config_object_spec.rb +344 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/test_1.yml +13 -0
- data/spec/test_2.yaml +7 -0
- data/spec/test_files/item_1.yml +7 -0
- data/spec/test_files/item_2.txt +1 -0
- data/spec/test_files/item_3/name.yml +1 -0
- data/spec/test_files/item_3/object.yml +2 -0
- data/spec/test_files/item_5.yaml +2 -0
- metadata +86 -0
data/README.rdoc
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
= ConfigObject
|
2
|
+
|
3
|
+
The purpose of this gem is to have a standard way of configuring objects that is useable by other Ruby gems.
|
4
|
+
|
5
|
+
== Features
|
6
|
+
|
7
|
+
There are plenty of other gems that provide a configuration facility. This gem exists to provide a unique set of special features.
|
8
|
+
|
9
|
+
* Support for complex configuration objects that can contain their own logic
|
10
|
+
* Support for multiple configuration objects of the same class
|
11
|
+
* Support for different settings for different environments
|
12
|
+
* Configuration can be done from Ruby code or YAML files
|
13
|
+
* DRY up your configuration with defaults
|
14
|
+
* Configured values are frozen so they can't be accidentally modified
|
15
|
+
* Configuration can be reloaded at any time and notify observing objects
|
16
|
+
|
17
|
+
== Examples
|
18
|
+
|
19
|
+
For the examples, we'll suppose we have some city data which is pretty static and doesn't change much.
|
20
|
+
|
21
|
+
To use this library, you just need to declare a class that includes ConfigObject. Including this module will add an +id+ attribute and an +initialize+ method that sets attributes from a hash. For each key in the hash, the initializer will look for a setter method and call it with the value. If there is no setter method, it will simply set an instance variable with the same name.
|
22
|
+
|
23
|
+
class City
|
24
|
+
include ConfigObject
|
25
|
+
attr_reader :name, :county, :state, :population, :census_year, :hostname
|
26
|
+
|
27
|
+
# Set the county information based on a hash. This feature can be used to create
|
28
|
+
# complex objects from simple configuration hashes.
|
29
|
+
def county= (attributes)
|
30
|
+
if attributes
|
31
|
+
@county = County.new(attributes)
|
32
|
+
else
|
33
|
+
@county = nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# You can use the objects as more than simple data stores because you can add whatever
|
38
|
+
# logic you like to the class
|
39
|
+
def size
|
40
|
+
if population > 1000000
|
41
|
+
:big
|
42
|
+
elsif population > 300000
|
43
|
+
:medium
|
44
|
+
else
|
45
|
+
:small
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# The initializer behavior of setting attributes from a hash is available in the module Attributes.
|
51
|
+
# If we just want that behavior, we can include it in any module.
|
52
|
+
class County
|
53
|
+
include ConfigObject::Attributes
|
54
|
+
attr_reader :name, :population
|
55
|
+
end
|
56
|
+
|
57
|
+
You can use a YAML file to load multiple cities like so:
|
58
|
+
|
59
|
+
chicago:
|
60
|
+
name: Chicago
|
61
|
+
state: IL
|
62
|
+
population: 2896016
|
63
|
+
census_year: 2000
|
64
|
+
county:
|
65
|
+
name: Cook
|
66
|
+
population: 5294664
|
67
|
+
|
68
|
+
st_louis:
|
69
|
+
name: St. Louis
|
70
|
+
state: MO
|
71
|
+
population: 348189
|
72
|
+
census_year: 2000
|
73
|
+
county:
|
74
|
+
name: St. Louis
|
75
|
+
population: 1016315
|
76
|
+
|
77
|
+
milwaukee:
|
78
|
+
name: Milwaukee
|
79
|
+
state: WI
|
80
|
+
population: 604477
|
81
|
+
census_year: 2008
|
82
|
+
county:
|
83
|
+
name: Milwaukee
|
84
|
+
population: 953328
|
85
|
+
|
86
|
+
== Multiple Objects
|
87
|
+
|
88
|
+
The above configuration will give us three cities with ids of "chicago", "st_louis", and "milwaukee". These object can be reference from the class:
|
89
|
+
|
90
|
+
City[:chicago]
|
91
|
+
|
92
|
+
or
|
93
|
+
|
94
|
+
City['chicago']
|
95
|
+
|
96
|
+
If we want all of them, we can call
|
97
|
+
|
98
|
+
City.all
|
99
|
+
|
100
|
+
If we want to find by something other than the id using a filter hash:
|
101
|
+
|
102
|
+
City.find(:name => "Chicago")
|
103
|
+
City.all(:state => "IL")
|
104
|
+
|
105
|
+
You can match values using a regular expression:
|
106
|
+
|
107
|
+
City.find(:name => /^St/)
|
108
|
+
City.all(:state => /(IL)|(WI)/)
|
109
|
+
|
110
|
+
You can use a dot syntax to specify a chain of attributes to call:
|
111
|
+
|
112
|
+
City.all("county.name" => 'Milwaukee')
|
113
|
+
|
114
|
+
If an attribute in the filter takes a single argument, it will be called with the match value:
|
115
|
+
|
116
|
+
City.all("county.population.>", 2000000)
|
117
|
+
|
118
|
+
The result of finding by a hash are cached for future lookups so their is no performance penalty for calling them multiple times.
|
119
|
+
|
120
|
+
== Configuring
|
121
|
+
|
122
|
+
As show above, you can configure a class with a YAML file. You can also specify multiple YAML files, a directory containing YAML files, or specify your configuration from Ruby code. When you specify multiple files, the setting in the later files will be merged into the settings in the earlier files in the list.
|
123
|
+
|
124
|
+
=== One YAML file
|
125
|
+
|
126
|
+
City.configuration_files = 'config/cities.yml'
|
127
|
+
|
128
|
+
=== Multiple YAML files
|
129
|
+
|
130
|
+
City.configuration_files = 'config/cities.yml', 'config/production_cities.yml'
|
131
|
+
|
132
|
+
=== Configure from code
|
133
|
+
City.configure({
|
134
|
+
:chicago => {:name => "Chicago", :population => 3000000},
|
135
|
+
:st_louis => {:name => "St. Louis", :population => 1000000}
|
136
|
+
})
|
137
|
+
|
138
|
+
== Multiple Environments
|
139
|
+
|
140
|
+
Often, your configuration will require different settings in different environments. There are a couple of ways to handle that.
|
141
|
+
|
142
|
+
First, if you use multiple objects, like in our city example, you can specify multiple configuration files or blocks depending on the environment.
|
143
|
+
|
144
|
+
For example, in a Rails application you could put your environment specific settings in separate files and call:
|
145
|
+
|
146
|
+
City.configuration_files = "config/cities.yml", "config/#{Rails.env}_cities.yml"
|
147
|
+
|
148
|
+
=== development_cities.yml
|
149
|
+
|
150
|
+
chicago:
|
151
|
+
host_name: chicago.local
|
152
|
+
|
153
|
+
st_louis:
|
154
|
+
host_name: st-louis.local
|
155
|
+
|
156
|
+
milwaukee:
|
157
|
+
host_name: milwaukee.local
|
158
|
+
|
159
|
+
=== production_cities.yml
|
160
|
+
|
161
|
+
chicago:
|
162
|
+
host_name: chicago.example.com
|
163
|
+
|
164
|
+
st_louis:
|
165
|
+
host_name: st-louis.example.com
|
166
|
+
|
167
|
+
milwaukee:
|
168
|
+
host_name: milwaukee.example.com
|
169
|
+
|
170
|
+
The other way to handle environment specific settings is if you only need one configuration object, use the environment name as the configuration id. For example, suppose we have a configuration class +HostConfig+ in a Rails application configured with this file:
|
171
|
+
|
172
|
+
development:
|
173
|
+
host: localhost
|
174
|
+
port: 5700
|
175
|
+
username: example
|
176
|
+
password: abc123
|
177
|
+
|
178
|
+
production:
|
179
|
+
host: example.com
|
180
|
+
port: 5700
|
181
|
+
username: example
|
182
|
+
password: $SEddd1
|
183
|
+
|
184
|
+
To get the correct settings you could then reference:
|
185
|
+
|
186
|
+
HostConfig[Rails.env]
|
187
|
+
|
188
|
+
== Default Values
|
189
|
+
|
190
|
+
If you have some attributes that are mostly the same across all configuration objects, you can specify default values to DRY up you configuration. For instance we could rewrite our sample +HostConfig+ file as:
|
191
|
+
|
192
|
+
defaults:
|
193
|
+
port: 5700
|
194
|
+
username: example
|
195
|
+
|
196
|
+
development:
|
197
|
+
host: localhost
|
198
|
+
password: abc123
|
199
|
+
|
200
|
+
production:
|
201
|
+
host: example.com
|
202
|
+
password: $SEddd1
|
203
|
+
|
204
|
+
Defaults can only be specified for root level hash. You can also specify defaults with the +set_defaults+ method.
|
205
|
+
|
206
|
+
== Stable Values
|
207
|
+
|
208
|
+
The attributes set on the configuration objects will be automatically frozen. This is to protect you from accidentally calling destructive methods on them (ie. <tt><<</tt> or +gsub!+ on String) and changing the original values. These sorts of bugs can be awfully hard to find especially in a Rails application where they may only appear when the code gets to production.
|
209
|
+
|
210
|
+
== Reloading
|
211
|
+
|
212
|
+
You can reload the configuration objects at any time with the +reload+ method.
|
213
|
+
|
214
|
+
Objects can register themselves with the configuration class to be notified whenever the configuration is reloaded by calling +add_observer+ and specifying a callback. The callback method or block will be called whenever the configuration is changed so that persistent objects that use the configuration can reinitialize themselves with the new values.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'spec/rake/spectask'
|
10
|
+
desc 'Run unit tests'
|
11
|
+
Spec::Rake::SpecTask.new(:test) do |t|
|
12
|
+
t.spec_files = FileList.new('spec/**/*_spec.rb')
|
13
|
+
end
|
14
|
+
rescue LoadError
|
15
|
+
task :test do
|
16
|
+
STDERR.puts "You must have rspec >= 1.2.9 to run the tests"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'Generate documentation for config_object.'
|
21
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
22
|
+
rdoc.rdoc_dir = 'rdoc'
|
23
|
+
rdoc.options << '--title' << 'ConfigObject' << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
|
24
|
+
rdoc.rdoc_files.include('README.rdoc')
|
25
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'jeweler'
|
30
|
+
Jeweler::Tasks.new do |gem|
|
31
|
+
gem.name = "config_object"
|
32
|
+
gem.summary = %Q{Simple and powerful configuration library}
|
33
|
+
gem.description = %Q{A configuration gem which is simple to use but full of awesome features.}
|
34
|
+
gem.email = "brian@embellishedvisions.com"
|
35
|
+
gem.homepage = "http://github.com/bdurand/config_object"
|
36
|
+
gem.authors = ["Brian Durand"]
|
37
|
+
gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile"].to_a
|
38
|
+
gem.has_rdoc = true
|
39
|
+
gem.extra_rdoc_files = ["README.rdoc"]
|
40
|
+
|
41
|
+
gem.add_development_dependency('rspec', '>= 1.2.9')
|
42
|
+
gem.add_development_dependency('jeweler')
|
43
|
+
end
|
44
|
+
|
45
|
+
Jeweler::GemcutterTasks.new
|
46
|
+
rescue LoadError
|
47
|
+
end
|
@@ -0,0 +1,319 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
# Classes that include ConfigObject can then be loaded from configuration using the ClassMethods. The objects
|
6
|
+
# themselves will have an +id+ method as well as an initializer that sets attributes from a hash (see Attributes
|
7
|
+
# for details).
|
8
|
+
module ConfigObject
|
9
|
+
|
10
|
+
def self.included (base) #:nodoc:
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
base.send :include, Attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
DEFAULTS = 'defaults'
|
17
|
+
|
18
|
+
# Find a configuration object by it's identifier or by a conditions hash.
|
19
|
+
#
|
20
|
+
# Identifiers are always looked up by string so find(:production) is the same as find('production').
|
21
|
+
#
|
22
|
+
# Condition hashes are used to select an item based on the field values. A match is made when either
|
23
|
+
# the attribute value matches a hash value. Hash values may be regular expressions. Matches are cached
|
24
|
+
# so there is no overhead in looking up a value by conditions multiple times.
|
25
|
+
#
|
26
|
+
# See #all for examples.
|
27
|
+
def find (id_or_conditions)
|
28
|
+
if id_or_conditions.is_a?(Hash)
|
29
|
+
lookup(id_or_conditions).first
|
30
|
+
else
|
31
|
+
configs[id_or_conditions.to_s]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :[], :find
|
36
|
+
|
37
|
+
# Find all configuration objects that match the conditions hash.
|
38
|
+
#
|
39
|
+
# Condition hashes are used to select an item based on the field values. A match is made when either
|
40
|
+
# the attribute value matches a hash value. Hash values may be regular expressions. Matches are cached
|
41
|
+
# so there is no overhead in looking up a value by conditions multiple times.
|
42
|
+
#
|
43
|
+
# Examples (assumes the City class includes ConfigObject):
|
44
|
+
#
|
45
|
+
# City.all(:name => "Springfield") # find all cities named Springfield
|
46
|
+
# City.all("state.name" => "Illinois") # find all cities where the state object has a name of Illinois
|
47
|
+
# City.all("population.>" => 1000000) # find all cities where population is greater than one million
|
48
|
+
# City.all("transportation.include?" => "train") # find all cities where transportation includes "train"
|
49
|
+
# City.all(:name => /^New/) # find all cities that start with "New"
|
50
|
+
def all (conditions = {})
|
51
|
+
if conditions.size == 0
|
52
|
+
configs.values
|
53
|
+
else
|
54
|
+
lookup(conditions)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get an array of ids for all configuration objects.
|
59
|
+
def ids
|
60
|
+
configs.keys
|
61
|
+
end
|
62
|
+
|
63
|
+
# Force the configuration objects to be reloaded from the configuration files.
|
64
|
+
# Any observers will be notified by invoking the callback provided.
|
65
|
+
def reload
|
66
|
+
@configs = nil
|
67
|
+
@cache = {}
|
68
|
+
notify_observers
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set default values for the attributes for all configuration objects.
|
73
|
+
#
|
74
|
+
# Calling this method multiple times will merge the default values with those from
|
75
|
+
# previous calls.
|
76
|
+
def set_defaults (values)
|
77
|
+
@defaults ||= {}
|
78
|
+
@defaults.merge!(stringify_keys(values))
|
79
|
+
reload
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add an observer to the configuration class. Whenever the configuration is reloaded,
|
83
|
+
# observers will be notified by either invoking the callback method provided or by
|
84
|
+
# calling the block.
|
85
|
+
def add_observer (observer, callback = nil, &block)
|
86
|
+
@observers ||= {}
|
87
|
+
@observers[observer] = callback || block
|
88
|
+
observer
|
89
|
+
end
|
90
|
+
|
91
|
+
# Remove an observer so it no longer receives notifications when the configuration is reloaded.
|
92
|
+
def remove_observer (observer)
|
93
|
+
@observers.delete(observer) if observer
|
94
|
+
observer
|
95
|
+
end
|
96
|
+
|
97
|
+
# Configure objects with a hash. This can be used in lieu of configuration files
|
98
|
+
# if you need to configure objects through code like in a Rails initializer.
|
99
|
+
# The argument passed in must be a hash where the keys are the configuration
|
100
|
+
# object ids and the values are the configuration attribute values.
|
101
|
+
#
|
102
|
+
# If this method is called multiple times, the values for each configuration
|
103
|
+
# object will be merged with the values from previous calls.
|
104
|
+
#
|
105
|
+
# Defaults can be set by using the special id "defaults" as one of the keys.
|
106
|
+
def configure (config_options)
|
107
|
+
@configure_hash ||= {}
|
108
|
+
config_options.each_pair do |id, values|
|
109
|
+
id = id.to_s
|
110
|
+
if id == DEFAULTS
|
111
|
+
set_defaults(values)
|
112
|
+
else
|
113
|
+
existing_values = @configure_hash[id] || {}
|
114
|
+
@configure_hash[id] = existing_values.merge(values)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
reload
|
118
|
+
end
|
119
|
+
|
120
|
+
# Set the files used to load the configuration objects. Each file will be read in the
|
121
|
+
# order specified. Values in later files will be merged into values specified in earlier
|
122
|
+
# files. This allows later files to serve as overrides for the earlier files and is
|
123
|
+
# ideal for specifying envrionment dependent settings.
|
124
|
+
def configuration_files= (*files)
|
125
|
+
@configuration_files = files.flatten.collect{|f| f.is_a?(Pathname) ? f : Pathname.new(f)}
|
126
|
+
reload
|
127
|
+
@configuration_files
|
128
|
+
end
|
129
|
+
|
130
|
+
# Get the list of files used to load the configuration. If the list is changed, you
|
131
|
+
# should call reload to ensure the new files are read.
|
132
|
+
def configuration_files
|
133
|
+
@configuration_files ||= []
|
134
|
+
end
|
135
|
+
|
136
|
+
# Clear all configuration settings. Mostly made available for testing purposes.
|
137
|
+
def clear
|
138
|
+
@configure_hash = nil
|
139
|
+
@configuration_files = nil
|
140
|
+
@defaults = nil
|
141
|
+
reload
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
# Notify all observers that the configuration has changed
|
147
|
+
def notify_observers
|
148
|
+
@observers.each_pair do |observer, callback|
|
149
|
+
args = []
|
150
|
+
callback = observer.method(callback) unless callback.is_a?(Proc)
|
151
|
+
args << self if callback.arity == 1 || callback.arity == -2
|
152
|
+
callback.call(*args)
|
153
|
+
end if @observers
|
154
|
+
end
|
155
|
+
|
156
|
+
# Determine if an object attribute matches the specified value.
|
157
|
+
def object_matches? (object, attribute, match_value) #:nodoc:
|
158
|
+
attribute = attribute.to_s.split('.') unless attribute.is_a?(Array)
|
159
|
+
method_name = attribute.first
|
160
|
+
if object.respond_to?(method_name)
|
161
|
+
arity = object.method(method_name).arity
|
162
|
+
# If this is the last attribute in a chain and the method requires a single argument, call the method with the match value.
|
163
|
+
if arity == 1 or arity == -2 and attribute.length == 1
|
164
|
+
return !!object.send(method_name, match_value)
|
165
|
+
end
|
166
|
+
value = object.send(method_name)
|
167
|
+
end
|
168
|
+
if value
|
169
|
+
if attribute.size > 1
|
170
|
+
return object_matches?(value, attribute[1, attribute.size], match_value)
|
171
|
+
else
|
172
|
+
if match_value.is_a?(Regexp)
|
173
|
+
return value.is_a?(String) && value.match(match_value)
|
174
|
+
else
|
175
|
+
return value == match_value
|
176
|
+
end
|
177
|
+
end
|
178
|
+
else
|
179
|
+
return match_value.nil?
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get the hash that contains all the configs mapping the ids to the objects.
|
184
|
+
def configs #:nodoc:
|
185
|
+
unless @configs
|
186
|
+
@cache = {}
|
187
|
+
hashes = {}
|
188
|
+
new_defaults = {}
|
189
|
+
configuration_files.each do |file|
|
190
|
+
file = Pathname.new(file) unless file.is_a?(Pathname)
|
191
|
+
load_yaml_file(file).each_pair do |id, values|
|
192
|
+
load_config_hash(hashes, new_defaults, id, values)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
if @configure_hash
|
196
|
+
@configure_hash.each do |id, values|
|
197
|
+
load_config_hash(hashes, new_defaults, id, values)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
@defaults ||= {}
|
201
|
+
@defaults = new_defaults.merge(@defaults)
|
202
|
+
@configs = {}
|
203
|
+
hashes.each_pair do |id, values|
|
204
|
+
@configs[id.to_s] = new(@defaults.merge(values.merge('id' => id)))
|
205
|
+
end
|
206
|
+
end
|
207
|
+
return @configs
|
208
|
+
end
|
209
|
+
|
210
|
+
# Look up items based on the conditions specified as a hash. Lookups are cached so they can be safely
|
211
|
+
# invoked over and over without incurring a performance penalty.
|
212
|
+
def lookup (conditions) #:nodoc:
|
213
|
+
@cache ||= {}
|
214
|
+
values = @cache[conditions]
|
215
|
+
unless values
|
216
|
+
values = configs.values.select do |obj|
|
217
|
+
conditions.all? do |attribute, match_value|
|
218
|
+
object_matches?(obj, attribute, match_value)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
@cache[conditions] = values
|
222
|
+
end
|
223
|
+
return values.dup
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
def load_config_hash (options, default_options, id, values) #:nodoc:
|
229
|
+
raise ArgumentError.new("Values defined for #{id} must be a hash") unless values.is_a?(Hash)
|
230
|
+
id = id.to_s
|
231
|
+
values = stringify_keys(values)
|
232
|
+
if id == DEFAULTS
|
233
|
+
default_options.merge!(values)
|
234
|
+
else
|
235
|
+
values = options[id].merge(values) if options[id]
|
236
|
+
options[id] = values
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def stringify_keys (hash) #:nodoc:
|
241
|
+
hash.inject({}) do |options, (key, value)|
|
242
|
+
options[key.to_s] = value
|
243
|
+
options
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Load a YAML file into a hash. If the specified file is a directory,
|
248
|
+
# all *.yml or *.yaml files will be loaded as the values with the file
|
249
|
+
# base name used as the key. This process is recursive.
|
250
|
+
def load_yaml_file (file) #:nodoc:
|
251
|
+
if file.exist? and (file.directory? or [".yaml", ".yml"].include?(file.extname.downcase))
|
252
|
+
if file.directory?
|
253
|
+
vals = {}
|
254
|
+
file.children.each do |child|
|
255
|
+
key = child.basename.to_s.sub(/\.y(a?)ml$/i, '')
|
256
|
+
vals[key] = load_yaml_file(child)
|
257
|
+
end
|
258
|
+
return vals
|
259
|
+
else
|
260
|
+
yaml = file.read
|
261
|
+
if yaml.nil? or yaml.gsub(/\s/, '').size == 0
|
262
|
+
return {}
|
263
|
+
else
|
264
|
+
return YAML.load(ERB.new(yaml).result) || {}
|
265
|
+
end
|
266
|
+
end
|
267
|
+
else
|
268
|
+
return {}
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# This module adds an initializer that sets attributes from a hash.
|
274
|
+
module Attributes
|
275
|
+
# Create a new configuration object with the specified attributes. Attributes are set by
|
276
|
+
# calling the setter method for each key if it exists. If there is no setter, an instance
|
277
|
+
# variable will be set. The value set will be duplicated and frozen if possible so that
|
278
|
+
# it can't be accidentally changed by calling a destructive method on it.
|
279
|
+
def initialize (attributes)
|
280
|
+
attributes.each_pair do |name, value|
|
281
|
+
setter = "#{name}=".to_sym
|
282
|
+
value = deep_freeze(value)
|
283
|
+
if respond_to?(setter)
|
284
|
+
send(setter, value)
|
285
|
+
else
|
286
|
+
instance_variable_set("@#{name}", value)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
protected
|
292
|
+
|
293
|
+
# Freeze a duplicate of the value passed in so that consumers of the configuration object
|
294
|
+
# don't accidentally change it by calling destructive methods on the original copy.
|
295
|
+
# For hashes and arrays recusively freezes every element.
|
296
|
+
def deep_freeze (value)
|
297
|
+
if value.is_a?(Hash)
|
298
|
+
frozen = {}
|
299
|
+
value.each_pair{|k, v| frozen[k] = deep_freeze(v)}
|
300
|
+
value = frozen
|
301
|
+
elsif value.is_a?(Array)
|
302
|
+
value = value.collect{|v| deep_freeze(v)}
|
303
|
+
else
|
304
|
+
if value and !value.is_a?(Numeric)
|
305
|
+
value = value.dup rescue value
|
306
|
+
end
|
307
|
+
end
|
308
|
+
value.freeze unless value.frozen?
|
309
|
+
return value
|
310
|
+
rescue
|
311
|
+
return value
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Id is the only attribute defined for all config objects. It will be set when the configuration is loaded.
|
316
|
+
def id
|
317
|
+
@id
|
318
|
+
end
|
319
|
+
end
|
@@ -0,0 +1,344 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
|
+
|
3
|
+
module ConfigObject
|
4
|
+
class Tester
|
5
|
+
include ConfigObject
|
6
|
+
|
7
|
+
attr_reader :value, :object
|
8
|
+
attr_accessor :name
|
9
|
+
|
10
|
+
def complex
|
11
|
+
@complex_called ||= 0
|
12
|
+
@complex_called += 1
|
13
|
+
@complex
|
14
|
+
end
|
15
|
+
|
16
|
+
def complex_called
|
17
|
+
@complex_called ||= 0
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def value= (val)
|
23
|
+
@value = val.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def object= (attributes)
|
27
|
+
@object = attributes.is_a?(Hash) ? Attributed.new(attributes) : attributes
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Tester2 < Tester
|
32
|
+
end
|
33
|
+
|
34
|
+
class Attributed
|
35
|
+
include ConfigObject::Attributes
|
36
|
+
attr_reader :name, :value, :object
|
37
|
+
|
38
|
+
def object= (attributes)
|
39
|
+
@object = attributes.is_a?(Hash) ? self.class.new(attributes) : attributes
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe ConfigObject do
|
45
|
+
|
46
|
+
after :each do
|
47
|
+
ConfigObject::Tester.clear
|
48
|
+
ConfigObject::Tester2.clear
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should be able to be configured with a hash" do
|
52
|
+
ConfigObject::Tester.configure({
|
53
|
+
:item_a => {:name => "Item A"},
|
54
|
+
:item_b => {:name => "Item B"}
|
55
|
+
})
|
56
|
+
ConfigObject::Tester['item_a'].name.should == "Item A"
|
57
|
+
ConfigObject::Tester[:item_b].name.should == "Item B"
|
58
|
+
ConfigObject::Tester['item_c'].should == nil
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should load configurations from a file" do
|
62
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml")
|
63
|
+
ConfigObject::Tester['item_1'].name.should == "Item One"
|
64
|
+
ConfigObject::Tester[:item_2].name.should == "Item Two"
|
65
|
+
ConfigObject::Tester['item_3'].should == nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should load configurations from multiple files overriding values from the first files with those from the later files" do
|
69
|
+
ConfigObject::Tester.configuration_files = Pathname.new(File.join(File.dirname(__FILE__), "test_1.yml"))
|
70
|
+
ConfigObject::Tester.configuration_files << File.join(File.dirname(__FILE__), "test_2.yaml")
|
71
|
+
ConfigObject::Tester['item_1'].name.should == "Item One"
|
72
|
+
ConfigObject::Tester[:item_2].name.should == "Item Too"
|
73
|
+
ConfigObject::Tester[:item_2].value.should == 2
|
74
|
+
ConfigObject::Tester[:item_2].complex.should == "Thing"
|
75
|
+
ConfigObject::Tester['item_3'].name.should == "Item Three"
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should load configurations from directories using the file names as the keys" do
|
79
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml"), File.join(File.dirname(__FILE__), "test_files")
|
80
|
+
ConfigObject::Tester[:item_1].name.should == "Item One"
|
81
|
+
ConfigObject::Tester[:item_1].value.should == 8
|
82
|
+
ConfigObject::Tester[:item_2].name.should == "Item Two" # Not overriddend by item_2.txt
|
83
|
+
ConfigObject::Tester[:item_3].name.should == "Item 3 Directory"
|
84
|
+
ConfigObject::Tester[:item_3].object.value.should == 10
|
85
|
+
ConfigObject::Tester[:item_5].name.should == "Item Five"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should load configurations from files only if they exist" do
|
89
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml"), "no_such_file.yaml"
|
90
|
+
ConfigObject::Tester['item_1'].name.should == "Item One"
|
91
|
+
ConfigObject::Tester[:item_2].name.should == "Item Two"
|
92
|
+
ConfigObject::Tester['item_3'].should == nil
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should evaluate ERB inside configuration files" do
|
96
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_2.yaml")
|
97
|
+
ConfigObject::Tester[:item_2].object.should == Date.today
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should load configuration from files and a hash with the hash taking precedence" do
|
101
|
+
ConfigObject::Tester.configure({
|
102
|
+
:item_a => {:name => "Item A"},
|
103
|
+
:item_1 => {:name => "Item Won"}
|
104
|
+
})
|
105
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml")
|
106
|
+
ConfigObject::Tester['item_a'].name.should == "Item A"
|
107
|
+
ConfigObject::Tester[:item_1].name.should == "Item Won"
|
108
|
+
ConfigObject::Tester[:item_2].name.should == "Item Two"
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should use default values set in a file for every configuration" do
|
112
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml")
|
113
|
+
ConfigObject::Tester[:item_1].value.should == 5
|
114
|
+
ConfigObject::Tester[:item_2].value.should == 1
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should let defaults be set manually" do
|
118
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml")
|
119
|
+
ConfigObject::Tester.set_defaults(:value => 6)
|
120
|
+
ConfigObject::Tester[:item_1].value.should == 5
|
121
|
+
ConfigObject::Tester[:item_2].value.should == 6
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should reload the configuration" do
|
125
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml")
|
126
|
+
ConfigObject::Tester[:item_1].name.should == "Item One"
|
127
|
+
ConfigObject::Tester[:item_3].should == nil
|
128
|
+
|
129
|
+
ConfigObject::Tester.configuration_files << File.join(File.dirname(__FILE__), "test_2.yaml")
|
130
|
+
ConfigObject::Tester[:item_3].should == nil
|
131
|
+
ConfigObject::Tester.reload
|
132
|
+
ConfigObject::Tester[:item_1].name.should == "Item One"
|
133
|
+
ConfigObject::Tester[:item_3].name.should == "Item Three"
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should reload automatically when the configuration files are set" do
|
137
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_1.yml")
|
138
|
+
ConfigObject::Tester[:item_1].name.should == "Item One"
|
139
|
+
ConfigObject::Tester[:item_3].should == nil
|
140
|
+
|
141
|
+
ConfigObject::Tester.configuration_files = File.join(File.dirname(__FILE__), "test_2.yaml")
|
142
|
+
ConfigObject::Tester[:item_1].should == nil
|
143
|
+
ConfigObject::Tester[:item_3].name.should == "Item Three"
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should reload automatically when a new configure is called" do
|
147
|
+
ConfigObject::Tester.configure({:item_a => {:name => "Item A"}})
|
148
|
+
ConfigObject::Tester[:item_a].name.should == "Item A"
|
149
|
+
ConfigObject::Tester[:item_b].should == nil
|
150
|
+
|
151
|
+
ConfigObject::Tester.configure({:item_b => {:name => "Item B"}})
|
152
|
+
ConfigObject::Tester[:item_a].name.should == "Item A"
|
153
|
+
ConfigObject::Tester[:item_b].name.should == "Item B"
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should reload automatically when the defaults are set" do
|
157
|
+
ConfigObject::Tester.configure({:item_a => {:name => "Item A"}})
|
158
|
+
ConfigObject::Tester[:item_a].name.should == "Item A"
|
159
|
+
ConfigObject::Tester[:item_a].value.should == nil
|
160
|
+
|
161
|
+
ConfigObject::Tester.set_defaults(:value => 14)
|
162
|
+
ConfigObject::Tester[:item_a].name.should == "Item A"
|
163
|
+
ConfigObject::Tester[:item_a].value.should == 14
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should notify observers with a callback method or block when the configuration is reloaded" do
|
167
|
+
ConfigObject::Tester.configure({:item_a => {:name => "Item A"}})
|
168
|
+
|
169
|
+
observer_1 = Object.new
|
170
|
+
def observer_1.update_config (config); @updated = config; end
|
171
|
+
def observer_1.updated_config?; @updated; end
|
172
|
+
ConfigObject::Tester.add_observer(observer_1, :update_config)
|
173
|
+
|
174
|
+
observer_2 = Object.new
|
175
|
+
observer_2_updated = nil
|
176
|
+
ConfigObject::Tester.add_observer(observer_2){|config| observer_2_updated = config}
|
177
|
+
|
178
|
+
observer_1.updated_config?.should == nil
|
179
|
+
observer_2_updated.should == nil
|
180
|
+
ConfigObject::Tester.reload
|
181
|
+
observer_1.updated_config?.should == ConfigObject::Tester
|
182
|
+
observer_2_updated.should == ConfigObject::Tester
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should be able to remove observers from being notified" do
|
186
|
+
ConfigObject::Tester.configure({:item_a => {:name => "Item A"}})
|
187
|
+
|
188
|
+
observer_1 = Object.new
|
189
|
+
observer_1_count = 0
|
190
|
+
ConfigObject::Tester.add_observer(observer_1){observer_1_count += 1}
|
191
|
+
|
192
|
+
observer_2 = Object.new
|
193
|
+
observer_2_count = 0
|
194
|
+
ConfigObject::Tester.add_observer(observer_2){observer_2_count += 1}
|
195
|
+
|
196
|
+
ConfigObject::Tester.reload
|
197
|
+
ConfigObject::Tester.remove_observer(observer_1)
|
198
|
+
ConfigObject::Tester.reload
|
199
|
+
observer_1_count.should == 1
|
200
|
+
observer_2_count.should == 2
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should get the ids of all configuration objects" do
|
204
|
+
ConfigObject::Tester.configure({
|
205
|
+
:item_a => {:name => "Item A"},
|
206
|
+
:item_b => {:name => "Item B"}
|
207
|
+
})
|
208
|
+
ConfigObject::Tester.ids.sort.should == ["item_a", "item_b"]
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should be able to find a configuration object by id" do
|
212
|
+
ConfigObject::Tester.configure({
|
213
|
+
:item_a => {:name => "Item A"},
|
214
|
+
:item_b => {:name => "Item B"}
|
215
|
+
})
|
216
|
+
ConfigObject::Tester.find("item_a").name.should == "Item A"
|
217
|
+
ConfigObject::Tester.find(:item_b).name.should == "Item B"
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should be able to find a configuration object by conditions" do
|
221
|
+
ConfigObject::Tester.configure({
|
222
|
+
:item_a => {:name => "Item A", :value => 10},
|
223
|
+
:item_b => {:name => "Item B"},
|
224
|
+
:item_c => {
|
225
|
+
:name => "Item C",
|
226
|
+
:value => 15,
|
227
|
+
:object => {:name => "Item C.1"},
|
228
|
+
:complex => [1, 2, 3]
|
229
|
+
}
|
230
|
+
})
|
231
|
+
ConfigObject::Tester.find("name" => "Item A").name.should == "Item A"
|
232
|
+
ConfigObject::Tester.find(:name => "Item B").name.should == "Item B"
|
233
|
+
ConfigObject::Tester.find(:name => /B/).name.should == "Item B"
|
234
|
+
ConfigObject::Tester.find(:value => 15).name.should == "Item C"
|
235
|
+
ConfigObject::Tester.find("object.name" => "Item C.1").name.should == "Item C"
|
236
|
+
ConfigObject::Tester.find("value.>" => 12).name.should == "Item C"
|
237
|
+
ConfigObject::Tester.find("complex.include?" => 1).name.should == "Item C"
|
238
|
+
ConfigObject::Tester.find("complex.include?" => 5).should == nil
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should be able to get all configuration objects" do
|
242
|
+
ConfigObject::Tester.configure({
|
243
|
+
:item_a => {:name => "Item A"},
|
244
|
+
:item_b => {:name => "Item B"}
|
245
|
+
})
|
246
|
+
ConfigObject::Tester.all.collect{|a| a.name}.should == ["Item A", "Item B"]
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should be able to get all configuration objects that match some conditions" do
|
250
|
+
ConfigObject::Tester.configure({
|
251
|
+
:item_a => {:name => "Item A", :value => 10},
|
252
|
+
:item_b => {:name => "Item B"},
|
253
|
+
:item_c => {
|
254
|
+
:name => "Item C",
|
255
|
+
:value => 15,
|
256
|
+
:object => {:name => "Item C.1"}
|
257
|
+
}
|
258
|
+
})
|
259
|
+
item_a = ConfigObject::Tester[:item_a]
|
260
|
+
item_b = ConfigObject::Tester[:item_b]
|
261
|
+
item_c = ConfigObject::Tester[:item_c]
|
262
|
+
ConfigObject::Tester.all("name" => "Item A").should == [item_a]
|
263
|
+
ConfigObject::Tester.all(:name => "Item B").should == [item_b]
|
264
|
+
ConfigObject::Tester.all(:name => /A|B/).sort{|a,b| a.name <=> b.name}.should == [item_a, item_b]
|
265
|
+
ConfigObject::Tester.all(:value => 15).should == [item_c]
|
266
|
+
ConfigObject::Tester.all("object.name" => "Item C.1").should == [item_c]
|
267
|
+
end
|
268
|
+
|
269
|
+
it "should cache the result of finding objects by conditions" do
|
270
|
+
ConfigObject::Tester.configure({
|
271
|
+
:item_a => {:name => "Item A", :complex => 1},
|
272
|
+
:item_b => {:name => "Item B", :complex => 2}
|
273
|
+
})
|
274
|
+
item_a = ConfigObject::Tester[:item_a]
|
275
|
+
item_b = ConfigObject::Tester[:item_b]
|
276
|
+
item_a.complex_called.should == 0
|
277
|
+
items = ConfigObject::Tester.all(:complex => 1).should == [item_a]
|
278
|
+
item_a.complex_called.should == 1
|
279
|
+
items = ConfigObject::Tester.all(:complex => 1).should == [item_a]
|
280
|
+
item_a.complex_called.should == 1
|
281
|
+
items = ConfigObject::Tester.all(:complex => 2).should == [item_b]
|
282
|
+
item_a.complex_called.should == 2
|
283
|
+
end
|
284
|
+
|
285
|
+
it "should call setter methods when initializing new objects" do
|
286
|
+
config = ConfigObject::Tester.new(:name => "Name", :value => "10")
|
287
|
+
config.name.should == "Name"
|
288
|
+
config.value.should == 10
|
289
|
+
end
|
290
|
+
|
291
|
+
it "should set instance variables when no setter is defined when intializing new objects" do
|
292
|
+
config = ConfigObject::Tester.new(:complex => "test", :no_attr => "here")
|
293
|
+
config.complex.should == "test"
|
294
|
+
config.instance_variable_get(:@no_attr).should == "here"
|
295
|
+
end
|
296
|
+
|
297
|
+
it "should duplicate and freeze the objects set in attributes" do
|
298
|
+
name = "Item A"
|
299
|
+
values = ["X", "Y"]
|
300
|
+
type = "array"
|
301
|
+
complex = {"values" => values, "type" => type}
|
302
|
+
ConfigObject::Tester.configure({:item_a => {:name => name, :complex => complex}})
|
303
|
+
item = ConfigObject::Tester[:item_a]
|
304
|
+
|
305
|
+
item.name.should == name
|
306
|
+
item.name.object_id.should_not == name.object_id
|
307
|
+
item.complex.should == complex
|
308
|
+
item.complex.object_id.should_not == complex.object_id
|
309
|
+
item.complex["values"].object_id.should_not == values.object_id
|
310
|
+
item.complex["type"].object_id.should_not == type.object_id
|
311
|
+
|
312
|
+
name.should_not be_frozen
|
313
|
+
complex.should_not be_frozen
|
314
|
+
values.should_not be_frozen
|
315
|
+
values.each{|v| v.should_not be_frozen}
|
316
|
+
type.should_not be_frozen
|
317
|
+
|
318
|
+
item.name.should be_frozen
|
319
|
+
item.complex.should be_frozen
|
320
|
+
item.complex["values"].should be_frozen
|
321
|
+
item.complex["values"].each{|v| v.should be_frozen}
|
322
|
+
item.complex["type"].should be_frozen
|
323
|
+
end
|
324
|
+
|
325
|
+
it "should not blow up when setting non-duplicable or non-freezable attributes" do
|
326
|
+
array = [1, 2]
|
327
|
+
array.freeze
|
328
|
+
ConfigObject::Tester.configure({:item_a => {:name => nil, :value => 1, :object => true, :complex => array}})
|
329
|
+
end
|
330
|
+
|
331
|
+
it "should have an id attribute that is set by the configuration" do
|
332
|
+
ConfigObject::Tester.configure(:item_a => {:name => "Item A"})
|
333
|
+
ConfigObject::Tester[:item_a].id.should == "item_a"
|
334
|
+
end
|
335
|
+
|
336
|
+
it "should not share objects between classes" do
|
337
|
+
ConfigObject::Tester.configure(:item_a => {:name => "Item A"})
|
338
|
+
ConfigObject::Tester.ids.should == ["item_a"]
|
339
|
+
ConfigObject::Tester2.ids.should == []
|
340
|
+
ConfigObject::Tester2.configure(:item_b => {:name => "Item B"})
|
341
|
+
ConfigObject::Tester.ids.should == ["item_a"]
|
342
|
+
ConfigObject::Tester2.ids.should == ["item_b"]
|
343
|
+
end
|
344
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/test_1.yml
ADDED
data/spec/test_2.yaml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
name: Item Two From Text File
|
@@ -0,0 +1 @@
|
|
1
|
+
Item 3 Directory
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: config_object
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-25 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.9
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: jeweler
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: A configuration gem which is simple to use but full of awesome features.
|
36
|
+
email: brian@embellishedvisions.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.rdoc
|
43
|
+
files:
|
44
|
+
- README.rdoc
|
45
|
+
- Rakefile
|
46
|
+
- lib/config_object.rb
|
47
|
+
- spec/config_object_spec.rb
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
- spec/test_1.yml
|
50
|
+
- spec/test_2.yaml
|
51
|
+
- spec/test_files/item_1.yml
|
52
|
+
- spec/test_files/item_2.txt
|
53
|
+
- spec/test_files/item_3/name.yml
|
54
|
+
- spec/test_files/item_3/object.yml
|
55
|
+
- spec/test_files/item_5.yaml
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/bdurand/config_object
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --charset=UTF-8
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.3.5
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Simple and powerful configuration library
|
84
|
+
test_files:
|
85
|
+
- spec/config_object_spec.rb
|
86
|
+
- spec/spec_helper.rb
|