gorillib 0.4.0pre → 0.4.1pre

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.
Files changed (54) hide show
  1. data/CHANGELOG.md +36 -1
  2. data/Gemfile +23 -19
  3. data/Guardfile +1 -1
  4. data/Rakefile +31 -31
  5. data/TODO.md +2 -30
  6. data/VERSION +1 -1
  7. data/examples/builder/ironfan.rb +4 -4
  8. data/gorillib.gemspec +40 -25
  9. data/lib/gorillib/array/average.rb +13 -0
  10. data/lib/gorillib/array/sorted_median.rb +11 -0
  11. data/lib/gorillib/array/sorted_percentile.rb +11 -0
  12. data/lib/gorillib/array/sorted_sample.rb +12 -0
  13. data/lib/gorillib/builder.rb +8 -14
  14. data/lib/gorillib/collection/has_collection.rb +31 -31
  15. data/lib/gorillib/collection/list_collection.rb +58 -0
  16. data/lib/gorillib/collection/model_collection.rb +63 -0
  17. data/lib/gorillib/collection.rb +57 -85
  18. data/lib/gorillib/logger/log.rb +26 -22
  19. data/lib/gorillib/model/base.rb +52 -39
  20. data/lib/gorillib/model/doc_string.rb +15 -0
  21. data/lib/gorillib/model/factories.rb +56 -61
  22. data/lib/gorillib/model/lint.rb +24 -0
  23. data/lib/gorillib/model/serialization.rb +12 -2
  24. data/lib/gorillib/model/validate.rb +2 -2
  25. data/lib/gorillib/pathname.rb +21 -6
  26. data/lib/gorillib/some.rb +2 -0
  27. data/lib/gorillib/type/extended.rb +0 -2
  28. data/lib/gorillib/type/url.rb +9 -0
  29. data/lib/gorillib/utils/console.rb +4 -1
  30. data/notes/HOWTO.md +22 -0
  31. data/notes/bucket.md +155 -0
  32. data/notes/builder.md +170 -0
  33. data/notes/collection.md +81 -0
  34. data/notes/factories.md +86 -0
  35. data/notes/model-overlay.md +209 -0
  36. data/notes/model.md +135 -0
  37. data/notes/structured-data-classes.md +127 -0
  38. data/spec/array/average_spec.rb +24 -0
  39. data/spec/array/sorted_median_spec.rb +18 -0
  40. data/spec/array/sorted_percentile_spec.rb +24 -0
  41. data/spec/array/sorted_sample_spec.rb +28 -0
  42. data/spec/gorillib/builder_spec.rb +46 -28
  43. data/spec/gorillib/collection_spec.rb +195 -10
  44. data/spec/gorillib/model/lint_spec.rb +28 -0
  45. data/spec/gorillib/model/record/factories_spec.rb +27 -13
  46. data/spec/gorillib/model/serialization_spec.rb +3 -5
  47. data/spec/gorillib/model_spec.rb +86 -104
  48. data/spec/spec_helper.rb +2 -1
  49. data/spec/support/gorillib_test_helpers.rb +83 -7
  50. data/spec/support/model_test_helpers.rb +9 -28
  51. metadata +52 -44
  52. data/lib/gorillib/configurable.rb +0 -28
  53. data/spec/gorillib/configurable_spec.rb +0 -62
  54. data/spec/support/shared_examples/included_module.rb +0 -20
@@ -12,25 +12,35 @@ module Gorillib
12
12
 
13
13
  def self.receive(type)
14
14
  case
15
- when type.is_a?(Proc) || type.is_a?(Method) then return Gorillib::Factory::ApplyProcFactory.new(type)
16
- when type.respond_to?(:receive) then return type
17
15
  when factories.include?(type) then return factories[type]
16
+ when type.respond_to?(:receive) then return type
17
+ when type.is_a?(Proc) || type.is_a?(Method) then return Gorillib::Factory::ApplyProcFactory.new(type)
18
18
  when type.is_a?(String) then
19
- return Gorillib::Inflector.constantize(Gorillib::Inflector.camelize(type.gsub(/\./, '/')))
19
+ return( factories[type] = Gorillib::Inflector.constantize(Gorillib::Inflector.camelize(type.gsub(/\./, '/'))) )
20
20
  else raise ArgumentError, "Don't know which factory makes a #{type}"
21
21
  end
22
22
  end
23
23
 
