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