configurability 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ChangeLog CHANGED
@@ -1,4 +1,43 @@
1
- 4[tip] 4d7044641f87 2010-07-12 13:38 -0700 ged
1
+ 17[tip] 4bff6d5f7b6e 2010-08-08 10:26 -0700 ged
2
+ Added tag 1.0.1 for changeset e3605ccbe057
3
+
4
+ 16[1.0.1] e3605ccbe057 2010-08-08 10:26 -0700 ged
5
+ Added signature for changeset 41bc1de0bf36
6
+
7
+ 15 41bc1de0bf36 2010-08-04 18:51 -0700 ged
8
+ More Configurability::Config work
9
+
10
+ 14 44db952eb824 2010-08-04 18:23 -0700 ged
11
+ More work on Configurability::Config
12
+
13
+ 13 2323bf260e7c 2010-08-04 16:28 -0700 ged
14
+ Finishing up Configurability::Config.
15
+
16
+ 12 9e0bab83afd5 2010-08-04 10:18 -0700 ged
17
+ Build system updates.
18
+
19
+ 11 cba63cef2f7f 2010-08-04 10:17 -0700 ged
20
+ Add an rspec shared behavior for testing classes with Configurability
21
+
22
+ 10 4ffdde3f7a2f 2010-07-16 16:55 -0700 ged
23
+ Adding a Configurability::Config class for YAML config loading.
24
+
25
+ 9 d225cef4269e 2010-07-16 16:54 -0700 ged
26
+ Updating README, project metadata, adding .irbrc
27
+
28
+ 8 9da882b82786 2010-07-16 16:53 -0700 ged
29
+ Version bump; debugging log
30
+
31
+ 7 ecf5d4565338 2010-07-12 15:52 -0700 ged
32
+ Added tag 1.0.0 for changeset 74b5dd9a89c9
33
+
34
+ 6[1.0.0] 74b5dd9a89c9 2010-07-12 15:52 -0700 ged
35
+ Added signature for changeset e0fef8dabba4
36
+
37
+ 5 e0fef8dabba4 2010-07-12 15:51 -0700 ged
38
+ README formatting, YARD tag fix.
39
+
40
+ 4 4d7044641f87 2010-07-12 13:38 -0700 ged
2
41
  Documentation, added a test for classname normalization.
3
42
 
4
43
  3 0b395dbf657f 2010-07-12 08:13 -0700 ged
data/README.md CHANGED
@@ -10,6 +10,7 @@ configuration is split up and sent to the objects that will use it.
10
10
  To add configurability to a class, just require the library and extend
11
11
  the class:
12
12
 
13
+ require 'configurability'
13
14
  class User
14
15
  extend Configurability
15
16
  end
@@ -35,7 +36,12 @@ _section name_ as a `Symbol` or a `String`:
35
36
 
36
37
  The section name is based on an object's _config key_, which is the name of
37
38
  the object that is being extended with all non-word characters converted into
38
- underscores (`_`) by default.
39
+ underscores (`_`) by default. It will also have any leading Ruby-style
40
+ namespaces stripped, e.g.,
41
+
42
+ MyClass -> :myclass
43
+ Acme::User -> :user
44
+ "J. Random Hacker" -> :j_random_hacker
39
45
 
40
46
  If the object responds to the `#name` method, then the return value of that
41
47
  method is used to derive the name. If it doesn't have a `#name` method, the
@@ -82,13 +88,131 @@ a `#configure` method that takes the config section as an argument:
82
88
  class WebServer
83
89
  extend Configurability
84
90
 
91
+ config_key :webserver
92
+
85
93
  def self::configure( configsection )
86
94
  @default_bind_addr = configsection[:host]
87
95
  @default_port = configsection[:port]
88
96
  end
89
97
  end
90
98
 