24
+ # Manufactures objects from their raw attributes hash
25
+ #
26
+ # A hash with a value for `:_type` is dispatched to the corresponding factory
27
+ # Everything else is returned directly
28
+ def self.make(obj)
29
+ if obj.respond_to?(:has_key?) && (obj.has_key?(:_type) || obj.has_key?('_type'))
30
+ factory = Gorillib::Factory(attrs[:_type])
31
+ factory.receive(obj)
32
+ else
33
+ obj
34
+ end
35
+ end
36
+
24
37
  private
25
38
  def self.factories
26
- @factories ||= Gorillib::Collection.new.tap{|f| f.key_method = :name }
39
+ @factories ||= Hash.new
27
40
  end
28
41
  public
29
42
 
30
- def self.register_factory(factory, *typenames)
31
- if typenames.blank?
32
- typenames = [factory.typename, factory.product]
33
- end
43
+ def self.register_factory(factory, typenames)
34
44
  typenames.each{|typename| factories[typename] = factory }
35
45
  end
36
46
 
@@ -38,42 +48,27 @@ module Gorillib
38
48
  # [Class] The type of objects produced by this factory
39
49
  class_attribute :product
40
50
 
41
- # [Array<Symbol>] methods that can be redefined by passing a block to an
42
- # instance No not add to your superclass' value in-place; instead, use
43
- # `self.redefinable_methods += [...]`.
44
- # @see #redefine
45
- class_attribute :redefinable_methods, :instance_writer => false
46
- self.redefinable_methods = Set.new([:blankish?, :convert])
47
-
48
- # [Array<Object>] objects considered to be equivalent to `nil`
49
- class_attribute :blankish_vals
50
- self.blankish_vals = Set.new([ nil, "" ]) # note: [] {} and false are NOT blankish by default
51
-
52
51
  def initialize(options={})
53
- @product = options.delete(:product) if options.has_key?(:product)
54
- @blankish_vals = options.delete(:blankish_vals) if options.has_key?(:blankish_vals)
55
- options.extract!(*redefinable_methods).each do |meth, value_or_block|
56
- redefine(meth, value_or_block)
52
+ @product = options.delete(:product){ self.class.product }
53
+ if options[:blankish]
54
+ define_singleton_method(:blankish, options.delete(:blankish))
57
55
  end
56
+ redefine(:convert, options.delete(:convert)) if options.has_key?(:convert)
58
57
  warn "Unknown options #{options.keys}" unless options.empty?
59
58
  end
60
59
 
61
60
  def self.typename
62
- Gorillib::Inflector.underscore(product.name).to_sym
61
+ @typename ||= Gorillib::Inflector.underscore(product.name).to_sym
63
62
  end
64
63
  def typename ; self.class.typename ; end
65
64
 
66
- def self.receive(*args, &block)
67
- self.new.receive(*args, &block)
68
- end
69
-
70
65
  # A `native` object does not need any transformation; it is accepted directly.
71
66
  # By default, an object is native if it `is_a?(product)`
72
67
  #
73
68
  # @param [Object] obj the object to convert and receive
74
69
  # @return [true, false] true if the item does not need conversion
75
70
  def native?(obj)
76
- obj.is_a?(product)
71
+ obj.is_a?(@product)
77
72
  end
78
73
  def self.native?(obj) self.new.native?(obj) ; end
79
74
 
@@ -82,12 +77,15 @@ module Gorillib
82
77
  # @param [Object] obj the object to convert and receive
83
78
  # @return [true, false] true if the item is equivalent to a nil value
84
79
  def blankish?(obj)
85
- blankish_vals.include?(obj)
80
+ obj.nil? || (obj == "")
86
81
  end
87
- def self.blankish?(obj) self.new.blankish?(obj) ; end
82
+ def self.blankish?(obj)
83
+ obj.nil? || (obj == "")
84
+ end
85
+
86
+ protected
88
87
 
89
88
  def redefine(meth, *args, &block)
90
- raise ArgumentError, "Cannot redefine #{meth} -- only #{redefinable_methods.inspect} are redefinable" unless redefinable_methods.include?(meth)
91
89
  if args.present?
92
90
  val = args.first
93
91
  case
@@ -100,8 +98,6 @@ module Gorillib
100
98
  self
101
99
  end
102
100
 
103
- protected
104
-
105
101
  # Raises a FactoryMismatchError.
