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
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.
|
data/notes/collection.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# gorillib/collection -- associative arrays of keyed objects
|
2
|
+
|
3
|
+
Collection flexibly exchanges `{name => obj}` and `[ obj_with_name, obj_with_name, ... ]` -- this makes Mongo and JSON happy.
|
4
|
+
|
5
|
+
### Collection type
|
6
|
+
|
7
|
+
Defining
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class Continent
|
11
|
+
include Gorillib::Record
|
12
|
+
# ...
|
13
|
+
field :countries, Collection, :of => Geo::Country, :key_method => :country_id
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
Lets it serialize as
|
18
|
+
|
19
|
+
```yaml
|
20
|
+
- name: Africa
|
21
|
+
countries:
|
22
|
+
- name: Rwanda
|
23
|
+
country_id: rw
|
24
|
+
capitol_city: Kigali
|
25
|
+
- name: Djibouti
|
26
|
+
country_id: dj
|
27
|
+
capitol_city: Djibouti
|
28
|
+
```
|
29
|
+
|
30
|
+
or naturally pivot to be
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
{ name: "Africa",
|
34
|
+
countries: {
|
35
|
+
rw: {
|
36
|
+
name: "Rwanda",
|
37
|
+
country_id: "rw",
|
38
|
+
capitol_city: "Kigali" },
|
39
|
+
dj: {
|
40
|
+
name: "Djibouti",
|
41
|
+
country_id: "dj",
|
42
|
+
capitol_city: "Djibouti" },
|
43
|
+
}
|
44
|
+
}
|
45
|
+
```
|
46
|
+
|
47
|
+
Calling `Collection.receive` works on either representation, and calling `to_a` or `to_hash` does what you'd think:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
africa.countries.to_a # [ <Geo::Country name="Rwanda"...>, <Geo::Country name="Djibouti"...> ]
|
51
|
+
africa.countries.to_hash # { :rw => <Geo::Country name="Rwanda"...>, :dj => <Geo::Country name="Djibouti"...> }
|
52
|
+
```
|
53
|
+
|
54
|
+
`Collection` proxies a small set of methods to an internal Hash. We purposefully keep you from calling methods that work differently on Hash and Array -- most notably, there is no `each` method, and it is currently *not* an Enumerable.
|
55
|
+
|
56
|
+
* `each_pair` iterates over the key/value pairs
|
57
|
+
* `each_value` iterates over the values
|
58
|
+
* `to_a` gives the list of values
|
59
|
+
* `to_hash` gives a duplicate of the proxied hash
|
60
|
+
* `inspect`, `to_s`, `as_json` and `to_json` show a list of values -- it serializes like an array
|
61
|
+
|
62
|
+
* `merge!`, `concat` or `receive!` add values in-place, and `merge` to a duplicate, as follows:
|
63
|
+
- if the given collection responds_to `to_hash`, it is merged into the internal collection; each hash key *must* match the id of its value or results are undefined.
|
64
|
+
- otherwise, it merges a hash generates from the id/value pairs of each object in the given collection.
|
65
|
+
|
66
|
+
* `[]=` adds a single value with a specified key; that key *must* match the value's id or it is undefined behavior.
|
67
|
+
* `<<` adds a single value at the right key.
|
68
|
+
|
69
|
+
* `build(attributes={})`
|
70
|
+
|
71
|
+
## advanced use
|
72
|
+
|
73
|
+
* `key_method` (read-only class attribute, a Symbol) defines how to find the id; by default, `:id`
|
74
|
+
* `factory` (read-only class attribute) defines how to create a new item given its raw material. This is handed to `Gorillib::Factory` for conversion the first time it's read.
|
75
|
+
|
76
|
+
## Decisions
|
77
|
+
|
78
|
+
## Mysteries
|
79
|
+
|
80
|
+
* ?? no `each` method
|
81
|
+
* ?? `Enumerable`
|
data/notes/factories.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# gorillib/factories (WIP -- NOT READY YET) -- efficient, predictable type conversion
|
2
|
+
|
3
|
+
* Is a missing value unset? or nil?
|
4
|
+
|
5
|
+
* (Boolean)
|
6
|
+
* String Symbol
|
7
|
+
* Regexp
|
8
|
+
* Bignum Integer Numeric
|
9
|
+
* Float (Double)
|
10
|
+
* Complex Rational
|
11
|
+
* Random
|
12
|
+
* Time
|
13
|
+
* Date
|
14
|
+
|
15
|
+
* Dir File Pathname
|
16
|
+
* Whatever -- alias for IdentityFactory
|
17
|
+
|
18
|
+
* Method, Proc
|
19
|
+
* Range
|
20
|
+
|
21
|
+
* Hash
|
22
|
+
* Array
|
23
|
+
|
24
|
+
* Object
|
25
|
+
* Class Module FalseClass TrueClass NilClass
|
26
|
+
|
27
|
+
These don't have factories because I can't really think of a reasonable use case
|
28
|
+
* DateTime Struct MatchData UnboundMethod IO Enumerator Fixnum Object File::Stat StringIO
|
29
|
+
|
30
|
+
These are *not* decorated because it's not a good idea
|
31
|
+
* BasicObject
|
32
|
+
* Exception Interrupt SignalException SystemExit
|
33
|
+
* Encoding Data Fiber Mutex ThreadGroup Thread Binding Queue
|
34
|
+
|
35
|
+
# name produces convert receivable? converted? nil "" [] {} false true
|
36
|
+
:Integer, Fixnum, :to_i, :to_i, is_a?(Float), nil nil, err, err
|
37
|
+
:Bignum
|
38
|
+
:Float, Float, :to_f, :to_f, is_a?(Fixnum), nil nil, err, err
|
39
|
+
:Complex
|
40
|
+
:Rational
|
41
|
+
|
42
|
+
# :Random
|
43
|
+
# :Long
|
44
|
+
# :Double
|
45
|
+
# :Numeric
|
46
|
+
|
47
|
+
:String, String, :to_s, :to_s, is_a?(String), nil, "", ??, ??
|
48
|
+
:Binary, Binary, :to_s, :to_s, is_a?(String), nil, "", ??, ??
|
49
|
+
:Symbol, Symbol, :to_s, :to_s, is_a?(String), nil, nil, ??, ??
|
50
|
+
:Regexp,
|
51
|
+
:Time
|
52
|
+
# :Date
|
53
|
+
# :Dir
|
54
|
+
# :File
|
55
|
+
# :Pathname
|
56
|
+
|
57
|
+
:Identity
|
58
|
+
:Whatever
|
59
|
+
|
60
|
+
:Class
|
61
|
+
:Module
|
62
|
+
:TrueClass, true,
|
63
|
+
:FalseClass, false,
|
64
|
+
:NilClass, nil,
|
65
|
+
:Boolean, [true,false], ??, [true, false], [true, false], nil, nil, ??, ??
|
66
|
+
|
67
|
+
:Hash
|
68
|
+
:Array
|
69
|
+
:Range
|
70
|
+
:Set
|
71
|
+
|
72
|
+
:Method
|
73
|
+
:Proc
|
74
|
+
|
75
|
+
INTERNAL_CLASSES_RE = /(^(Encoding|Struct|Gem)|Error\b|Errno|#<|fatal)/)
|
76
|
+
ADDABLE_CLASSES = [Identity, Whatever, Itself, Boolean, Long, Double, Pathname, Set]
|
77
|
+
INTERESTING_CLASSES = [String, Symbol, Regexp, Integer, Bignum, Float, Numeric, Complex, Rational, Time, Date, Dir, File, Hash, Array, Range, Method, Proc, Random, Class, Module, NilClass, TrueClass, FalseClass, ]
|
78
|
+
BORING_CLASSES = [Object, File::Stat, StringIO, DateTime, Struct, MatchData, UnboundMethod, IO, Enumerator, Fixnum, BasicObject, Exception, Interrupt, SignalException, SystemExit, Encoding, Data, Fiber, Mutex, ThreadGroup, Thread, Binding, ]
|
79
|
+
|
80
|
+
ObjectSpace.each_object(Class).
|
81
|
+
reject{|kl| (kl.to_s =~ INTERNAL_CLASSES_RE }.map(&:to_s).sort - (ADDED_CLASSES + INTERESTING_CLASSES + BORING_CLASSES)
|
82
|
+
|
83
|
+
|
84
|
+
[:to_ary, :to_io, :to_str, :to_hash, :to_sym, :to_path, :to_proc, :to_int, :to_f, :to_a, :to_r, :to_c, :to_i, :to_s, :to_enum]
|
85
|
+
|
86
|
+
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# gorillib/record/overlay (WIP -- NOT READY YET) -- Progressively overlayed configuration
|
2
|
+
|
3
|
+
`Record::Overlay` lets you specify an ordered stack of attributes, each overriding the following. For example, you may wih to load your program's configuration using
|
4
|
+
|
5
|
+
(wins) value set directly on self
|
6
|
+
commandline parameters
|
7
|
+
environment variables
|
8
|
+
per-user config file
|
9
|
+
machine-wide config file
|
10
|
+
(loses) application defaults
|
11
|
+
|
12
|
+
In the Ironfan toolset, servers are organized into hierarchical groups -- 'provider', then 'cluster', then 'facet'. Its configuration builder lets you describe a cloud server as follows:
|
13
|
+
|
14
|
+
(wins) server-specific
|
15
|
+
facet
|
16
|
+
cluster
|
17
|
+
provider
|
18
|
+
(loses) default
|
19
|
+
|
20
|
+
### Higher layers override lower layers
|
21
|
+
|
22
|
+
In the following example, we set a value for the `environment` attribute on the cluster, then override it in the facet:
|
23
|
+
|
24
|
+
cluster :gibbon do |c|
|
25
|
+
c.environment :prod
|
26
|
+
|
27
|
+
c.facet :worker do |f|
|
28
|
+
# since no value has been set on the facet, a read pokes through to the cluster's value
|
29
|
+
f.environment #=> :prod
|
30
|
+
# setting a value on the facet overrides the value from the cluster:
|
31
|
+
f.environment :dev
|
32
|
+
f.environment #=> :dev
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
This diagram might help, for a hypothetical 'owner' setting in confilgiere:
|
37
|
+
|
38
|
+
Settings:
|
39
|
+
+-- reads come in from the top
|
40
|
+
_______________________ |
|
41
|
+
| .owner | \|/
|
42
|
+
+----------------------+------------------+
|
43
|
+
| self | |
|
44
|
+
| overlay(:commandline) | |
|
45
|
+
| overlay(:env_vars) | 'bob' | <-- writes go in from the side
|
46
|
+
| overlay(:user_conf) | |
|
47
|
+
| overlay(:system_conf) | 'alice' | <--
|
48
|
+
| default, if any | |
|
49
|
+
+----------------------+------------------+
|
50
|
+
|
51
|
+
In this diagram, values have been set on the objects at the 'env_vars' and 'system_conf' layers, so when you read the final settings value with `Settings.owner` it returns `"bob"`. If the user had specified `--owner="charlie"` on the commandline, it would have set a value in the 'commandline' row and taken precedence; `Settings.owner` would return `"charlie"` instead.
|
52
|
+
|
53
|
+
### Late Resolution means overlays are dynamic
|
54
|
+
|
55
|
+
Overlay layers are late-resolved -- they return whatever value is appropriate at time of read:
|
56
|
+
|
57
|
+
cluster(:gibbon) do |c|
|
58
|
+
c.environment(:prod)
|
59
|
+
# since no value has been set on the facet, a read pokes through to the cluster's value
|
60
|
+
c.facet(:worker).environment #=> :prod
|
61
|
+
|
62
|
+
# changing the cluster's value changes the facet's effective value as well:
|
63
|
+
c.environment(:test)
|
64
|
+
c.facet(:worker).environment #=> :test
|
65
|
+
end
|
66
|
+
|
67
|
+
## Defining an Overlay
|
68
|
+
|
69
|
+
You can apply overlay behavior to any `Gorillib::Record`: the `Builder`, `Model` and `Bucket` classes, or any diabolical little variant you've cooked up.
|
70
|
+
|
71
|
+
An Overlay'ed object's `read_unset_attribute` calls `super`, so if there is a default value but neither the object nor any overlay have a value set,
|
72
|
+
|
73
|
+
## Cascading attributes
|
74
|
+
|
75
|
+
I want to be able to define what's uniform across a collection at one layer, and customize only what's special at another. In this example, our overlay stack is `cluster > facet`; each has a `volumes` collection field. The `:master` facet overrides the size and tags applied to the volume; in the latter case this means modifying the value read from the cluster layer and applying it to the facet layer:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
cluster :gibbon do
|
79
|
+
volumes(:hadoop_data) do
|
80
|
+
size 300
|
81
|
+
mount_point '/hadoop_data'
|
82
|
+
tags( :hadoop_data => true, :persistent => true, :bulk => true)
|
83
|
+
end
|
84
|
+
# the master uses a smaller volume and only stores data for the namenode
|
85
|
+
facet :master do
|
86
|
+
volumes(:hadoop_data) do
|
87
|
+
size 50
|
88
|
+
tags tags.except(:hadoop_data).merge(:hadoop_namenode_data => true)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# workers use the volume just as described
|
92
|
+
facet :worker
|
93
|
+
# ...
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
_note_: This does not feel elegant at all.
|
99
|
+
|
100
|
+
class Facet
|
101
|
+
attr_accessor :cluster
|
102
|
+
overlays :cluster
|
103
|
+
collection :volumes, Volume, :members_overlay => :cluster
|
104
|
+
end
|
105
|
+
|
106
|
+
When a collection element is built on the
|
107
|
+
|
108
|
+
|
109
|
+
## Deep Merge
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
## Dynamic Resolution Rules
|
114
|
+
|
115
|
+
As you've seen, the core resolution rule is 'first encountered wins'. Simple and predictable, but sometimes insufficient.
|
116
|
+
|
117
|
+
Define your field with a `:combine_with` block (or anything that responds to `#call`) to specify a custom resolution rule:
|
118
|
+
|
119
|
+
Settings.option :flavors, Array, :of => String, :combine_with => ->(obj,attr,layer_vals){ ... }
|
120
|
+
|
121
|
+
Whenever there's a conflict, the block will be called with these arguments:
|
122
|
+
|
123
|
+
* the host object,
|
124
|
+
* the field name in question,
|
125
|
+
* a list of the layer values in this order:
|
126
|
+
- field default (if set)
|
127
|
+
- lowest layer's value (if set)
|
128
|
+
- ...
|
129
|
+
- highest layer's value (if set)
|
130
|
+
- object's own value (if set)
|
131
|
+
|
132
|
+
If you pass a symbol to `:combine_with`, the corresponding method will be invoked on each layer, starting with the lowest-priority:
|
133
|
+
|
134
|
+
Settings.option :flavors, Array, :of => String, :combine_with => :concat
|
135
|
+
# effectively performs
|
136
|
+
# layer(:charlie).flavors.
|
137
|
+
# concat( layer(:bob).flavors ).
|
138
|
+
# concat( layer(:alice).flavors )
|
139
|
+
|
140
|
+
Settings.option :flavors, Hash, :of => String, :combine_with => :merge
|
141
|
+
# effectively performs
|
142
|
+
# layer(:charlie).flavors.
|
143
|
+
# merge( layer(:bob).flavors ).
|
144
|
+
# merge( layer(:alice).flavors )
|
145
|
+
|
146
|
+
Notes:
|
147
|
+
* a field's default counts as a layer: the combiner is invoked even if just the default and a value are present.
|
148
|
+
* normal (first-encountered-wins) resolution uses `read_unset_attribute`
|
149
|
+
* dynamic (explicit-block) resolution uses `read_attribute`
|
150
|
+
* there's not a lot of sugar here, because my guess is that concat'ing arrays and merging or deep-merging hashes covers all the repeatable cases.
|
151
|
+
|
152
|
+
__________________________________________________________________________
|
153
|
+
__________________________________________________________________________
|
154
|
+
__________________________________________________________________________
|
155
|
+
|
156
|
+
|
157
|
+
## Decisions
|
158
|
+
|
159
|
+
* Layer *does not* catch `Gorillib::UnknownFieldError` exceptions -- a layered field must be defined on each part of the layer stack.
|
160
|
+
* a field's default counts as a layer: the combiner is invoked even if just the default and a value are present
|
161
|
+
|
162
|
+
__________________________________________________________________________
|
163
|
+
|
164
|
+
**Provisional**:
|
165
|
+
|
166
|
+
## LayeredObject -- push some properties through to parent; earn predictable deep merge characteristics
|
167
|
+
|
168
|
+
* holds a stack of objects
|
169
|
+
* properties 'poke through' to get value with well-defined resultion rules
|
170
|
+
* you can define a custom resolution rule if you define a property, or when you set its value
|
171
|
+
|
172
|
+
* Properties are resolved in a first-one-wins
|
173
|
+
* Defined properties can have resolution rules attached
|
174
|
+
* You can put yourself in an explicit scope; higher properties are set, lower ones appear unset
|
175
|
+
|
176
|
+
Server overlays facet
|
177
|
+
Facet overlays cluster
|
178
|
+
Cluster overlays provider
|
179
|
+
|
180
|
+
don't prescribe *any* resolution semantics except the following:
|
181
|
+
|
182
|
+
* layers are evaluated in order to create a composite, `dup`ing as you go.
|
183
|
+
* while building a composite, when a later layer and the current layer collide:
|
184
|
+
- if layer has merge logic, hand it off.
|
185
|
+
- simple + any: clobber
|
186
|
+
- array + array: layer value appended to composite value
|
187
|
+
- hash + hash: recurse
|
188
|
+
- otherwise: error
|
189
|
+
|
190
|
+
### no complicated resolution rules allowed
|
191
|
+
|
192
|
+
This is the key to the whole thing.
|
193
|
+
|
194
|
+
* You can very easily
|
195
|
+
* You can adorn an object with merge logic if it's more complicated than that
|
196
|
+
|
197
|
+
(?mystery - duping: I'm not certain of the always-`dup` rule. We'll find out).
|
198
|
+
|
199
|
+
How do volumes overlay?
|
200
|
+
|
201
|
+
Which direction is overlay declared?
|
202
|
+
|
203
|
+
Configliere:
|
204
|
+
|
205
|
+
* Default
|
206
|
+
* File
|
207
|
+
* Env var
|
208
|
+
* Cmdline
|
209
|
+
* Setting it directly
|
data/notes/model.md
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
# gorillib/record -- construct lightweight structured data classes
|
2
|
+
|
3
|
+
## Goals
|
4
|
+
|
5
|
+
* light, predictable magic; you can define records without requiring everything & the kitchen sink.
|
6
|
+
* No magic in normal operation: you are left with regular instance-variable attrs, control over your initializer, and in almost every respect can do anything you'd like to do with a regular ruby class.
|
7
|
+
* Compatible with the [Avro schema format](http://avro.apache.org/)'s terminology & conceptual model
|
8
|
+
* Upwards compatible with ActiveSupport / ActiveModel
|
9
|
+
* All four obey the basic contract of a Gorillib::Record
|
10
|
+
* Encourages assertive code -- no `method_missing` or complex proxy soup.
|
11
|
+
|
12
|
+
___________________________________________________________________________
|
13
|
+
|
14
|
+
## Gorillib::Record
|
15
|
+
|
16
|
+
### Defining a record
|
17
|
+
|
18
|
+
To make a class a record, simply `include Gorillib::Record`.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class Place
|
22
|
+
include Gorillib::Record
|
23
|
+
field :name, String
|
24
|
+
field :geo, GeoCoordinates, :doc => 'geographic location of the place'
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
### Field
|
29
|
+
|
30
|
+
A record has `field`s that describe its attributes. The simplest definition just requires a field name and a type: `field :name, String`. (Use `Object` as the type to accept any value as-is). Specify optional attributes using keyword arguments -- for example, `doc` describes the field:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
field :geo, GeoCoordinates, :doc => 'geographic location of the place'
|
34
|
+
```
|
35
|
+
|
36
|
+
You can list the fields, the field names (in the order they were defined), and ask if a field has been defined:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Place.fields #=> {:name=>field(:name, Integer), :geo=>field(:geo, Geocoordinates)}
|
40
|
+
Place.field_names #=> [:name, :geo]
|
41
|
+
Place.has_field?(:name) #=> true
|
42
|
+
```
|
43
|
+
|
44
|
+
Subclasses inherit their parent's fields, just as you'd expect:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class Stadium < Place
|
48
|
+
field :capacity, Integer, :doc => 'quantity of seats'
|
49
|
+
end
|
50
|
+
Stadium.field_names #=> [:name, :geo, :capacity]
|
51
|
+
|
52
|
+
# Add a field to the parent and it shows up on the children, no sweat:
|
53
|
+
Place.field :country_id, String
|
54
|
+
Place.field_names #=> [:name, :geo, :country_id]
|
55
|
+
Stadium.field_names #=> [:name, :geo, :capacity, :country_id]
|
56
|
+
```
|
57
|
+
|
58
|
+
### Reading, writing and unsetting values
|
59
|
+
|
60
|
+
Defining a field defines accessor methods:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
lunch_spot = Place.receive({ :name => "Torchy's Tacos", :country_id => "us",
|
64
|
+
:geo => { :latitude => "30.295", :longitude => "-97.745" }})
|
65
|
+
|
66
|
+
### Attributes
|
67
|
+
|
68
|
+
(A class defines `fields`; instances receive `value`s for those fields; the collection of an instance's `values` form its `attributes`)
|
69
|
+
|
70
|
+
## Contract
|
71
|
+
|
72
|
+
Every record responds to and guarantees uniform behavior for these methods:
|
73
|
+
|
74
|
+
* Class methods from `Gorillib::Record` -- `field`, `fields`, `field_names`, `has_field?`, `metamodel`
|
75
|
+
* Instance methods from `Gorillib::Record` -- `read_attribute`, `write_attribute`, `unset_attribute`, `attribute_set?`, `read_unset_attribute`, `attributes`
|
76
|
+
|
77
|
+
Records generally respond to the following, but are allowed to get fancy as long as they fulfill your basic expectations (and can mark accessors private/protected or omit them):
|
78
|
+
|
79
|
+
* Class methods from `Gorillib::Record` -- `receive`, `inspect`
|
80
|
+
* Instance methods from `Gorillib::Record` -- `receive!`, `update`, `inspect`, `to_s`, `==`
|
81
|
+
* Metamodel methods (eg, field named 'foo') -- `receive_foo`, `foo=`, `foo`
|
82
|
+
|
83
|
+
These are the only
|
84
|
+
|
85
|
+
* `Object#blank?`, `Hash#symbolize_keys`, `Object#try`
|
86
|
+
|
87
|
+
### `read_attribute`, `write_attribute` and friends
|
88
|
+
|
89
|
+
All normal access to attributes goes through `read_attribute`, `write_attribute`, `unset_attribute` and `attribute_set?`. All 'fixup' access goes through each field's `receive_XXX` method, which calls `write_attribute` in turn. This provides a consistent attachment point for advanced magic.
|
90
|
+
|
91
|
+
external methods fixup gate accessor gate
|
92
|
+
|
93
|
+
Klass.receive => receive_foo(val) => write_attribute(:foo, val)
|
94
|
+
receive!(:foo => val) => receive_foo(val) => write_attribute(:foo, val)
|
95
|
+
receive_foo(val) => write_attribute(:foo, val)
|
96
|
+
update_attributes(:foo => val) => write_attribute(:foo, val)
|
97
|
+
foo=(val) => write_attribute(:foo, val)
|
98
|
+
|
99
|
+
attributes => read_attribute(:foo)
|
100
|
+
foo => read_attribute(:foo)
|
101
|
+
|
102
|
+
attribute_set?(:foo)
|
103
|
+
unset_attribute(:foo)
|
104
|
+
|
105
|
+
If you are writing library code to extend `Gorillib::Record`, you *must* call the `xx_attribute` methods -- do not assume any behavior from (or even the existence of) accessors or anything else. By default, the core `xx_attribute` methods get/set/remove instance variables, but we've deliberately left them open to be implemented as hash values, by delegation, as a passthrough to database access, or things as-yet undreamt of. That's just for library code, though -- your class knows how it's built and can naturally leverage all its amenities.
|
106
|
+
|
107
|
+
If you call `read_attribute` on an unset value, it in turn calls `read_unset_attribute`; the mixins that provide defaults, lazy access, or layered configuration hook in here.
|
108
|
+
|
109
|
+
|
110
|
+
### method visibility
|
111
|
+
|
112
|
+
You can mark a field's methods (`:reader`, `:writer`, `:receiver`) as public, private or protected, or even prevent its creation in the first place:
|
113
|
+
|
114
|
+
field :monogram, String, :writer => false, :receiver => :protected # a read-only field
|
115
|
+
|
116
|
+
Visibility can be `:public`, `:protected`, `:private`, `true` (meaning `:public`) or `false` (in which case no method is manufactured at all).
|
117
|
+
|
118
|
+
### extra_attributes
|
119
|
+
|
120
|
+
Extra attributes passed to `receive!` are collected in `@extra_attributes`, but nothing is done with them.
|
121
|
+
|
122
|
+
|
123
|
+
## Record::Default
|
124
|
+
|
125
|
+
|
126
|
+
* default values
|
127
|
+
- nil by default
|
128
|
+
- simple value is returned directly
|
129
|
+
- Proc is `call`ed: `->{ Time.now }`
|
130
|
+
- to return a block as default, just wrap it in a dummy block: `->{ ->(obj,fn){ } }`
|
131
|
+
- The block may store an attribute value if desired, but must do so explicitly; otherwise, the block will be invoked on every access while the value is unset.
|
132
|
+
- how to invoke?
|
133
|
+
- foo_default
|
134
|
+
- attribute_default(:foo)
|
135
|
+
- field(:foo).default(self)
|