configurable 0.1.0 → 0.3.0

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.
@@ -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