106
102
  def mismatched!(obj, message=nil, *args)
107
103
  message ||= "item cannot be converted to #{product}"
@@ -109,8 +105,9 @@ module Gorillib
109
105
  raise FactoryMismatchError, message, *args
110
106
  end
111
107
 
112
- def self.register_factory!(*args)
113
- Gorillib::Factory.register_factory(self, *args)
108
+ def self.register_factory!(*typenames)
109
+ typenames = [typename, product] if typenames.empty?
110
+ Gorillib::Factory.register_factory(self.new, typenames)
114
111
  end
115
112
  end
116
113
 
@@ -139,14 +136,13 @@ module Gorillib
139
136
  # throws a mismatch error for anything else.
140
137
  #
141
138
  # @example
142
- # ff = Gorillib::Factory::NonConvertingFactory.new(:product => String, :blankish_vals => [nil])
139
+ # ff = Gorillib::Factory::NonConvertingFactory.new(:product => String, :blankish => ->(obj){ obj.nil? })
143
140
  # ff.receive(nil) #=> nil
144
141
  # ff.receive("bob") #=> "bob"
145
142
  # ff.receive(:bob) #=> Gorillib::Factory::FactoryMismatchError: must be an instance of String, got 3
146
143
  #
147
144
  class NonConvertingFactory < BaseFactory
148
- self.blankish_vals = [nil]
149
-
145
+ def blankish?(obj) obj.nil? ; end
150
146
  def receive(obj)
151
147
  return nil if blankish?(obj)
152
148
  return obj if native?(obj)
@@ -156,17 +152,20 @@ module Gorillib
156
152
  end
157
153
  end
158
154
 
159
- class IdenticalFactory < BaseFactory
160
- self.redefinable_methods = []
161
- self.blankish_vals = []
155
+ class ::Whatever < BaseFactory
156
+ def initialize(options={})
157
+ options.slice!(:convert, :blankish)
158
+ super(options)
159
+ end
162
160
  def native?(obj) true ; end
163
161
  def blankish?(obj) false ; end
164
- def receive(obj)
162
+ def receive(obj) obj ; end
163
+ def self.receive(obj)
165
164
  obj
166
165
  end
167
- register_factory!(:identical, :whatever)
166
+ Gorillib::Factory.register_factory(self, [self, :identical, :whatever])
168
167
  end
169
- ::Whatever = IdenticalFactory unless defined?(Whatever)
168
+ IdenticalFactory = ::Whatever unless defined?(IdenticalFactory)
170
169
 
171
170
  # __________________________________________________________________________
172
171
  #
@@ -175,7 +174,7 @@ module Gorillib
175
174
 
176
175
  class StringFactory < ConvertingFactory
177
176
  self.product = String
178
- self.blankish_vals -= [""]
177
+ def blankish?(obj) obj.nil? ; end
179
178
  def native?(obj) obj.respond_to?(:to_str) end
180
179
  def convert(obj) obj.to_s end
181
180
  register_factory!
@@ -262,13 +261,13 @@ module Gorillib
262
261
 
263
262
  class NilFactory < NonConvertingFactory
264
263
  self.product = NilClass
265
- self.blankish_vals = []
264
+ def blankish?(obj) false ; end
266
265
  register_factory!(:nil, NilClass)
267
266
  end
268
267
 
269
268
  class BooleanFactory < ConvertingFactory
270
269
  self.product = [TrueClass, FalseClass]
271
- self.blankish_vals = [nil]
270
+ def blankish?(obj) obj.nil? ; end
272
271
  def native?(obj) obj.equal?(true) || obj.equal?(false) ; end
273
272
  def convert(obj) (obj.to_s == "false") ? false : true ; end
274
273
  register_factory!(:boolean)
@@ -282,20 +281,18 @@ module Gorillib
282
281
  class EnumerableFactory < ConvertingFactory
283
282
  # [#receive] factory for converting items
284
283
  attr_reader :items_factory
285
- self.redefinable_methods += [:empty_product]
286
- self.blankish_vals = Set.new([ nil ])
287
284
 
288
285
  def initialize(options={})
289
- @items_factory = Gorillib::Factory.receive( options.delete(:items){ IdenticalFactory.new } )
286
+ @items_factory = Gorillib::Factory.receive( options.delete(:items){ Gorillib::Factory(:identical) } )
287
+ redefine(:empty_product, options.delete(:empty_product)) if options.has_key?(:empty_product)
290
288
  super(options)
