gorillib 0.4.0pre → 0.4.1pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG.md +36 -1
  2. data/Gemfile +23 -19
  3. data/Guardfile +1 -1
  4. data/Rakefile +31 -31
  5. data/TODO.md +2 -30
  6. data/VERSION +1 -1
  7. data/examples/builder/ironfan.rb +4 -4
  8. data/gorillib.gemspec +40 -25
  9. data/lib/gorillib/array/average.rb +13 -0
  10. data/lib/gorillib/array/sorted_median.rb +11 -0
  11. data/lib/gorillib/array/sorted_percentile.rb +11 -0
  12. data/lib/gorillib/array/sorted_sample.rb +12 -0
  13. data/lib/gorillib/builder.rb +8 -14
  14. data/lib/gorillib/collection/has_collection.rb +31 -31
  15. data/lib/gorillib/collection/list_collection.rb +58 -0
  16. data/lib/gorillib/collection/model_collection.rb +63 -0
  17. data/lib/gorillib/collection.rb +57 -85
  18. data/lib/gorillib/logger/log.rb +26 -22
  19. data/lib/gorillib/model/base.rb +52 -39
  20. data/lib/gorillib/model/doc_string.rb +15 -0
  21. data/lib/gorillib/model/factories.rb +56 -61
  22. data/lib/gorillib/model/lint.rb +24 -0
  23. data/lib/gorillib/model/serialization.rb +12 -2
  24. data/lib/gorillib/model/validate.rb +2 -2
  25. data/lib/gorillib/pathname.rb +21 -6
  26. data/lib/gorillib/some.rb +2 -0
  27. data/lib/gorillib/type/extended.rb +0 -2
  28. data/lib/gorillib/type/url.rb +9 -0
  29. data/lib/gorillib/utils/console.rb +4 -1
  30. data/notes/HOWTO.md +22 -0
  31. data/notes/bucket.md +155 -0
  32. data/notes/builder.md +170 -0
  33. data/notes/collection.md +81 -0
  34. data/notes/factories.md +86 -0
  35. data/notes/model-overlay.md +209 -0
  36. data/notes/model.md +135 -0
  37. data/notes/structured-data-classes.md +127 -0
  38. data/spec/array/average_spec.rb +24 -0
  39. data/spec/array/sorted_median_spec.rb +18 -0
  40. data/spec/array/sorted_percentile_spec.rb +24 -0
  41. data/spec/array/sorted_sample_spec.rb +28 -0
  42. data/spec/gorillib/builder_spec.rb +46 -28
  43. data/spec/gorillib/collection_spec.rb +195 -10
  44. data/spec/gorillib/model/lint_spec.rb +28 -0
  45. data/spec/gorillib/model/record/factories_spec.rb +27 -13
  46. data/spec/gorillib/model/serialization_spec.rb +3 -5
  47. data/spec/gorillib/model_spec.rb +86 -104
  48. data/spec/spec_helper.rb +2 -1
  49. data/spec/support/gorillib_test_helpers.rb +83 -7
  50. data/spec/support/model_test_helpers.rb +9 -28
  51. metadata +52 -44
  52. data/lib/gorillib/configurable.rb +0 -28
  53. data/spec/gorillib/configurable_spec.rb +0 -62
  54. 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.
@@ -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)