configurable 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
- require 'lazydoc/attributes'
1
+ require 'lazydoc'
2
2
  require 'configurable/delegate_hash'
3
- require 'configurable/validation'
4
3
  require 'configurable/indifferent_access'
4
+ require 'configurable/validation'
5
5
 
6
6
  autoload(:ConfigParser, 'config_parser')
7
7
 
@@ -12,61 +12,63 @@ module Configurable
12
12
  module ClassMethods
13
13
  include Lazydoc::Attributes
14
14
 
15
- # A hash holding the class configurations.
15
+ # A hash of (key, Delegate) pairs defining the class configurations.
16
16
  attr_reader :configurations
17
17
 
18
18
  def self.extended(base) # :nodoc:
19
- caller.each_with_index do |line, index|
20
- case line
21
- when /\/configurable.rb/ then next
22
- when Lazydoc::CALLER_REGEXP
23
- base.instance_variable_set(:@source_file, File.expand_path($1))
24
- break
25
- end
19
+ unless base.instance_variable_defined?(:@source_file)
20
+ caller[2] =~ Lazydoc::CALLER_REGEXP
21
+ base.instance_variable_set(:@source_file, File.expand_path($1))
26
22
  end
27
-
28
- configurations = {}.extend IndifferentAccess
29
- base.instance_variable_set(:@configurations, configurations)
23
+
24
+ base.send(:initialize_configurations).extend(IndifferentAccess)
30
25
  end
31
26
 
32
27
  def inherited(child) # :nodoc:
33
28
  unless child.instance_variable_defined?(:@source_file)
34
- caller.first =~ Lazydoc::CALLER_REGEXP
29
+ caller[0] =~ Lazydoc::CALLER_REGEXP
35
30
  child.instance_variable_set(:@source_file, File.expand_path($1))
36
31
  end
37
-
38
- configurations = {}
39
- configurations.extend IndifferentAccess if @configurations.kind_of?(IndifferentAccess)
40
- @configurations.each_pair {|key, config| configurations[key] = config.dup }
41
- child.instance_variable_set(:@configurations, configurations)
32
+
33
+ # deep duplicate configurations
34
+ unless child.instance_variable_defined?(:@configurations)
35
+ duplicate = child.instance_variable_set(:@configurations, configurations.dup)
36
+ duplicate.each_pair {|key, config| duplicate[key] = config.dup }
37
+ duplicate.extend(IndifferentAccess) if configurations.kind_of?(IndifferentAccess)
38
+ end
42
39
  super
43
40
  end
44
-
45
- def parser
41
+
42
+ # Parses configurations from argv in a non-destructive manner by generating
43
+ # a ConfigParser using the configurations for self. Parsed configs are
44
+ # added to config (note that you must keep a separate reference to
45
+ # config as it is not returned by parse). The parser will is yielded to the
46
+ # block, if given, to register additonal options. Returns an array of the
47
+ # arguments that remain after parsing.
48
+ #
49
+ # See ConfigParser#parse for more information.
50
+ def parse(argv=ARGV, config={})
46
51
  ConfigParser.new do |parser|
47
- configurations.to_a.sort_by do |(key, config)|
48
- config.attributes[:order] || 0
49
- end.each do |(key, config)|
50
- parser.define(key, config.default, config.attributes)
51
- end
52
- end
52
+ parser.add(configurations)
53
+ yield(parser) if block_given?
54
+ end.parse(argv, config)
53
55
  end
54
-
55
- # Loads the contents of path as YAML. Returns an empty hash if the path
56
- # is empty, does not exist, or is not a file.
57
- def load_config(path)
58
- # the last check prevents YAML from auto-loading itself for empty files
59
- return {} if path == nil || !File.file?(path) || File.size(path) == 0
60
- YAML.load_file(path) || {}
56
+
57
+ # Same as parse, but removes parsed args from argv.
58
+ def parse!(argv=ARGV, config={})
59
+ argv.replace(parse(argv, config))
61
60
  end
62
61
 
63
62
  protected
64
-
65
- def use_indifferent_access(value=true)
66
- current = @configurations
67
- @configurations = value ? HashWithIndifferentAccess.new : {}
68
- current.each_pair do |key, value|
69
- @configurations[key] = value
63
+
64
+ # Sets configurations to symbolize keys for AGET ([]) and ASET([]=)
65
+ # operations, or not. By default, configurations will use
66
+ # indifferent access.
67
+ def use_indifferent_access(input=true)
68
+ if input
69
+ @configurations.extend(IndifferentAccess)
70
+ else
71
+ @configurations = configurations.dup
70
72
  end
71
73
  end
72
74
 
@@ -93,19 +95,16 @@ module Configurable
93
95
  # end
94
96
  # end
95
97
  #
