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.
- data/CHANGELOG.md +36 -1
- data/Gemfile +23 -19
- data/Guardfile +1 -1
- data/Rakefile +31 -31
- data/TODO.md +2 -30
- data/VERSION +1 -1
- data/examples/builder/ironfan.rb +4 -4
- data/gorillib.gemspec +40 -25
- data/lib/gorillib/array/average.rb +13 -0
- data/lib/gorillib/array/sorted_median.rb +11 -0
- data/lib/gorillib/array/sorted_percentile.rb +11 -0
- data/lib/gorillib/array/sorted_sample.rb +12 -0
- data/lib/gorillib/builder.rb +8 -14
- data/lib/gorillib/collection/has_collection.rb +31 -31
- data/lib/gorillib/collection/list_collection.rb +58 -0
- data/lib/gorillib/collection/model_collection.rb +63 -0
- data/lib/gorillib/collection.rb +57 -85
- data/lib/gorillib/logger/log.rb +26 -22
- data/lib/gorillib/model/base.rb +52 -39
- data/lib/gorillib/model/doc_string.rb +15 -0
- data/lib/gorillib/model/factories.rb +56 -61
- data/lib/gorillib/model/lint.rb +24 -0
- data/lib/gorillib/model/serialization.rb +12 -2
- data/lib/gorillib/model/validate.rb +2 -2
- data/lib/gorillib/pathname.rb +21 -6
- data/lib/gorillib/some.rb +2 -0
- data/lib/gorillib/type/extended.rb +0 -2
- data/lib/gorillib/type/url.rb +9 -0
- data/lib/gorillib/utils/console.rb +4 -1
- data/notes/HOWTO.md +22 -0
- data/notes/bucket.md +155 -0
- data/notes/builder.md +170 -0
- data/notes/collection.md +81 -0
- data/notes/factories.md +86 -0
- data/notes/model-overlay.md +209 -0
- data/notes/model.md +135 -0
- data/notes/structured-data-classes.md +127 -0
- data/spec/array/average_spec.rb +24 -0
- data/spec/array/sorted_median_spec.rb +18 -0
- data/spec/array/sorted_percentile_spec.rb +24 -0
- data/spec/array/sorted_sample_spec.rb +28 -0
- data/spec/gorillib/builder_spec.rb +46 -28
- data/spec/gorillib/collection_spec.rb +195 -10
- data/spec/gorillib/model/lint_spec.rb +28 -0
- data/spec/gorillib/model/record/factories_spec.rb +27 -13
- data/spec/gorillib/model/serialization_spec.rb +3 -5
- data/spec/gorillib/model_spec.rb +86 -104
- data/spec/spec_helper.rb +2 -1
- data/spec/support/gorillib_test_helpers.rb +83 -7
- data/spec/support/model_test_helpers.rb +9 -28
- metadata +52 -44
- data/lib/gorillib/configurable.rb +0 -28
- data/spec/gorillib/configurable_spec.rb +0 -62
- 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 ||=
|
39
|
+
@factories ||= Hash.new
|
27
40
|
end
|
28
41
|
public
|
29
42
|
|
30
|
-
def self.register_factory(factory,
|
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)
|
54
|
-
|
55
|
-
|
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
|
-
|
80
|
+
obj.nil? || (obj == "")
|
86
81
|
end
|
87
|
-
def self.blankish?(obj)
|
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!(*
|
113
|
-
|
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, :
|
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
|
-
|
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
|
160
|
-
|
161
|
-
|
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
|
166
|
+
Gorillib::Factory.register_factory(self, [self, :identical, :whatever])
|
168
167
|
end
|
169
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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){
|
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
|
294
|
-
|
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){
|
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
|
-
|
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
|
361
|
-
|
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
|
-
|
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!(
|
12
|
+
def hashlike!(val)
|
13
13
|
return true if val.respond_to?(:[]) && val.respond_to?(:has_key?)
|
14
|
-
raise ArgumentError, "#{
|
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)
|
data/lib/gorillib/pathname.rb
CHANGED
@@ -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(
|
16
|
-
|
15
|
+
def register_paths(handle_paths = {})
|
16
|
+
handle_paths.each_pair{|handle, pathsegs| register_path(handle, *pathsegs) }
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
|
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
@@ -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.
|