291
289
  end
292
290
 
293
- def native?(obj)
294
- false
295
- end
291
+ def blankish?(obj) obj.nil? ; end
292
+ def native?(obj) false ; end
296
293
 
297
294
  def empty_product
298
- product.new
295
+ @product.new
299
296
  end
300
297
 
301
298
  def convert(obj)
@@ -323,7 +320,7 @@ module Gorillib
323
320
  self.product = Hash
324
321
 
325
322
  def initialize(options={})
326
- @keys_factory = Gorillib::Factory( options.delete(:keys){ Whatever.new } )
323
+ @keys_factory = Gorillib::Factory( options.delete(:keys){ Gorillib::Factory(:identical) } )
327
324
  super(options)
328
325
  end
329
326
 
@@ -339,7 +336,7 @@ module Gorillib
339
336
 
340
337
  class RangeFactory < NonConvertingFactory
341
338
  self.product = Range
342
- self.blankish_vals = [ nil, [] ]
339
+ def blankish?(obj) obj.nil? || obj == [] ; end
343
340
  register_factory!
344
341
  end
345
342
 
@@ -347,7 +344,6 @@ module Gorillib
347
344
 
348
345
  class ApplyProcFactory < ConvertingFactory
349
346
  attr_reader :callable
350
- self.blankish_vals = Set.new([nil])
351
347
 
352
348
  def initialize(callable=nil, options={}, &block)
353
349
  if block_given?
@@ -357,9 +353,8 @@ module Gorillib
357
353
  @callable = callable
358
354
  super(options)
359
355
  end
360
- def native?(val)
361
- false
362
- end
356
+ def blankish?(obj) obj.nil? ; end
357
+ def native?(val) false ; end
363
358
  def convert(obj)
364
359
  callable.call(obj)
365
360
  end
@@ -0,0 +1,24 @@
1
+ module Gorillib
2
+ module Model
3
+ #
4
+ # A set of guards for good behavior:
5
+ #
6
+ # * checks that fields given to read_attribute, write_attribute, etc are defined
7
+ #
8
+ module Lint
9
+ def read_attribute(field_name, *) check_field(field_name) ; super ; end
10
+ def write_attribute(field_name, *) check_field(field_name) ; super ; end
11
+ def unset_attribute(field_name, *) check_field(field_name) ; super ; end
12
+ def attribute_set?(field_name, *) check_field(field_name) ; super ; end
13
+
14
+ protected
15
+ # @return [true] if the field exists
16
+ # @raise [UnknownFieldError] if the field is missing
17
+ def check_field(field_name)
18
+ return true if self.class.has_field?(field_name)
19
+ raise UnknownFieldError, "unknown field: #{field_name} for #{self}"
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -1,17 +1,27 @@
1
+
2
+ class Array
3
+ def to_tsv
4
+ join("\t")
5
+ end
6
+ end
7
+
1
8
  module Gorillib
2
9
  module Model
3
-
4
10
  def to_wire(options={})
5
11
  attributes.merge(:_type => self.class.typename).inject({}) do |acc, (key,attr)|
6
12
  acc[key] = attr.respond_to?(:to_wire) ? attr.to_wire(options) : attr
7
13
  acc
8
14
  end
9
15
  end
16
+ def as_json(*args) to_wire(*args) ; end
10
17
 
11
18
  def to_json(options={})
12
19
  MultiJson.dump(to_wire(options), options)
13
20
  end
14
- alias_method(:as_json, :to_wire)
21
+
22
+ def to_tsv
23
+ attribute_values.map(&:to_s).join("\t")
24
+ end
15
25
 
16
26
  module ClassMethods
17
27
  def from_tuple(*vals)
@@ -9,9 +9,9 @@ module Gorillib
9
9
  raise ArgumentError, "Name must start with [A-Za-z_] and subsequently contain only [A-Za-z0-9_]", caller unless name =~ VALID_NAME_RE
10
10
  end
11
11
 
12
- def hashlike!(desc, val)
12
+ def hashlike!(val)
13
13
  return true if val.respond_to?(:[]) && val.respond_to?(:has_key?)
14
- raise ArgumentError, "#{desc} should be something that behaves like a hash: #{val.inspect}", caller
14
+ raise ArgumentError, "#{block_given? ? yield : 'value'} should be something that behaves like a hash: #{val.inspect}", caller
15
15
  end