96
- def config(key, value=nil, options={}, &block)
97
- # register with Lazydoc
98
- options[:desc] ||= Lazydoc.register_caller
98
+ def config(key, value=nil, attributes={}, &block)
99
+ attributes = merge_attributes(block, attributes)
99
100
 
100
101
  if block_given?
101
- options = default_options(block).merge!(options)
102
-
103
102
  instance_variable = "@#{key}".to_sym
104
- config_attr(key, value, options) do |input|
103
+ config_attr(key, value, attributes) do |input|
105
104
  instance_variable_set(instance_variable, yield(input))
106
105
  end
107
106
  else
108
- config_attr(key, value, options)
107
+ config_attr(key, value, attributes)
109
108
  end
110
109
  end
111
110
 
@@ -134,11 +133,11 @@ module Configurable
134
133
  # end
135
134
  # end
136
135
  #
137
- def config_attr(key, value=nil, options={}, &block)
138
- options = default_options(block).merge!(options)
139
-
136
+ def config_attr(key, value=nil, attributes={}, &block)
137
+ attributes = merge_attributes(block, attributes)
138
+
140
139
  # define the default public reader method
141
- reader = options.delete(:reader)
140
+ reader = attributes.delete(:reader)
142
141
 
143
142
  case reader
144
143
  when true
@@ -150,7 +149,7 @@ module Configurable
150
149
  end
151
150
 
152
151
  # define the default public writer method
153
- writer = options.delete(:writer)
152
+ writer = attributes.delete(:writer)
154
153
 
155
154
  if block_given? && writer != true
156
155
  raise ArgumentError, "a block may not be specified without writer == true"
@@ -165,10 +164,7 @@ module Configurable
165
164
  writer = "#{key}="
166
165
  end
167
166
 
168
- # register with Lazydoc
169
- options[:desc] ||= Lazydoc.register_caller
170
-
171
- configurations[key] = Delegate.new(reader, writer, value, options)
167
+ configurations[key] = Delegate.new(reader, writer, value, attributes)
172
168
  end
173
169
 
174
170
  # Adds a configuration to self accessing the configurations for the
@@ -197,10 +193,9 @@ module Configurable
197
193
  # b = B.new
198
194
  # b.config[:a] # => {:key => 'value'}
199
195
  #
200
- # Nest may be provided a block which receives the first value for
201
- # the nested config and is expected to initialize an instance of
202
- # configurable_class. In this case a reader for the instance is
203
- # created and access becomes quite natural.
196
+ # Nest may be provided a block which initializes an instance of
197
+ # configurable_class. In this case accessors for the instance
198
+ # are created and access becomes quite natural.
204
199
  #
205
200
  # class C
206
201
  # include Configurable
@@ -222,33 +217,114 @@ module Configurable
222
217
  #
223
218
  # c.config[:a] = {:key => 'three'}
224
219
  # c.a.key # => "three"
225
- #
226
- # Nesting with an initialization block creates private methods
227
- # that config[:a] uses to read and write the instance configurations;
228
- # these methods are "#{key}_config" and "#{key}_config=" by default,
229
- # but they may be renamed using the :reader and :writer options.
230
220
  #
231
- # Nest checks for recursive nesting and raises an error if
232
- # a recursive nest is detected.
221
+ # The initialize block executes in class context, much like config.
222
+ #
223
+ # # An equivalent class to illustrate class-context
224
+ # class EquivalentClass
225
+ # attr_reader :a, A
226
+ #
227
+ # INITIALIZE_BLOCK = lambda {|overrides| A.new(overrides) }
228
+ #
229
+ # def initialize(overrides={})
230
+ # @a = INITIALIZE_BLOCK.call(overrides[:a] || {})
231
+ # end
232
+ # end
233
+ #
234
+ # Nest checks for recursive nesting and raises an error if a recursive nest
235
+ # is detected.
236
+ #
237
+ # ==== Attributes
238
+ #
239
+ # Nesting with an initialization block creates the public accessor for the
240
+ # instance, private methods to read and write the instance configurations,
241
+ # and a private method to initialize the instance. The default names
242
+ # for these methods are listed with the attributes to override them:
243
+ #
244
+ # :instance_reader key
245
+ # :instance_writer "#{key}="
246
+ # :instance_initializer "#{key}_initialize"
247
+ # :reader "#{key}_config_reader"
248
+ # :writer "#{key}_config_writer"
249
+ #
250
+ # These attributes are ignored if no block is given; true/false/nil
251
+ # values are meaningless and will be treated as the default.
252
+ #
253
+ def nest(key, configurable_class, attributes={}, &block)
254
+ attributes = merge_attributes(block, attributes)
255
+
256
+ if block_given?
257
+ instance_variable = "@#{key}".to_sym
258
+ nest_attr(key, configurable_class, attributes) do |input|
259
+ instance_variable_set(instance_variable, yield(input))
260
+ end
261
+ else
262
+ nest_attr(key, configurable_class, attributes)
263
+ end
264
+ end
265
+
266
+ # Same as nest, except the initialize block executes in instance-context.
267
+ #
268
+ # class C
269
+ # include Configurable
270
+ # nest(:a, A) {|overrides| A.new(overrides) }
271
+ #
272
+ # def initialize(overrides={})
273
+ # initialize_config(overrides)
274
+ # end
275
+ # end
233
276
  #
