configurability 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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