16
16
 
17
17
  def included_in!(desc, val, colxn)
@@ -9,15 +9,21 @@ module Gorillib
9
9
 
10
10
  def register_path(handle, *pathsegs)
11
11
  ArgumentError.arity_at_least!(pathsegs, 1)
12
- ROOT_PATHS[handle] = pathsegs
12
+ ROOT_PATHS[handle.to_sym] = pathsegs
13
13
  end
14
14
 
15
- def register_paths(pairs = {})
16
- pairs.each_pair{ |handle, pathsegs| register_path(handle, *pathsegs) }
15
+ def register_paths(handle_paths = {})
16
+ handle_paths.each_pair{|handle, pathsegs| register_path(handle, *pathsegs) }
17
17
  end
18
18
 
19
- def unregister_path handle
20
- ROOT_PATHS.delete handle
19
+ def register_default_paths(handle_paths = {})
20
+ handle_paths.each_pair do |handle, pathsegs|
21
+ register_path(handle, *pathsegs) unless ROOT_PATHS.has_key?(handle.to_sym)
22
+ end
23
+ end
24
+
25
+ def unregister_path(handle)
26
+ ROOT_PATHS.delete handle.to_sym
21
27
  end
22
28
 
23
29
  # Expand a path with late-evaluated segments.
@@ -59,8 +65,13 @@ module Gorillib
59
65
  def relative_path_to(*pathsegs)
60
66
  ArgumentError.arity_at_least!(pathsegs, 1)
61
67
  pathsegs = pathsegs.map{|ps| expand_pathseg(ps) }.flatten
62
- new(File.join(*pathsegs)).cleanpath(true)
68
+ self.new(File.join(*pathsegs)).cleanpath(true)
63
69
  end
70
+ def relpath_to(*args) relative_path_to(*args) ; end
71
+
72
+ # def make_pathname(*args)
73
+ # Pathname.new(*args)
74
+ # end
64
75
 
65
76
  protected
66
77
  # Recursively expand a path handle
@@ -75,4 +86,8 @@ end
75
86
 
76
87
  class Pathname
77
88
  extend Gorillib::Pathref
89
+ class << self ; alias_method :new_pathname, :new ; end
90
+
91
+ # FIXME: find out if this is dangerous
92
+ alias_method :to_str, :to_path
78
93
  end
data/lib/gorillib/some.rb CHANGED
@@ -5,6 +5,8 @@ require 'pathname'
5
5
 
6
6
  require 'gorillib/base'
7
7
 
8
+ require 'gorillib/logger/log'
9
+
8
10
  require 'gorillib/pathname'
9
11
  require 'gorillib/string/simple_inflector'
10
12
  require 'gorillib/string/inflections'
@@ -9,8 +9,6 @@ class ::Guid < ::String ; end
9
9
  class ::IpAddress < ::String ; end
10
10
  class ::Hostname < ::String ; end
11
11
 
12
- class ::Url < ::String ; end
13
-
14
12
  # require 'gorillib/metaprogramming/delegation'
15
13
  #
16
14
  # class ::Boolean < ::Object
@@ -0,0 +1,9 @@
1
+ require 'addressable/uri'
2
+
3
+ class ::Url < Addressable::URI ; end
4
+
5
+ class UrlFactory < Gorillib::Factory::ConvertingFactory
6
+ self.product = ::Url
7
+ def convert(obj) product.parse(obj) ; end
8
+ register_factory!
9
+ end
@@ -36,6 +36,9 @@ class ItsATrap < BasicObject
36
36
  alias_method :pretty_inspect, :inspect
37
37
  def methods() @obj.methods ; end
38
38
 
39
+ # @returns the proxied object
40
+ def __obj__ ; @obj ; end
41
+
39
42
  # These are defined on BasicObject, delegate them along with the rest
40
43
  # BasicObject.instance_methods
41
44
  # => [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
@@ -58,7 +61,7 @@ private
58
61
  end
59
62
 
60
63
  def __describe_and_send__(meth, *args, &block)
61
- pref = "%-3d %-14s %-15s" % [@call_count, __id__, self.to_s[0..14]]
64
+ pref = "%-3d %-14s %-15s" % [@call_count, @obj.__id__, self.to_s[0..14]]
62
65
  @call_count += 1
