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 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
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'config_object'))
data/spec/test_1.yml ADDED
@@ -0,0 +1,13 @@
1
+ item_1:
2
+ name: Item One
3
+ value: 5
4
+
5
+ item_2:
6
+ name: Item Two
7
+ complex: "Thing"
8
+
9
+ item_4:
10
+ name: Item Four
11
+
12
+ defaults:
13
+ value: 1
data/spec/test_2.yaml ADDED
@@ -0,0 +1,7 @@
1
+ item_2:
2
+ name: Item Too
3
+ value: 2
4
+ object: <%= Date.today.to_s %>
5
+
6
+ item_3:
7
+ name: Item Three
@@ -0,0 +1,7 @@
1
+ value: 8
2
+ object:
3
+ name: object one
4
+ value: 14
5
+ object:
6
+ name: object two
7
+ value: 15
@@ -0,0 +1 @@
1
+ name: Item Two From Text File
@@ -0,0 +1 @@
1
+ Item 3 Directory
@@ -0,0 +1,2 @@
1
+ name: object three
2
+ value: 10
@@ -0,0 +1,2 @@
1
+ name: Item Five
2
+ value: 10
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