gorillib 0.4.2 → 0.5.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.
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/gorillib.gemspec +32 -5
- data/lib/gorillib/array/hashify.rb +11 -0
- data/lib/gorillib/base.rb +1 -0
- data/lib/gorillib/data_munging.rb +8 -1
- data/lib/gorillib/exception/raisers.rb +6 -1
- data/lib/gorillib/factories.rb +26 -13
- data/lib/gorillib/model/base.rb +6 -1
- data/lib/gorillib/model/schema_magic.rb +1 -0
- data/lib/gorillib/model/serialization/csv.rb +2 -0
- data/lib/gorillib/model/serialization/json.rb +44 -0
- data/lib/gorillib/model/serialization/lines.rb +30 -0
- data/lib/gorillib/model/serialization/tsv.rb +55 -0
- data/lib/gorillib/pathname/utils.rb +34 -0
- data/lib/gorillib/type/extended.rb +1 -0
- data/lib/gorillib/type/ip_address.rb +153 -0
- 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/gorillib/array/hashify_spec.rb +20 -0
- data/spec/gorillib/builder_spec.rb +2 -2
- data/spec/gorillib/{model/factories_spec.rb → factories_spec.rb} +3 -5
- data/spec/gorillib/model/serialization/tsv_spec.rb +17 -0
- data/spec/gorillib/type/ip_address_spec.rb +143 -0
- metadata +35 -5
@@ -0,0 +1,153 @@
|
|
1
|
+
module IpAddresslike
|
2
|
+
ONES = 0xFFFFFFFF
|
3
|
+
|
4
|
+
# Masks off all but the `bitness` most-significant-bits
|
5
|
+
#
|
6
|
+
# @example /24 keeps only the first three quads
|
7
|
+
# IpAddress.new('1.2.3.4').bitness_min(24) # '1.2.3.0'
|
8
|
+
#
|
9
|
+
def bitness_min(bitness)
|
10
|
+
raise ArgumentError, "IP addresses have only 32 bits (got #{bitness.inspect})" unless (0..32).include?(bitness)
|
11
|
+
lsbs = 32 - bitness
|
12
|
+
(packed >> lsbs) << lsbs
|
13
|
+
end
|
14
|
+
|
15
|
+
# Masks off all but the `bitness` most-significant-bits, filling with ones
|
16
|
+
#
|
17
|
+
# @example /24 fills the last quad
|
18
|
+
# IpAddress.new('1.2.3.4').bitness_min(24) # '1.2.3.255'
|
19
|
+
#
|
20
|
+
def bitness_max(bitness)
|
21
|
+
raise ArgumentError, "IP addresses have only 32 bits (got #{bitness.inspect})" unless (0..32).include?(bitness)
|
22
|
+
packed | (ONES >> bitness)
|
23
|
+
end
|
24
|
+
|
25
|
+
def hex_str
|
26
|
+
packed.to_s(16)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
dotted
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class ::IpAddress < ::String
|
36
|
+
include IpAddresslike
|
37
|
+
|
38
|
+
def dotted
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_i
|
43
|
+
packed
|
44
|
+
end
|
45
|
+
|
46
|
+
# @returns [Integer] the 32-bit integer for this IP address
|
47
|
+
def packed
|
48
|
+
ip_a, ip_b, ip_c, ip_d = quads
|
49
|
+
((ip_a << 24) + (ip_b << 16) + (ip_c << 8) + (ip_d))
|
50
|
+
end
|
51
|
+
|
52
|
+
def quads
|
53
|
+
self.split(".", 4).map{|qq| Integer(qq) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# === class methods ===
|
57
|
+
|
58
|
+
def self.from_packed(pi)
|
59
|
+
str = [ (pi >> 24) & 0xFF, (pi >> 16) & 0xFF, (pi >> 8) & 0xFF, (pi) & 0xFF ].join(".")
|
60
|
+
new(str)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.from_dotted(str)
|
64
|
+
new(str)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Stores an IP address in numeric form.
|
69
|
+
#
|
70
|
+
# IpNumeric instances are immutable, and memoize most of their methods.
|
71
|
+
class ::IpNumeric
|
72
|
+
include IpAddresslike
|
73
|
+
include Comparable
|
74
|
+
|
75
|
+
def receive(val)
|
76
|
+
new(val)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(addr)
|
80
|
+
@packed = addr.to_int
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_i ; packed ; end
|
84
|
+
def to_int ; packed ; end
|
85
|
+
def ==(other) ; packed == other.to_int ; end
|
86
|
+
def <=>(other) ; packed <=> other.to_int ; end
|
87
|
+
def +(int) ; self.class.new(to_int + int) ; end
|
88
|
+
|
89
|
+
|
90
|
+
def packed ; @packed ; end
|
91
|
+
|
92
|
+
def dotted
|
93
|
+
@dotted ||= quads.join('.').freeze
|
94
|
+
end
|
95
|
+
|
96
|
+
def quads
|
97
|
+
@quads ||= [ (@packed >> 24) & 0xFF, (@packed >> 16) & 0xFF, (@packed >> 8) & 0xFF, (@packed) & 0xFF ].freeze
|
98
|
+
end
|
99
|
+
|
100
|
+
# === class methods ===
|
101
|
+
|
102
|
+
def self.from_packed(pi)
|
103
|
+
new(pi)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.from_dotted(dotted)
|
107
|
+
ip_a, ip_b, ip_c, ip_d = quads = dotted.split(".", 4).map(&:to_i)
|
108
|
+
obj = new((ip_a << 24) + (ip_b << 16) + (ip_c << 8) + (ip_d))
|
109
|
+
obj.instance_variable_set('@dotted', dotted.freeze)
|
110
|
+
obj.instance_variable_set('@quads', quads.freeze)
|
111
|
+
obj
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class ::IpRange < Range
|
116
|
+
|
117
|
+
def initialize(min_or_range, max=nil, exclusive=false)
|
118
|
+
if max.nil?
|
119
|
+
min = min_or_range.min
|
120
|
+
max = min_or_range.max
|
121
|
+
exclusive = min_or_range.exclude_end?
|
122
|
+
else
|
123
|
+
min = min_or_range
|
124
|
+
end
|
125
|
+
raise ArgumentError, "Only inclusive #{self.class.name}s are implemented" if exclusive
|
126
|
+
super( IpNumeric.new(min), IpNumeric.new(max), false )
|
127
|
+
end
|
128
|
+
|
129
|
+
def bitness_blocks(bitness)
|
130
|
+
raise ArgumentError, "IP addresses have only 32 bits (got #{bitness.inspect})" unless (0..32).include?(bitness)
|
131
|
+
return [] if min.nil?
|
132
|
+
lsbs = 32 - bitness
|
133
|
+
middle_min = min.bitness_max(bitness) + 1
|
134
|
+
return [[min, max]] if max < middle_min
|
135
|
+
middle_max = max.bitness_min(bitness)
|
136
|
+
blks = []
|
137
|
+
stride = 1 << lsbs
|
138
|
+
#
|
139
|
+
blks << [min, IpNumeric.new(middle_min-1)]
|
140
|
+
(middle_min ... middle_max).step(stride){|beg| blks << [IpNumeric.new(beg), IpNumeric.new(beg+stride-1)] }
|
141
|
+
blks << [IpNumeric.new(middle_max), max]
|
142
|
+
blks
|
143
|
+
end
|
144
|
+
|
145
|
+
CIDR_RE = %r{\A(\d+\.\d+\.\d+\.\d+)/([0-3]\d)\z}
|
146
|
+
|
147
|
+
def self.from_cidr(cidr_str)
|
148
|
+
cidr_str =~ CIDR_RE or raise ArgumentError, "CIDR string should look like an ip address and bitness, eg 1.2.3.4/24 (got #{cidr_str})"
|
149
|
+
bitness = $2.to_i
|
150
|
+
ip_address = IpNumeric.from_dotted($1)
|
151
|
+
new( ip_address.bitness_min(bitness), ip_address.bitness_max(bitness) )
|
152
|
+
end
|
153
|
+
end
|
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.
|
data/notes/builder.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# gorillib/builder -- construct elegant ruby Domain-Specific Languages (DSLs).
|
2
|
+
|
3
|
+
`Gorillib::Builder` provides foundation models for elegant ruby DSLs (Domain-Specific Languages). A builder block's relaxed ruby syntax enables highly-readable specification of complex behavior:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
garage do
|
7
|
+
car :ford_tudor do
|
8
|
+
manufacturer 'Ford Motor Company'
|
9
|
+
car_model 'Tudor'
|
10
|
+
year 1939
|
11
|
+
doors 2
|
12
|
+
style :sedan
|
13
|
+
engine do
|
14
|
+
volume 350
|
15
|
+
cylinders 8
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
car :wildcat do
|
20
|
+
manufacturer 'Buick'
|
21
|
+
car_model 'Wildcat'
|
22
|
+
year 1968
|
23
|
+
doors 2
|
24
|
+
style :convertible
|
25
|
+
engine do
|
26
|
+
volume 455
|
27
|
+
cylinders 8
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
### Defining a builder
|
34
|
+
|
35
|
+
To make a class be a builder, simply `include Gorillib::Builder`:
|
36
|
+
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class Garage
|
40
|
+
include Gorillib::Builder
|
41
|
+
collection :car
|
42
|
+
end
|
43
|
+
|
44
|
+
class Car
|
45
|
+
include Gorillib::Builder
|
46
|
+
field :name, String
|
47
|
+
field :manufacturer, String
|
48
|
+
field :car_model, String
|
49
|
+
field :year, Integer
|
50
|
+
field :doors, Integer
|
51
|
+
field :style, Symbol, :validates => { :inclusion_in => [:sedan, :coupe, :convertible] }
|
52
|
+
member :engine
|
53
|
+
belongs_to :garage
|
54
|
+
end
|
55
|
+
|
56
|
+
class Engine
|
57
|
+
include Gorillib::Builder
|
58
|
+
field :volume, Integer
|
59
|
+
field :cylinders, Integer
|
60
|
+
belongs_to :car
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### getset accessors
|
65
|
+
|
66
|
+
Fields of a Builder class create a single "getset" accessor (and not the familiar 'foo/foo=' pair):
|
67
|
+
|
68
|
+
* `car_model.foo` -- returns value of foo
|
69
|
+
* `car_model.foo(val)` -- sets foo to `val`, which can be any value, even `nil`.
|
70
|
+
|
71
|
+
### member fields
|
72
|
+
|
73
|
+
A builder class can have `member` fields; the type of a member field should be a builder class itself
|
74
|
+
|
75
|
+
With no arguments, the accessor returns the value of engine attribute, creating if necessary:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
car.engine #=> #<Engine volume=~ cylinders=~>
|
79
|
+
```
|
80
|
+
|
81
|
+
If you pass in a hash of values, the engine is created if necessary and then asked to `receive!` the hash.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
car.engine #=> #<Engine volume=~ cylinders=~>
|
85
|
+
car.engine(:cylinders => 8) #=> #<Engine volume=~ cylinders=8>
|
86
|
+
car.engine.cylinders #=> 8
|
87
|
+
```
|
88
|
+
|
89
|
+
If you provide a no-args block, it is `instance_eval`ed in the context of the member object:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
car :ford_tudor do
|
93
|
+
engine(:cylinders => 8) do
|
94
|
+
self #=> #<Engine volume=~ cylinders=8>
|
95
|
+
volume 455
|
96
|
+
cylinders self.cylinders - 2
|
97
|
+
self #=> #<Engine volume=455 cylinders=6>
|
98
|
+
end
|
99
|
+
engine #=> #<Engine volume=455 cylinders=6>
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
Some people disapprove of `instance_eval`, as they consider it unseemly to mess around with `self`. If you instead provide a one-arg block, the member object is passed in:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
car :ford_tudor do |c|
|
107
|
+
c.engine(:cylinders => 8) do |eng|
|
108
|
+
self #=> #<Car ...>
|
109
|
+
eng.volume 455
|
110
|
+
eng.cylinders eng.cylinders - 2
|
111
|
+
eng #=> #<Engine volume=455 cylinders=6>
|
112
|
+
end
|
113
|
+
c.engine #=> #<Engine volume=455 cylinders=6>
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
### collections
|
118
|
+
|
119
|
+
A builder class can also have `collection` fields, to contain named builder objects.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
garage do
|
123
|
+
car(:ford_tudor) #=> #<Car name="ford tudor" ...>
|
124
|
+
cars #=> { :ford_tudor => #<Car ...>, :wildcat => #<Car ...> }
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
The collected items must respond to `id` (FIXME:). The singular accessor accepts arguments just like a `member` accessor:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
garage do
|
132
|
+
car(:ford_tudor, :year => 1939)
|
133
|
+
#=> #<Car name=`<:ford_tudor year=1939 doors=~ ...>
|
134
|
+
car(:ford_tudor, :year => 1939) do
|
135
|
+
doors 2
|
136
|
+
style :convertible
|
137
|
+
end
|
138
|
+
#=> #<Car name="ford tudor" year=1939 doors=2 ...>
|
139
|
+
car(:wildcat, :year => 1968) do |c|
|
140
|
+
c.doors 2
|
141
|
+
c.style :convertible
|
142
|
+
end
|
143
|
+
#=> #<Car name= year=1939 doors=2 ...>
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
|
148
|
+
### Model methods
|
149
|
+
|
150
|
+
builders have the following:
|
151
|
+
|
152
|
+
* `Model::Defaults`
|
153
|
+
* `Model::Naming`
|
154
|
+
* `Model::Conversion`
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
### SubclassRegistry
|
159
|
+
|
160
|
+
When a subclass happens, decorates class with `saucepot(...)`, equivalent to
|
161
|
+
|
162
|
+
update_or_create
|
163
|
+
registers
|
164
|
+
|
165
|
+
* At class level, `registry_for(CookingUtensil)` gives
|
166
|
+
- `add_cooking_utensil()` and so forth for adding and getting
|
167
|
+
- Protected `cooking_utensils` hash class attribute
|
168
|
+
* Enlisting a resource class (Kitchen.register(FryingPan) and gives you a magic `frying_pan` factory method with signature `frying_pan(name, *args, &block)`
|
169
|
+
- If resource does not exist, calls `FryingPan.new` with full set of params and block. Add it to `cooking_utensils` registry.
|
170
|
+
- If resource with that name exists, retrieve it. call `merge()` with the parameters, and `run_in_scope` with the block.
|