63
66
  $stderr.puts "%s %-15s <- %-30s %s -- %s" % [pref, meth.to_s[0..14], args.map(&:inspect).join(','), block, ::Kernel.caller.first]
64
67
  ret = @obj.__send__(meth, *args, &block)
data/notes/HOWTO.md ADDED
@@ -0,0 +1,22 @@
1
+ # Walkthrough
2
+
3
+ **note (2012-06): you must use the dev branch, `version_1` of Gorillib** -- https://github.com/infochimps-labs/gorillib/tree/version_1/
4
+
5
+ 1. Here's a set of examples of Gorillib records in action.
6
+ * [examples of `Gorillib::Model`](https://github.com/infochimps-labs/gorillib/tree/version_1/examples/model)
7
+ * see example gorillib records in the /examples directories
8
+
9
+ __________________________________________________________________________
10
+
11
+ 2. Walkthrough dataflow for a new customer:
12
+ * models to represent records in a dataflow (gorillib/examples)
13
+ * describe the macro dataflow (wukong/examples)
14
+ * simulate the dataflow at commandline
15
+ * project the dataflow using Flume
16
+ * export the dataflow using graphviz
17
+
18
+ https://github.com/infochimps-labs/wukong/wiki
19
+ https://github.com/infochimps-labs/gorillib/wiki
20
+ Plus source files (or you can instead see the Yard Docs)
21
+
22
+ 3. flume event model shim
data/notes/bucket.md ADDED
@@ -0,0 +1,155 @@
1
+ # gorillib/bucket (WIP -- NOT READY YET) -- freeform storage with definable access
2
+
3
+ * A `Go::Bucket` fulfills the contract of `Go::Model`
4
+
5
+ ## Arbitrary read/writes with `[]/[]=`, defined access with `.foo/.foo=`
6
+
7
+ Overriding `method_missing` is uncouth. It screws up your stack traces, muddies your interface and leads to unassertive code:
8
+
9
+ ```ruby
10
+ Settings.defcon = 3
11
+ # ... elsewhere ...
12
+ if Settings.def_con.to_i <= 1
13
+ WOPR.launch_nukes!
14
+ end
15
+ ```
16
+
17
+ Here, mispelling `def_con` as `defcon` (along with being wussy about the attribute's type and negligent about prescribing a default value) leads to the destruction of humanity.
18
+
19
+ Nonetheless, for several reasons (prudent laziness, handling configuration of an external component, etc) it's important to handle arbitrary attributes uniformly.
20
+
21
+ So the rule is that you can get or set anything you like using `[]` and `[]=` respectively, no need to define it first:
22
+
23
+ Settings[:whatever] = 3
24
+ Settings[:whatever] #=> 3
25
+
26
+ You don't get any magic, and the ugly accessor leaves you in no doubt that you're being lazy (ain't judging, just sayin'). When you define a setting you get accessors for that attribute and all associated magic:
27
+
28
+ Settings.option :defcon, Integer, :doc => 'Current NORAD defense condition', :default => 5, :validates => { :in => 1..5 }
29
+ Settings.defcon #=> 5
30
+ Settings.defcon = 0
31
+ Settings.validate! # raises a validation exception
32
+
33
+ Defined fields' magic works whichever form of access you use -- here, type-converting the value on assignment:
34
+
35
+ Settings[:defcon] = '5' #=> 5
36
+ Settings.defcon = '5' #=> 5
37
+ Settings.receive_defcon('5') #=> 5
38
+
39
+ ## Keys
40
+
41
+ Keys be lower-cased identifiers: they should match `/\A([a-z][a-z0-9\_]*)\z/`.
42
+
43
+ ## Deep Hash
44
+
45
+ Hash.new {|h,k| h[k] = Hash.new(&h.default_proc)}
46
+
47
+ ### Dot addressing
48
+
49
+ Can retrieve keys as 'x.y.z' meaning 'foo[x][y][z]'.
50
+
51
+ * On a `get`, will
52
+ - return the value, if `foo[:x][:y][:z]` exists
53
+ - return nil, if either `foo[:x]` or `foo[:x][:y]` is *unset*. It will not create `foo[:x]` or `foo[:x][:y]`.
54
+ - raise an error if either `foo[:x]` or `foo[:x][:y]` is set to a value that does not respond to `[]`
55
+
56
+ * On a `set`, will
57
+ - raise an error if either `foo[:x]` or `foo[:x][:y]` is set to a value that doesn't respond to `[]`
58
+ - set and return the value; any intermediate buckets (`foo[:x]` and `foo[:x][:y]`) will be created if they don't exist.
59
+
60
+
61
+ __________________________________________________________________________
62
+
63
+
64
+ ## Examples
65
+
66
+ ### how hash-like is this?
67
+
68
+ * obj.deep_get(:foo, :bar, :baz) #
69
+ * obj.deep_set(:foo, :bar, :baz, val) # sets foo bar baz.
70
+ - where property has a type, it uses that type
71
+ - otherwise it uses Mash, and calls [] on it
72
+
73
+ * TD votes NO on magic recursive hash behavior
74
+
75
+
76
+ {
77
+ :buck1 => {
78
+ :buck2 => { :k3 => 33, :ehsh => {}, :arr => [11, 12, 13] },
79
+ :cars => [{ :model => 'ford', :cylinders => 8 }, { :model => 'buick', :cylinders => 6 }],
80
+ }
81
+ :buck3 => {
82
+ :buck4 => { :k4 => 44 }
83
+ :k5 => nil,
84
+ }
85
+ :k6 => 69
86
+ }
87
+
88
+ * `obj[:buck3]` # b{ :buck4=>b{ :k4=>44 },:k5=>nil } -- it's a bucket
89
+ * `obj[:buck5] = Hash.new`
90
+ * `obj[:buck5].class` # Bucket -- it's converted to bucket
91
+ * `obj[:xxx]` # nil -- it's not there
92
+ * `obj[:xxx][:yyy][:zzz]` # fails -- it doesn't try to index into the nil `obj[:xxx]` cell.
93
+ * `obj[:xxx]` # nil -- it didn't create the `obj[:xxx]` object when we tried to read on the previous line.
94
+
95
+ c.options_for 'wheels', 'interior.fabric', 'interior.carpeting'
96
+
97
+ c[:wheels] # c{ }
98
+ c[:wheels][:whitewall] # nil
99
+ c[:wheels][:whitewall] = true # true
100
+
101
+ c.interior.fabric.color
102
+
103
+
104
+ * `obj[:k6][:bomb]` # raises ArgumentError; `obj[:k6]` is not a bucket.
105
+
106
+ * `obj[:'hello-there'] # undefined; `'hello-there'` is not a valid identifier.
107
+
108
+ * `obj[:buck3][:buck4][:k5] = 55`
109
+ `obj[:buck3]` # b{ :buck4=>b{ :k4=>44 },:k5 => 55}
110
+ * `obj[:f][:g][:h] = 7`
111
+ `obj[:f]` # b{ :g => b{ :h => 7 } }
112
+
113
+ * `obj[:foo][:bar][:baz] ||= 1` -- **hard** ?I think?
114
+
115
+ * `obj[:foo]` -- nil
116
+ * `obj[:foo][:bar]` -- raise
117
+ * `obj[:foo][:bar] = 3` --
118
+ - now obj[:foo][:bar] is 3
119
+ - and obj[:foo] is a ?dsl_object?? but
120
+
121
+ `obj[:foo][:bar]`
122
+
123
+ Suppose `obj[:foo]` is set to a
124
+
125
+ Seems clear these should do the right thing:
126
+
127
+ * `obj.merge`
128
+ * `obj.reverse_merge`
129
+ * `obj.keys`
130
+ * `obj.values`
131
+ * ... and a few more
132
+
133
+ Also:
134
+
135
+ * `obj.to_a`?
136
+ * `obj.each`?
137
+ * other crazy Enumerable properties?
138
+
139
+ ### TODO
140
+
141
+ * figure out the method structure for
142
+ - read/write/unset of attributes when Hash vs Accessors vs Instance Variables
143
+ - reader/writer: raw vs. hooks, dirty, etc.
144
+
145
+ ## Configliere Settings
146
+
147
+
148
+ Configliere lets you define arbitrary attributes of a param, notably:
149
+
150
+ [:description] Documentation for the param, used in the --help message
151
+ [:default] Sets a default value (applied immediately)
152
+ [:env_var] Environment variable to adopt (applied immediately, and after +:default+)
153
+ [:type] Converts param's value to the given type, just before the finally block is called
154
+ [:finally] Block of code to postprocess settings or handle complex configuration.
155
+ [:required] Raises an error if, at the end of calling resolve!, the param's value is nil.