gorillib 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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`
@@ -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)