91
- If you still want the `config` variable to be set, just `super` from your implementation; don't if you don't want it to be set.
99
+ If you still want the `@config` variable to be set, just `super` from your implementation; don't if you don't want it to be set.
100
+
101
+
102
+ ## Configuration Objects
103
+
104
+ Configurability also includes `Configurability::Config`, a fairly simple
105
+ configuration object class that can be used to load a YAML configuration file,
106
+ and then present both a Hash-like and a Struct-like interface for reading
107
+ configuration sections and values; it's meant to be used in tandem with Configurability, but it's also useful on its own.
108
+
109
+ Here's a quick example to demonstrate some of its features. Suppose you have a
110
+ config file that looks like this:
111
+
112
+ ---
113
+ database:
114
+ development:
115
+ adapter: sqlite3
116
+ database: db/dev.db
117
+ pool: 5
118
+ timeout: 5000
119
+ testing:
120
+ adapter: sqlite3
121
+ database: db/testing.db
122
+ pool: 2
123
+ timeout: 5000
124
+ production:
125
+ adapter: postgres
126
+ database: fixedassets
127
+ pool: 25
128
+ timeout: 50
129
+ ldap:
130
+ uri: ldap://ldap.acme.com/dc=acme,dc=com
131
+ bind_dn: cn=web,dc=acme,dc=com
132
+ bind_pass: Mut@ge.Mix@ge
133
+ branding:
134
+ header: "#333"
135
+ title: "#dedede"
136
+ anchor: "#9fc8d4"
137
+
138
+ You can load this config like so:
139
+
140
+ require 'configurability/config'
141
+ config = Configurability::Config.load( 'examples/config.yml' )
142
+ # => #<Configurability::Config:0x1018a7c7016 loaded from
143
+ examples/config.yml; 3 sections: database, ldap, branding>
144
+
145
+ And then access it using struct-like methods:
146
+
147
+ config.database
148
+ # => #<Configurability::Config::Struct:101806fb816
149
+ {:development=>{:adapter=>"sqlite3", :database=>"db/dev.db", :pool=>5,
150
+ :timeout=>5000}, :testing=>{:adapter=>"sqlite3",
151
+ :database=>"db/testing.db", :pool=>2, :timeout=>5000},
152
+ :production=>{:adapter=>"postgres", :database=>"fixedassets",
153
+ :pool=>25, :timeout=>50}}>
154
+
155
+ config.database.development.adapter
156
+ # => "sqlite3"
157
+
158
+ config.ldap.uri
159
+ # => "ldap://ldap.acme.com/dc=acme,dc=com"
160
+
161
+ config.branding.title
162
+ # => "#dedede"
163
+
164
+ or using a Hash-like interface using either `Symbol`s, `String`s, or a mix of
165
+ both:
166
+
167
+ config[:branding][:title]
168
+ # => "#dedede"
169
+
170
+ config['branding']['header']
171
+ # => "#333"
172
+
173
+ config['branding'][:anchor]
174
+ # => "#9fc8d4"
175
+
176
+ You can install it via the Configurability interface:
177
+
178
+ config.install
179
+
180
+ Check to see if the file it was loaded from has changed since you
181
+ loaded it:
182
+
183
+ config.changed?
184
+ # => false
185
+
186
+ # Simulate changing the file by manually changing its mtime
187
+ File.utime( Time.now, Time.now, config.path )
188
+ config.changed?
189
+ # => true
190
+
191
+ If it has changed (or even if it hasn't), you can reload it, which automatically re-installs it via the Configurability interface:
192
+
193
+ config.reload
194
+
195
+ You can make modifications via the same Struct- or Hash-like interfaces and write the modified config back out to the same file:
196
+
197
+ config.database.testing.adapter = 'mysql'
198
+ config[:database]['testing'].database = 't_fixedassets'
199
+
200
+ then dump it to a YAML string:
201
+
202
+ config.dump
203
+ # => "--- \ndatabase: \n development: \n adapter: sqlite3\n
204
+ database: db/dev.db\n pool: 5\n timeout: 5000\n testing: \n
205
+ adapter: mysql\n database: t_fixedassets\n pool: 2\n timeout:
206
+ 5000\n production: \n adapter: postgres\n database:
207
+ fixedassets\n pool: 25\n timeout: 50\nldap: \n uri:
208
+ ldap://ldap.acme.com/dc=acme,dc=com\n bind_dn:
209
+ cn=web,dc=acme,dc=com\n bind_pass: Mut@ge.Mix@ge\nbranding: \n
210
+ header: \"#333\"\n title: \"#dedede\"\n anchor: \"#9fc8d4\"\n"
211
+
212
+ or write it back to the file it was loaded from:
213
+
214
+ config.write
215
+
92
216
 
93
217
 
94
218
  ## Development
data/Rakefile CHANGED
@@ -76,7 +76,7 @@ elsif VERSION_FILE.exist?
76
76
  PKG_VERSION = VERSION_FILE.read[ /VERSION\s*=\s*['"](\d+\.\d+\.\d+)['"]/, 1 ]
77
77
  end
78
78
 
79
- PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION )
79
+ PKG_VERSION = '0.0.0' unless defined?( PKG_VERSION ) && !PKG_VERSION.nil?
80
80
 
81
81
  PKG_FILE_NAME = "#{PKG_NAME.downcase}-#{PKG_VERSION}"
82
82
  GEM_FILE_NAME = "#{PKG_FILE_NAME}.gem"
@@ -168,7 +168,7 @@ include RakefileHelpers
168
168
 
169
169
  # Set the build ID if the mercurial executable is available
170
170
  if hg = which( 'hg' )
171
- id = IO.read('|-') or exec hg.to_s, 'id', '-n'
171
+ id = `#{hg} id -n`.chomp
172
172
  PKG_BUILD = "pre%03d" % [(id.chomp[ /^[[:xdigit:]]+/ ] || '1')]
173
173
  else
174
174
  PKG_BUILD = 'pre000'
@@ -237,7 +237,11 @@ GEMSPEC = Gem::Specification.new do |gem|
237
237
 
238
238
  gem.summary = PKG_SUMMARY
239
239
  gem.description = [
240
- "Configurability is mixin that allows you to add configurability to one or more classes, assign them each a section of the configuration, and then when the configuration is loaded, the class is given the section it requested.",
240
+ "Configurability is a mixin that allows you to add ",
241
+ "configurability to one or more classes, assign them ",
242
+ "each a section of the configuration, and then when ",
243
+ "the configuration is loaded, the class is given the ",
244
+ "section it requested.",
241
245
  ].join( "\n" )
242
246
 
243
247
  gem.authors = "Michael Granger"
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'configurability'
4
+ require 'spec'
5
+
6
+ share_examples_for "an object with Configurability" do
7
+
8
+ before( :each ) do
9
+ fail "this behavior expects the object under test to be in the @object " +
10
+ "instance variable" unless defined?( @object )
11
+ end
12
+
13
+ it "is extended with Configurability" do
14
+ Configurability.configurable_objects.should include( @object )
15
+ end
16
+
17
+ it "has a Symbol config key" do
18
+ @object.config_key.should be_a( Symbol )
19
+ end
20
+
21
+ it "has a config key that is a reasonable section name" do
22
+ @object.config_key.to_s.should =~ /^[a-z][a-z0-9]*$/i
23
+ end
24
+
25
+ end
26
+
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tmpdir'
4
+ require 'pathname'
5
+ require 'forwardable'
6
+ require 'yaml'
7
+ require 'logger'
8
+
9
+ require 'configurability'
10
+
11
+ # A configuration object class for systems with Configurability
12
+ #
13
+ # @author Michael Granger <ged@FaerieMUD.org>
14
+ # @author Mahlon E. Smith <mahlon@martini.nu>
15
+ #
16
+ # This class also delegates some of its methods to the underlying struct:
17
+ #
18
+ # @see Configurability::Config::Struct#to_hash
19
+ # #to_hash (delegated to its internal Struct)
20
+ # @see Configurability::Config::Struct#member?
21
+ # #member? (delegated to its internal Struct)
22
+ # @see Configurability::Config::Struct#members
23
+ # #members (delegated to its internal Struct)
24
+ # @see Configurability::Config::Struct#merge
25
+ # #merge (delegated to its internal Struct)
26
+ # @see Configurability::Config::Struct#merge!
27
+ # #merge! (delegated to its internal Struct)
28
+ # @see Configurability::Config::Struct#each
29
+ # #each (delegated to its internal Struct)
30
+ # @see Configurability::Config::Struct#[]
31
+ # #[] (delegated to its internal Struct)
32
+ # @see Configurability::Config::Struct#[]=
33
+ # #[]= (delegated to its internal Struct)
34
+ #
35
+ class Configurability::Config
36
+ extend Forwardable
37
+
38
+
39
+ #############################################################
40
+ ### C L A S S M E T H O D S
41
+ #############################################################
42
+
43
+ ### Read and return a Configurability::Config object from the file at the given +path+.
44
+ ### @param [String] path the path to the config file
45
+ ### @param [Hash] defaults a Hash of default config values which will be
46
+ ### used if the config at +path+ doesn't override
47
+ ### them.
48
+ ### @param block passed through as the block argument to {#initialize}.
49
+ def self::load( path, defaults=nil, &block )
50
+ path = Pathname( path ).expand_path
51
+ source = path.read
52
+ Configurability.log.debug "Read %d bytes from %s" % [ source.length, path ]
53
+ return new( source, path, defaults, &block )
54
+ end
55
+
56
+
57
+ ### Recursive hash-merge function. Used as the block argument to a Hash#merge.
58
+ ### @param [Symbol] key the key that's in conflict
59
+ ### @param [Object] oldval the value in the original Hash
60
+ ### @param [Object] newval the value in the Hash being merged
61
+ def self::merge_complex_hashes( key, oldval, newval )
62
+ return oldval.merge( newval, &method(:merge_complex_hashes) ) if
63
+ oldval.is_a?( Hash ) && newval.is_a?( Hash )
64
+ return newval
65
+ end
66
+
67
+
68
+
69
+ #############################################################
70
+ ### I N S T A N C E M E T H O D S
71
+ #############################################################
72
+
73
+ ### Create a new Configurability::Config object. If the optional +source+ argument
74
+ ### is specified, parse the config from it.
75
+ ###
76
+ ### @param [String] source the YAML source of the configuration
77
+ ### @param [String, Pathname] path the path to the config file (if loaded from a file)
78
+ ### @param [Hash] defaults a Hash containing default values which the loaded values
79
+ ### will be merged into.
80
+ ### @yield The block will be evaluated in the context of the config object after
81
+ ### the config is loaded, unless it accepts an argument, in which case the config
82
+ ### object is passed as the argument.
83
+ def initialize( source=nil, path=nil, defaults=nil, &block )
84
+
85
+ # Shift the hash parameter if it shows up as the path
86
+ if path.is_a?( Hash )
87
+ defaults = path
88
+ path = nil
89
+ end
90
+
91
+ # Make a deep copy of the defaults before loading so we don't modify
92
+ # the argument
93
+ @defaults = Marshal.load( Marshal.dump(defaults) ) if defaults
94
+ @time_created = Time.now
95
+ @path = path
96
+
97
+ if source
98
+ @struct = self.make_configstruct_from_source( source, @defaults )
99
+ else
100
+ @struct = Configurability::Config::Struct.new( @defaults )
101
+ end
102
+
103
+ if block
104
+ Configurability.log.debug "Block arity is: %p" % [ block.arity ]
105
+
106
+ # A block with an argument is called with the config as the argument
107
+ # instead of instance_evaled
108
+ case block.arity
109
+ when 0, -1 # 1.9 and 1.8, respectively
110
+ Configurability.log.debug "Instance evaling in the context of %p" % [ self ]
111
+ self.instance_eval( &block )
112
+ else
113
+ block.call( self )
114
+ end
115
+ end
116
+ end
117
+
118
+
119
+ ######
120
+ public
121
+ ######
122
+
123
+ # Define delegators to the inner data structure
124
+ def_delegators :@struct, :to_hash, :to_h, :member?, :members, :merge,
125
+ :merge!, :each, :[], :[]=
126
+
127
+ # @return [Configurability::Config::Struct] The underlying config data structure
128
+ attr_reader :struct
129
+
130
+ # @return [Time] The time the configuration was loaded
131
+ attr_accessor :time_created
132
+
133
+ # @return [Pathname] the path to the config file, if loaded from a file
134
+ attr_accessor :path
135
+
136
+
137
+ ### Install this config object in any objects that have added
138
+ ### Configurability.
139
+ def install
140
+ Configurability.configure_objects( self )
141
+ end
142
+
143
+
144
+ ### Return the config object as a YAML hash
145
+ def dump
146
+ strhash = stringify_keys( self.to_h )
147
+ return YAML.dump( strhash )
148
+ end
149
+
150
+
151
+ ### Write the configuration object using the specified name and any
152
+ ### additional +args+.
153
+ def write( path=@path, *args )
154
+ raise ArgumentError,
155
+ "No name associated with this config." unless path
156
+ path.open( File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
157
+ ofh.print( self.dump )
158
+ end
159
+ end
160
+
161
+
162
+ ### Returns +true+ for methods which can be autoloaded
163
+ def respond_to?( sym )
164
+ return true if @struct.member?( sym.to_s.sub(/(=|\?)$/, '').to_sym )
165
+ super
166
+ end
167
+
168
+
169
+ ### Returns +true+ if the configuration has changed since it was last
170
+ ### loaded, either by setting one of its members or changing the file
171
+ ### from which it was loaded.
172
+ def changed?
173
+ return self.changed_reason ? true : false
174
+ end
175
+
176
+
177
+ ### If the configuration has changed, return the reason. If it hasn't,
178
+ ### returns nil.
179
+ def changed_reason
180
+ if @struct.dirty?
181
+ Configurability.log.debug "Changed_reason: struct was modified"
182
+ return "Struct was modified"
183
+ end
184
+
185
+ if self.path && self.is_older_than?( self.path )
186
+ Configurability.log.debug "Source file (%s) has changed." % [ self.path ]
187
+ return "Config source (%s) has been updated since %s" %
188
+ [ self.path, self.time_created ]
189
+ end
190
+
191
+ return nil
192
+ end
193
+
194
+
195
+ ### Return +true+ if the specified +file+ is newer than the time the receiver
196
+ ### was created.
197
+ def is_older_than?( path )
198
+ return false unless path.exist?
199
+ st = path.stat
200
+ Configurability.log.debug "File mtime is: %s, comparison time is: %s" %
201
+ [ st.mtime, @time_created ]
202
+ return st.mtime > @time_created
203
+ end
204
+
205
+
206
+ ### Reload the configuration from the original source if it has
207
+ ### changed. Returns +true+ if it was reloaded and +false+ otherwise.
208
+ ###
209
+ def reload
210
+ raise "can't reload from an in-memory source" unless self.path
211
+
212
+ if self.changed?
213
+ self.time_created = Time.now
214
+ source = self.path.read
215
+ @struct = self.make_configstruct_from_source( source, @defaults )
216
+
217
+ self.install
218
+ return true
219
+ else
220
+ return false
221
+ end
222
+ end
223
+
224
+
225
+ ### Return a human-readable, compact representation of the configuration
226
+ ### suitable for debugging.
227
+ def inspect
228
+ return "#<%s:0x%0x16 loaded from %s; %d sections: %s>" % [
229
+ self.class.name,
230
+ self.object_id * 2,
231
+ self.path ? self.path : "memory",
232
+ self.struct.members.length,
233
+ self.struct.members.join( ', ' )
234
+ ]
235
+ end
236
+
237
+
238
+ #########
239
+ protected
240
+ #########
241
+
242
+
243
+ ### Read in the specified +filename+ and return a config struct.
244
+ ### @param [String] source the YAML source to be converted
245
+ ### @return [Configurability::Config::Struct] the converted config struct
246
+ def make_configstruct_from_source( source, defaults=nil )
247
+ defaults ||= {}
248
+ mergefunc = Configurability::Config.method( :merge_complex_hashes )
249
+ hash = YAML.load( source )
250
+ ihash = symbolify_keys( untaint_values(hash) )
251
+ mergedhash = defaults.merge( ihash, &mergefunc )
252
+
253
+ return Configurability::Config::Struct.new( mergedhash )
254
+ end
255
+
256
+
257
+ ### Handle calls to struct-members
258
+ def method_missing( sym, *args )
259
+ key = sym.to_s.sub( /(=|\?)$/, '' ).to_sym
260
+
261
+ self.class.class_eval %{
262
+ def #{key}; @struct.#{key}; end
263
+ def #{key}=(arg); @struct.#{key} = arg; end
264
+ def #{key}?; @struct.#{key}?; end
265
+ }
266
+
267
+ return self.method( sym ).call( *args )
268
+ end
269
+
270
+
271
+ #######
272
+ private
273
+ #######
274
+
275
+ ### Return a copy of the specified +hash+ with all of its values
276
+ ### untainted.
277
+ def untaint_values( hash )
278
+ newhash = {}
279
+ hash.each do |key,val|
280
+ case val
281
+ when Hash
282
+ newhash[ key ] = untaint_values( hash[key] )
283
+
284
+ when Array
285
+ newval = val.collect {|v| v.dup.untaint}
286
+ newhash[ key ] = newval
287
+
288
+ when NilClass, TrueClass, FalseClass, Numeric, Symbol
289
+ newhash[ key ] = val
290
+
291
+ else
292
+ newval = val.dup
293
+ newval.untaint
294
+ newhash[ key ] = newval
295
+ end
296
+ end
297
+ return newhash
298
+ end
299
+
300
+
301
+ ### Return a duplicate of the given +hash+ with its identifier-like keys
302
+ ### transformed into symbols from whatever they were before.
303
+ def symbolify_keys( hash )
304
+ newhash = {}
305
+ hash.each do |key,val|
306
+ if val.is_a?( Hash )
307
+ newhash[ key.to_sym ] = symbolify_keys( val )
308
+ else
309
+ newhash[ key.to_sym ] = val
310
+ end
311
+ end
312
+
313
+ return newhash
314
+ end
315
+
316
+
317
+ ### Return a version of the given +hash+ with its keys transformed
318
+ ### into Strings from whatever they were before.
319
+ def stringify_keys( hash )
320
+ newhash = {}
321
+ hash.each do |key,val|
322
+ if val.is_a?( Hash )
323
+ newhash[ key.to_s ] = stringify_keys( val )
324
+ else
325
+ newhash[ key.to_s ] = val
326
+ end
327
+ end
328
+
329
+ return newhash
330
+ end
331
+
332
+
333
+
334
+ #############################################################
335
+ ### I N T E R I O R C L A S S E S
336
+ #############################################################
337
+
338
+ ### Hash-wrapper that allows struct-like accessor calls on nested
339
+ ### hashes.
340
+ class Struct
341
+ extend Forwardable
342
+ include Enumerable
343
+
344
+ # Mask most of Kernel's methods away so they don't collide with
345
+ # config values.
346
+ Kernel.methods(false).each {|meth|
347
+ next unless method_defined?( meth )
348
+ next if /^(?:__|dup|object_id|inspect|class|raise|method_missing)/.match( meth )
349
+ undef_method( meth )
350
+ }
351
+
352
+
353
+ ### Create a new ConfigStruct using the values from the given +hash+ if specified.
354
+ ### @param [Hash] hash a hash of config values
355
+ def initialize( hash=nil )
356
+ hash ||= {}
357
+ @hash = hash.dup
358
+ @dirty = false
359
+ end
360
+
361
+
362
+ ######
363
+ public
364
+ ######
365
+
366
+ # Forward some methods to the internal hash
367
+ def_delegators :@hash, :keys, :key?, :values, :value?, :length,
368
+ :empty?, :clear, :each
369
+
370
+ # Let :each be called as :each_section, too
371
+ alias_method :each_section, :each
372
+
373
+
374
+ ### Return the value associated with the specified +key+.
375
+ ### @param [Symbol, String] key the key of the value to return
376
+ ### @return [Object] the value associated with +key+, or another
377
+ ### Configurability::Config::ConfigStruct if +key+
378
+ ### is a section name.
379
+ def []( key )
380
+ key = key.untaint.to_sym
381
+
382
+ # Create the config struct on the fly for subsections
383
+ if !@hash.key?( key )
384
+ @hash[ key ] = self.class.new
385
+ elsif @hash[ key ].is_a?( Hash )
386
+ @hash[ key ] = self.class.new( @hash[key] )
387
+ end
388
+
389
+ return @hash[ key ]
390
+ end
391
+
392
+
393
+ ### Set the value associated with the specified +key+ to +value+.
394
+ ### @param [Symbol, String] key the key of the value to set
395
+ ### @param [Object] value the value to set
396
+ def []=( key, value )
397
+ key = key.untaint.to_sym
398
+ self.mark_dirty if @hash[ key ] != value
399
+ @hash[ key ] = value
400
+ end
401
+
402
+
403
+ ### Mark the struct has having been modified since its creation.
404
+ def mark_dirty
405
+ @dirty = true
406
+ end
407
+
408
+
409
+ ### Returns +true+ if the ConfigStruct or any of its sub-structs
410
+ ### have changed since it was created.
411
+ def dirty?
412
+ return true if @dirty
413
+ return true if @hash.values.find do |obj|
414
+ obj.respond_to?( :dirty? ) && obj.dirty?
415
+ end
416
+ end
417
+
418
+
419
+ ### Return the receiver's values as a (possibly multi-dimensional)
420
+ ### Hash with String keys.
421
+ def to_hash
422
+ rhash = {}
423
+ @hash.each {|k,v|
424
+ case v
425
+ when Configurability::Config::Struct
426
+ rhash[k] = v.to_h
427
+ when NilClass, FalseClass, TrueClass, Numeric
428
+ # No-op (can't dup)
429
+ rhash[k] = v
430
+ when Symbol
431
+ rhash[k] = v.to_s
432
+ else
433
+ rhash[k] = v.dup
434
+ end
435
+ }
436
+ return rhash
437
+ end
438
+ alias_method :to_h, :to_hash
439
+
440
+
441
+ ### Return +true+ if the receiver responds to the given
442
+ ### method. Overridden to grok autoloaded methods.
443
+ def respond_to?( sym, priv=false )
444
+ key = sym.to_s.sub( /(=|\?)$/, '' ).to_sym
445
+ return true if @hash.key?( key )
446
+ super
447
+ end
448
+
449
+
450
+ ### Returns an Array of Symbols, one for each of the struct's members.
451
+ def members
452
+ return @hash.keys
453
+ end
454
+
455
+
456
+ ### Returns +true+ if the given +name+ is the name of a member of
457
+ ### the receiver.
458
+ def member?( name )
459
+ return @hash.key?( name.to_s.to_sym )
460
+ end
461
+
462
+
463
+ ### Merge the specified +other+ object with this config struct. The
464
+ ### +other+ object can be either a Hash, another Configurability::Config::Struct, or an
465
+ ### Configurability::Config.
466
+ def merge!( other )
467
+ mergefunc = Configurability::Config.method( :merge_complex_hashes )
468
+
469
+ case other
470
+ when Hash
471
+ @hash = self.to_h.merge( other, &mergefunc )
472
+
473
+ when Configurability::Config::Struct
474
+ @hash = self.to_h.merge( other.to_h, &mergefunc )
475
+
476
+ when Configurability::Config
477
+ @hash = self.to_h.merge( other.struct.to_h, &mergefunc )
478
+
479
+ else
480
+ raise TypeError,
481
+ "Don't know how to merge with a %p" % other.class
482
+ end
483
+
484
+ # :TODO: Actually check to see if anything has changed?
485
+ @dirty = true
486
+
487
+ return self
488
+ end
489
+
490
+
491
+ ### Return a new Configurability::Config::Struct which is the result of merging the
492
+ ### receiver with the given +other+ object (a Hash or another
493
+ ### Configurability::Config::Struct).
494
+ def merge( other )
495
+ self.dup.merge!( other )
496
+ end
497
+
498
+
499
+ ### Return a human-readable representation of the Struct suitable for debugging.
500
+ def inspect
501
+ return "#<%s:%0x16 %p>" % [
502
+ self.class.name,
503
+ self.object_id * 2,
504
+ @hash,
505
+ ]
506
+ end
507
+
508
+
509
+ #########
510
+ protected
511
+ #########
512
+
513
+ ### Handle calls to key-methods
514
+ def method_missing( sym, *args )
515
+ key = sym.to_s.sub( /(=|\?)$/, '' ).to_sym
516
+
517
+ # Create new methods for this key
518
+ reader = self.create_member_reader( key )
519
+ writer = self.create_member_writer( key )
520
+ predicate = self.create_member_predicate( key )
521
+
522
+ # ...and install them
523
+ self.class.send( :define_method, key, &reader )
524
+ self.class.send( :define_method, "#{key}=", &writer )
525
+ self.class.send( :define_method, "#{key}?", &predicate )
526
+
527
+ # Now jump to the requested method in a way that won't come back through
528
+ # the proxy method if something didn't get defined
529
+ self.method( sym ).call( *args )
530
+ end
531
+
532
+
533
+ ### Create a reader method for the specified +key+ and return it.
534
+ ### @param [Symbol] key the config key to create the reader method body for
535
+ ### @return [Proc] the body of the new method
536
+ def create_member_reader( key )
537
+ return lambda { self[key] }
538
+ end
539
+
540
+
541
+ ### Create a predicate method for the specified +key+ and return it.
542
+ ### @param [Symbol] key the config key to create the predicate method body for
543
+ ### @return [Proc] the body of the new method
544
+ def create_member_predicate( key )
545
+ return lambda { self[key] ? true : false }
546
+ end
547
+
548
+
549
+ ### Create a writer method for the specified +key+ and return it.
550
+ ### @param [Symbol] key the config key to create the writer method body for
551
+ ### @return [Proc] the body of the new method
552
+ def create_member_writer( key )
553
+ return lambda {|val| self[key] = val }
554
+ end
555
+
556
+ end # class Struct
557
+
558
+ end # class Configurability::Config
559
+
560
+ # vim: set nosta noet ts=4 sw=4:
561
+
@@ -9,10 +9,10 @@ require 'yaml'
9
9
  module Configurability
10
10
 
11
11
  # Library version constant
12
- VERSION = '1.0.0'
12
+ VERSION = '1.0.1'
13
13
 
14
14
  # Version-control revision constant
15
- REVISION = %q$Revision: e0fef8dabba4 $
15
+ REVISION = %q$Revision: 9da882b82786 $
16
16
 
17
17
  require 'configurability/logformatter.rb'
18
18
 
@@ -84,6 +84,9 @@ module Configurability
84
84
  ### +config_key+, the object's #configure method is called with +nil+
85
85
  ### instead.
86
86
  def self::configure_objects( config )
87
+ self.log.debug "Splitting up config %p between %d objects with configurability." %
88
+ [ config, self.configurable_objects.length ]
89
+
87
90
  self.configurable_objects.each do |obj|
88
91
  section = obj.config_key.to_sym
89
92
  self.log.debug "Configuring %p with the %p section of the config." %
@@ -53,6 +53,31 @@ begin
53
53
  class YARD::RegistryStore; include YardGlobals; end
54
54
  class YARD::Docstring; include YardGlobals; end
55
55
  module YARD::Templates::Helpers::ModuleHelper; include YardGlobals; end
56
+
57
+ if vvec(RUBY_VERSION) >= vvec("1.9.1")
58
+ # Monkeypatched to allow more than two '#' characters at the beginning
59
+ # of the comment line.
60
+ # Patched from yard-0.5.8
61
+ require 'yard/parser/ruby/ruby_parser'
62
+ class YARD::Parser::Ruby::RipperParser < Ripper
63
+ def on_comment(comment)
64
+ $stderr.puts "Adding comment: %p" % [ comment ]
65
+ visit_ns_token(:comment, comment)
66
+
67
+ comment = comment.gsub(/^\#+\s{0,1}/, '').chomp
68
+ append_comment = @comments[lineno - 1]
69
+
70
+ if append_comment && @comments_last_column == column
71
+ @comments.delete(lineno - 1)
72
+ comment = append_comment + "\n" + comment
73
+ end
74
+
75
+ @comments[lineno] = comment
76
+ @comments_last_column = column
77
+ end
78
+ end # class YARD::Parser::Ruby::RipperParser
79
+ end
80
+
56
81
  # </metamonkeypatch>
57
82
 
58
83
  YARD_OPTIONS = [] unless defined?( YARD_OPTIONS )
data/rake/hg.rb CHANGED
@@ -215,13 +215,20 @@ unless defined?( HG_DOTDIR )
215
215
  paths = get_repo_paths()
216
216
  if origin_url = paths['default']
217
217
  ask_for_confirmation( "Pull and update from '#{origin_url}'?", false ) do
218
- run 'hg', 'pull', '-u'
218
+ Rake::Task['hg:pull_without_confirmation'].invoke
219
219
  end
220
220
  else
221
221
  trace "Skipping pull: No 'default' path."
222
222
  end
223
223
  end
224
224
 
225
+
226
+ desc "Pull and update without confirmation"
227
+ task :pull_without_confirmation do
228
+ run 'hg', 'pull', '-u'
229
+ end
230
+
231
+
225
232
  desc "Check the current code in if tests pass"
226
233
  task :checkin => ['hg:pull', 'hg:newfiles', 'test', COMMIT_MSG_FILE] do
227
234
  targets = get_target_args()
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ BEGIN {
4
+ require 'pathname'
5
+ basedir = Pathname.new( __FILE__ ).dirname.parent.parent
6
+
7
+ libdir = basedir + "lib"
8
+
9
+ $LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
10
+ }
11
+
12
+ require 'tempfile'
13
+ require 'logger'
14
+ require 'fileutils'
15
+
16
+ require 'spec'
17
+ require 'spec/lib/helpers'
18
+
19
+ require 'configurability/config'
20
+
21
+
22
+
23
+ #####################################################################
24
+ ### C O N T E X T S
25
+ #####################################################################
26
+ describe Configurability::Config do
27
+ include Configurability::SpecHelpers
28
+
29
+ TEST_CONFIG = %{
30
+ ---
31
+ section:
32
+ subsection:
33
+ subsubsection: value
34
+ listsection:
35
+ - list
36
+ - values
37
+ - are
38
+ - neat
39
+ mergekey: Yep.
40
+ textsection: |-
41
+ With some text as the value
42
+ ...and another line.
43
+ }.gsub(/^\t/, '')
44
+
45
+
46
+ before( :all ) do
47
+ setup_logging( :fatal )
48
+ end
49
+
50
+ after( :all ) do
51
+ reset_logging()
52
+ end
53
+
54
+ it "can dump itself as YAML" do
55
+ Configurability::Config.new.dump.should == "--- {}\n\n"
56
+ end
57
+
58
+ it "returns nil as its change description" do
59
+ Configurability::Config.new.changed_reason.should be_nil()
60
+ end
61
+
62
+ it "autogenerates accessors for non-existant struct members" do
63
+ config = Configurability::Config.new
64
+ config.plugins.filestore.maxsize = 1024
65
+ config.plugins.filestore.maxsize.should == 1024
66
+ end
67
+
68
+ it "merges values loaded from the config with any defaults given" do
69
+ config = Configurability::Config.new( TEST_CONFIG, :defaultkey => "Oh yeah." )
70
+ config.defaultkey.should == "Oh yeah."
71
+ end
72
+
73
+ it "yields itself if a block is given at creation" do
74
+ yielded_self = nil
75
+ config = Configurability::Config.new { yielded_self = self }
76
+ yielded_self.should equal( config )
77
+ end
78
+
79
+ it "passes itself as the block argument if a block of arity 1 is given at creation" do
80
+ arg_self = nil
81
+ yielded_self = nil
82
+ config = Configurability::Config.new do |arg|
83
+ yielded_self = self
84
+ arg_self = arg
85
+ end
86
+ yielded_self.should_not equal( config )
87
+ arg_self.should equal( config )
88
+ end
89
+
90
+ it "supports both Symbols and Strings for Hash-like access" do
91
+ config = Configurability::Config.new( TEST_CONFIG )
92
+ config[:section]['subsection'][:subsubsection].should == 'value'
93
+ end
94
+
95
+
96
+ describe "created with in-memory YAML source" do
97
+
98
+ before(:each) do
99
+ @config = Configurability::Config.new( TEST_CONFIG )
100
+ end
101
+
102
+ it "responds to methods which are the same as struct members" do
103
+ @config.should respond_to( :section )
104
+ @config.section.should respond_to( :subsection )
105
+ @config.should_not respond_to( :pork_sausage )
106
+ end
107
+
108
+ it "contains values specified in the source" do
109
+ # section:
110
+ # subsection:
111
+ # subsubsection: value
112
+ @config.section.subsection.subsubsection.should == 'value'
113
+
114
+ # listsection:
115
+ # - list
116
+ # - values
117
+ # - are
118
+ # - neat
119
+ @config.listsection.should == %w[list values are neat]
120
+
121
+ # mergekey: Yep.
122
+ @config.mergekey.should == 'Yep.'
123
+
124
+ # textsection: |-
125
+ # With some text as the value
126
+ # ...and another line.
127
+ @config.textsection.should == "With some text as the value\n...and another line."
128
+ end
129
+
130
+ it "returns struct members as an Array of Symbols" do
131
+ @config.members.should be_an_instance_of( Array )
132
+ @config.members.should have_at_least( 4 ).things
133
+ @config.members.each do |member|
134
+ member.should be_an_instance_of( Symbol)
135
+ end
136
+ end
137
+
138
+ it "is able to iterate over sections" do
139
+ @config.each do |key, struct|
140
+ key.should be_an_instance_of( Symbol)
141
+ end
142
+ end
143
+
144
+ it "dumps values specified in the source" do
145
+ @config.dump.should =~ /^section:/
146
+ @config.dump.should =~ /^\s+subsection:/
147
+ @config.dump.should =~ /^\s+subsubsection:/
148
+ @config.dump.should =~ /^- list/
149
+ end
150
+
151
+ it "provides a human-readable description of itself when inspected" do
152
+ @config.inspect.should =~ /4 sections/i
153
+ @config.inspect.should =~ /mergekey/
154
+ @config.inspect.should =~ /textsection/
155
+ @config.inspect.should =~ /from memory/i
156
+ end
157
+
158
+ it "raises an exception when reloaded" do
159
+ expect {
160
+ @config.reload
161
+ }.to raise_exception( RuntimeError, /can't reload from an in-memory source/i )
162
+ end
163
+
164
+ end
165
+
166
+
167
+ # saving if changed since loaded
168
+ describe " whose internal values have been changed since loaded" do
169
+ before(:each) do
170
+ @config = Configurability::Config.new( TEST_CONFIG )
171
+ @config.section.subsection.anothersection = 11451
172
+ end
173
+
174
+
175
+ ### Specifications
176
+ it "should report that it is changed" do
177
+ @config.changed?.should == true
178
+ end
179
+
180
+ it "should report that its internal struct was modified as the reason for the change" do
181
+ @config.changed_reason.should =~ /struct was modified/i
182
+ end
183
+
184
+ end
185
+
186
+
187
+ # loading from a file
188
+ describe " loaded from a file" do
189
+ before(:all) do
190
+ @tmpfile = Tempfile.new( 'test.conf', '.' )
191
+ @tmpfile.print( TEST_CONFIG )
192
+ @tmpfile.close
193
+ end
194
+
195
+ after(:all) do
196
+ @tmpfile.delete
197
+ end
198
+
199
+
200
+ before(:each) do
201
+ @config = Configurability::Config.load( @tmpfile.path )
202
+ end
203
+
204
+
205
+ ### Specifications
206
+ it "remembers which file it was loaded from" do
207
+ @config.path.should == Pathname( @tmpfile.path ).expand_path
208
+ end
209
+
210
+ it "writes itself back to the same file by default" do
211
+ @config.port = 114411
212
+ @config.write
213
+ otherconfig = Configurability::Config.load( @tmpfile.path )
214
+
215
+ otherconfig.port.should == 114411
216
+ end
217
+
218
+ it "includes the name of the file in its inspect output" do
219
+ @config.inspect.should include( File.basename(@tmpfile.path) )
220
+ end
221
+
222
+ it "yields itself if a block is given at load-time" do
223
+ yielded_self = nil
224
+ config = Configurability::Config.load( @tmpfile.path ) do
225
+ yielded_self = self
226
+ end
227
+ yielded_self.should equal( config )
228
+ end
229
+
230
+ it "passes itself as the block argument if a block of arity 1 is given at load-time" do
231
+ arg_self = nil
232
+ yielded_self = nil
233
+ config = Configurability::Config.load( @tmpfile.path ) do |arg|
234
+ yielded_self = self
235
+ arg_self = arg
236
+ end
237
+ yielded_self.should_not equal( config )
238
+ arg_self.should equal( config )
239
+ end
240
+
241
+ it "doesn't re-read its source file if it hasn't changed" do
242
+ @config.path.should_not_receive( :read )
243
+ Configurability.should_not_receive( :configure_objects )
244
+ @config.reload.should be_false()
245
+ end
246
+ end
247
+
248
+
249
+ # reload if file changes
250
+ describe " whose file changes after loading" do
251
+ before(:all) do
252
+ @tmpfile = Tempfile.new( 'test.conf', '.' )
253
+ @tmpfile.print( TEST_CONFIG )
254
+ @tmpfile.close
255
+ end
256
+
257
+ after(:all) do
258
+ @tmpfile.delete
259
+ end
260
+
261
+
262
+ before(:each) do
263
+ old_date = Time.now - 3600
264
+ File.utime( old_date, old_date, @tmpfile.path )
265
+ @config = Configurability::Config.load( @tmpfile.path )
266
+ now = Time.now + 10
267
+ File.utime( now, now, @tmpfile.path )
268
+ end
269
+
270
+
271
+ ### Specifications
272
+ it "reports that it is changed" do
273
+ @config.should be_changed
274
+ end
275
+
276
+ it "reports that its source was updated as the reason for the change" do
277
+ @config.changed_reason.should =~ /source.*updated/i
278
+ end
279
+
280
+ it "re-reads its file when reloaded" do
281
+ @config.path.should_receive( :read ).and_return( TEST_CONFIG )
282
+ Configurability.should_receive( :configure_objects ).with( @config )
283
+ @config.reload.should be_true()
284
+ end
285
+
286
+ it "reapplies its defaults when reloading" do
287
+ config = Configurability::Config.load( @tmpfile.path, :defaultskey => 8 )
288
+ config.reload
289
+ config.defaultskey.should == 8
290
+ end
291
+ end
292
+
293
+
294
+ # merging
295
+ describe " created by merging two other configs" do
296
+ before(:each) do
297
+ @config1 = Configurability::Config.new
298
+ @config2 = Configurability::Config.new( TEST_CONFIG )
299
+ @merged = @config1.merge( @config2 )
300
+ end
301
+
302
+
303
+ ### Specifications
304
+ it "should contain values from both" do
305
+ @merged.mergekey.should == @config2.mergekey
306
+ end
307
+ end
308
+
309
+ end
310
+
311
+ # vim: set nosta noet ts=4 sw=4:
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 1
7
7
  - 0
8
- - 0
9
- version: 1.0.0
8
+ - 1
9
+ version: 1.0.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Michael Granger
@@ -14,11 +14,16 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-07-12 00:00:00 -07:00
17
+ date: 2010-08-08 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
21
- description: Configurability is mixin that allows you to add configurability to one or more classes, assign them each a section of the configuration, and then when the configuration is loaded, the class is given the section it requested.
21
+ description: |-
22
+ Configurability is a mixin that allows you to add
23
+ configurability to one or more classes, assign them
24
+ each a section of the configuration, and then when
25
+ the configuration is loaded, the class is given the
26
+ section it requested.
22
27
  email:
23
28
  - ged@FaerieMUD.org
24
29
  executables: []
@@ -34,8 +39,11 @@ files:
34
39
  - ChangeLog
35
40
  - README.md
36
41
  - LICENSE
42
+ - spec/configurability/config_spec.rb
37
43
  - spec/configurability_spec.rb
38
44
  - spec/lib/helpers.rb
45
+ - lib/configurability/behavior.rb
46
+ - lib/configurability/config.rb
39
47
  - lib/configurability/logformatter.rb
40
48
  - lib/configurability.rb
41
49
  - rake/191_compat.rb
@@ -86,5 +94,6 @@ signing_key:
86
94
  specification_version: 3
87
95
  summary: A configurability mixin for Ruby
88
96
  test_files:
97
+ - spec/configurability/config_spec.rb
89
98
  - spec/configurability_spec.rb
90
99
  - spec/lib/helpers.rb