234
- def nest(key, configurable_class, options={})
277
+ # # An equivalent class to illustrate instance-context
278
+ # class EquivalentClass
279
+ # attr_reader :a, A
280
+ #
281
+ # def a_initialize(overrides)
282
+ # A.new(overrides)
283
+ # end
284
+ #
285
+ # def initialize(overrides={})
286
+ # @a = send(:a_initialize, overrides[:a] || {})
287
+ # end
288
+ # end
289
+ #
290
+ def nest_attr(key, configurable_class, attributes={}, &block)
235
291
  unless configurable_class.kind_of?(Configurable::ClassMethods)
236
292
  raise ArgumentError, "not a Configurable class: #{configurable_class}"
237
293
  end
238
-
239
- reader = options.delete(:reader)
240
- writer = options.delete(:writer)
241
-
294
+
295
+ attributes = merge_attributes(block, attributes)
296
+
297
+ # add some tracking attributes
298
+ attributes[:receiver] ||= configurable_class
299
+
300
+ # remove method attributes
301
+ instance_reader = attributes.delete(:instance_reader)
302
+ instance_writer = attributes.delete(:instance_writer)
303
+ initializer = attributes.delete(:instance_initializer)
304
+ reader = attributes.delete(:reader)
305
+ writer = attributes.delete(:writer)
306
+
242
307
  if block_given?
243
308
  # define instance accessor methods
244
- instance_var = "@#{key}".to_sym
245
- reader = "#{key}_config" unless reader
246
- writer = "#{key}_config=" unless writer
247
-
309
+ instance_reader = boolean_select(instance_reader, key)
310
+ instance_writer = boolean_select(instance_writer, "#{key}=")
311
+ instance_var = "@#{instance_reader}".to_sym
312
+
313
+ initializer = boolean_select(reader, "#{key}_initialize")
314
+ reader = boolean_select(reader, "#{key}_config_reader")
315
+ writer = boolean_select(writer, "#{key}_config_writer")
316
+
248
317
  # the public accessor
249
- attr_reader key
250
- public(key)
251
-
318
+ attr_reader instance_reader
319
+
320
+ define_method(instance_writer) do |value|
321
+ instance_variable_set(instance_var, value)
322
+ end
323
+ public(instance_reader, instance_writer)
324
+
325
+ # the initializer
326
+ define_method(initializer, &block)
327
+
252
328
  # the reader returns the config for the instance
253
329
  define_method(reader) do
254
330
  instance_variable_get(instance_var).config
@@ -260,19 +336,16 @@ module Configurable
260
336
  if instance_variable_defined?(instance_var)
261
337
  instance_variable_get(instance_var).reconfigure(value)
262
338
  else
263
- instance_variable_set(instance_var, yield(value))
339
+ instance_variable_set(instance_var, send(initializer, value))
264
340
  end
265
341
  end
266
342
  private(reader, writer)
267
343
  else
268
344
  reader = writer = nil
269
345
  end
270
-
271
- # register with Lazydoc
272
- options[:desc] ||= Lazydoc.register_caller
273
346
 
274
- value = DelegateHash.new(configurable_class.configurations).update
275
- configurations[key] = Delegate.new(reader, writer, value, options)
347
+ value = DelegateHash.new(configurable_class.configurations)
348
+ configurations[key] = Delegate.new(reader, writer, value, attributes)
276
349
 
277
350
  check_infinite_nest(configurable_class.configurations)
278
351
  end
@@ -284,25 +357,97 @@ module Configurable
284
357
 
285
358
  private
286
359
 
287
- def default_options(block)
288
- Validation::ATTRIBUTES[block].merge(
289
- :reader => true,
290
- :writer => true,
291
- :order => configurations.length)
360
+ # a helper to select a value or the default, if the default is true,
361
+ # false, or nil. used by nest_attr to handle attributes
362
+ def boolean_select(value, default) # :nodoc:
363
+ case value
364
+ when true, false, nil then default
365
+ else value
366
+ end
292
367
  end
293
368
 
