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