gorillib 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/gorillib.gemspec +32 -5
- data/lib/gorillib/array/hashify.rb +11 -0
- data/lib/gorillib/base.rb +1 -0
- data/lib/gorillib/data_munging.rb +8 -1
- data/lib/gorillib/exception/raisers.rb +6 -1
- data/lib/gorillib/factories.rb +26 -13
- data/lib/gorillib/model/base.rb +6 -1
- data/lib/gorillib/model/schema_magic.rb +1 -0
- data/lib/gorillib/model/serialization/csv.rb +2 -0
- data/lib/gorillib/model/serialization/json.rb +44 -0
- data/lib/gorillib/model/serialization/lines.rb +30 -0
- data/lib/gorillib/model/serialization/tsv.rb +55 -0
- data/lib/gorillib/pathname/utils.rb +34 -0
- data/lib/gorillib/type/extended.rb +1 -0
- data/lib/gorillib/type/ip_address.rb +153 -0
- data/notes/HOWTO.md +22 -0
- data/notes/bucket.md +155 -0
- data/notes/builder.md +170 -0
- data/notes/collection.md +81 -0
- data/notes/factories.md +86 -0
- data/notes/model-overlay.md +209 -0
- data/notes/model.md +135 -0
- data/notes/structured-data-classes.md +127 -0
- data/spec/gorillib/array/hashify_spec.rb +20 -0
- data/spec/gorillib/builder_spec.rb +2 -2
- data/spec/gorillib/{model/factories_spec.rb → factories_spec.rb} +3 -5
- data/spec/gorillib/model/serialization/tsv_spec.rb +17 -0
- data/spec/gorillib/type/ip_address_spec.rb +143 -0
- metadata +35 -5
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)
|