294
- # helper to recursively check a set of
295
- # configurations for an infinite nest
296
- def check_infinite_nest(configurations) # :nodoc:
297
- raise "infinite nest detected" if configurations == self.configurations
298
-
299
- configurations.each_pair do |key, config|
300
- config_hash = config.default(false)
369
+ # a helper to initialize configurations for the first time,
370
+ # mainly implemented as a hook for OrderedHashPatch
371
+ def initialize_configurations # :nodoc:
372
+ @configurations ||= {}
373
+ end
374
+
375
+ # a helper method to merge the default attributes for the block with
376
+ # the input attributes. also registers a Trailer description.
377
+ def merge_attributes(block, attributes) # :nodoc:
378
+ defaults = DEFAULT_ATTRIBUTES[nil].dup
379
+ defaults.merge!(DEFAULT_ATTRIBUTES[block]) if block
380
+ defaults.merge!(attributes)
381
+
382
+ # register with Lazydoc
383
+ defaults[:desc] ||= Lazydoc.register_caller(Lazydoc::Trailer, 2)
384
+
385
+ defaults
386
+ end
301
387
 
302
- if config_hash.kind_of?(DelegateHash)
303
- check_infinite_nest(config_hash.delegates)
388
+ # helper to recursively check for an infinite nest
389
+ def check_infinite_nest(delegates) # :nodoc:
390
+ raise "infinite nest detected" if delegates == self.configurations
391
+
392
+ delegates.each_pair do |key, delegate|
393
+ if delegate.is_nest?
394
+ check_infinite_nest(delegate.default(false).delegates)
304
395
  end
305
396
  end
306
397
  end
307
398
  end
308
- end
399
+ end
400
+
401
+ module Configurable
402
+
403
+ # Beginning with ruby 1.9, Hash tracks the order of insertion and methods
404
+ # like each_pair return pairs in order. Configurable leverages this feature
405
+ # to keep configurations in order for the command line documentation produced
406
+ # by ConfigParser.
407
+ #
408
+ # Pre-1.9 ruby implementations require a patched Hash that tracks insertion
409
+ # order. This very thin subclass of hash does that for ASET insertions and
410
+ # each_pair. OrderedHashPatches are used as the configurations object in
411
+ # Configurable classes for pre-1.9 ruby implementations and for nothing else.
412
+ class OrderedHashPatch < Hash
413
+ def initialize
414
+ super
415
+ @insertion_order = []
416
+ end
417
+
418
+ # ASET insertion, tracking insertion order.
419
+ def []=(key, value)
420
+ @insertion_order << key unless @insertion_order.include?(key)
421
+ super
422
+ end
423
+
424
+ # Keys, sorted into insertion order
425
+ def keys
426
+ super.sort_by do |key|
427
+ @insertion_order.index(key) || length
428
+ end
429
+ end
430
+
431
+ # Yields each key-value pair to the block in insertion order.
432
+ def each_pair
433
+ keys.each do |key|
434
+ yield(key, fetch(key))
435
+ end
436
+ end
437
+
438
+ # Ensures the insertion order of duplicates is separate from parents.
439
+ def initialize_copy(orig)
440
+ super
441
+ @insertion_order = orig.instance_variable_get(:@insertion_order).dup
442
+ end
443
+ end
444
+
445
+ module ClassMethods
446
+ undef_method :initialize_configurations
447
+
448
+ # applies OrderedHashPatch
449
+ def initialize_configurations # :nodoc:
450
+ @configurations ||= OrderedHashPatch.new
451
+ end
452
+ end
453
+ end if RUBY_VERSION < '1.9'
@@ -22,9 +22,9 @@ module Configurable
22
22
  # The writer method, by default key=
23
23
  attr_reader :writer
24
24
 
25
- # An array of metadata for self, used to present the
25
+ # An hash of metadata for self, used to present the
26
26
  # delegate in different contexts (ex on the command
27
- # line or web).
27
+ # line, in a web form, or a desktop app).
28
28
  attr_reader :attributes
29
29
 
30
30
  # Initializes a new Delegate with the specified key
@@ -36,6 +36,17 @@ module Configurable
36
36
 
37
37
  @attributes = attributes
38
38
  end
39
+
40
+ # Sets the value of an attribute.
41
+ def []=(key, value)
42
+ attributes[key] = value
43
+ end
44
+
45
+ # Returns the value for the specified attribute, or
46
+ # default, if the attribute is unspecified.
47
+ def [](key, default=nil)
48
+ attributes.has_key?(key) ? attributes[key] : default
49
+ end
39
50
 
40
51
  # Sets the default value for self.
41
52
  def default=(value)
@@ -61,6 +72,11 @@ module Configurable
61
72
  def writer=(value)
62
73
  @writer = value == nil ? value : value.to_sym
63
74
  end
75
+
76
+ # Returns true if the default value is a kind of DelegateHash.
77
+ def is_nest?
78
+ @default.kind_of?(DelegateHash)
79
+ end
64
80
 
65
81
  # True if another is a kind of Delegate with the same
66
82
  # reader, writer, and default value. Attributes are