gorillib 0.4.2pre → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,209 +0,0 @@
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 DELETED
@@ -1,135 +0,0 @@
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)
@@ -1,127 +0,0 @@
1
- # gorillib's structured data classes -- Record, Model, Builder and Bucket
2
-
3
- ## Overview
4
-
5
- Gorillib provides these general flavors of model:
6
-
7
- * `Gorillib::Record`: **lightweight structured records**. Easily assemble a dynamic data structure that enables both rich behavior and generic manipulation, serialization and transformation. Especially useful when you just need to pull something from JSON, attach some functionality, and get it back on the wire.
8
-
9
- ```ruby
10
- class Place
11
- include Gorillib::Record
12
- # fields can be simple...
13
- field :name, String
14
- field :country_id, String, :doc => 'Country code (2-letter alpha) containing the place'
15
- # ... or complext
16
- field :geo, GeoCoordinates, :doc => 'geographic location of the place'
17
- end
18
-
19
- class GeoCoordinates
20
- include Gorillib::Record
21
- field :latitude, Float, :doc => 'latitude in decimal degrees; negative numbers are south of the equator'
22
- field :longitude, Float, :doc => 'longitude in decimal degrees; negative numbers are west of Greenwich'
23
- end
24
-
25
- # It's simple to instantiate complex nested data structures
26
- lunch_spot = Place.receive({ :name => "Torchy's Tacos", :country_id => "us",
27
- :geo => { :latitude => "30.295", :longitude => "-97.745" }})
28
- ```
29
-
30
- * `Gorillib::Model`: **rich structured models** offering predictable magic with a disciplined footprint. Comparable to ActiveRecord or Datamapper, but for a world dominated by JSON+HTTP, not relational databases
31
-
32
- ```ruby
33
- class GeoCoordinates
34
- include Gorillib::Model
35
- field :latitude, Float, :doc => 'latitude in decimal degrees; negative numbers are south of the equator', :validates => { :numericality => { :>= => -90, :<= => 90 } }
36
- field :longitude, Float, :doc => 'longitude in decimal degrees; negative numbers are west of Greenwich', :validates => { :numericality => { :>= => -180, :<= => 180 } }
37
- end
38
- position = GeoCoordinates.from_tuple(30.295, -97.745)
39
- # A Gorillib::Model obeys the ActiveModel contract
40
- GeoCoordinates.model_name.human # => 'Geo coordinates'
41
- ```
42
-
43
- * `Gorillib::Builder`: **foundation models for elegant ruby DSLs** (Domain-Specific Languages). Relaxes ruby syntax to enable highly-readable specification of complex behavior.
44
-
45
- ```ruby
46
- workflow(:bake_pie) do
47
- step :make_crust
48
- step :add_filling
49
- step :bake
50
- step :cool
51
- end
52
- ```
53
-
54
- * `Gorillib::Bucket` (?name?): **record-style access to freeform hashes**. Provide a disciplined interface on top of arbitrarily-structured data. Used by [[Configliere]] and others.
55
-
56
- ```ruby
57
- # pre-defining a field gives you special powers...
58
- Settings.define :port, Integer, :doc => 'API server port number', :default => 80
59
- # but you can still store or read anything you'd like...
60
- Settings[:shout] = "SAN DIMAS HIGH SCHOOL FOOTBALL RULES"
61
- # and treat the object as a hash when you'd like to
62
- conn = Connections.open(Settings.merge(user_overrides))
63
- ```
64
-
65
- ## Decisions
66
-
67
- * **initializer** - *does not* inject an initializer; if you want one, do `alias_method :initialize, :receive!`
68
- * **frills** --
69
- - *does inject*: `inspect` on the class, `inspect`, `to_s` on the instance
70
- - *does inject*: accessors (`foo`, `foo=`) for each field.
71
- - *does inject*: `==` on the instance
72
- - *does not define*: `schema`, `initialize`
73
- * **field defaults** - evaluated on first read, at which point its value is fixed on the record.
74
-
75
- * **define_metamodel_record** -- visibility=false does not remove an existing method from the metamodel
76
-
77
- * ?? **hash vs mash** -- does `attributes` return a mash or hash?
78
- * are these basic functionality:
79
- - **extra_attributes** -- ??
80
- - **default** -- ??
81
-
82
- * Record:
83
- - class methods -- `field`, `fields`, `field_names`, `has_field?`, `metamodel`, `receive`, `inspect`
84
- - instance methods -- `read_attribute`, `write_attribute`, `unset_attribute`, `attribute_set?`, `attributes`, `receive!`, `update`, `inspect`, `to_s`, `==`
85
- - with each attribute -- `receive_foo`, `foo=`, `foo`
86
-
87
- * Builder:
88
- - defining classes before they're used
89
-
90
-
91
- ## Features
92
-
93
- ### `Record`
94
-
95
- * `Record::Ordered` -- sort order on fields (and thus record)
96
- * `Record::FieldAliasing` -- aliases for fields and receivers
97
- * `Record::Defaults` -- default values
98
- * `Record::Schema` -- icss schema
99
- * `Record::HashAccessors` -- adds `[]`, `[]=`, `delete`, `keys`, `#has_key?`, `to_hash`, and `update`. This allows it to be `hashlike`, but you must include that explicitly.
100
- * `Record::Hashlike` -- mixes in `Record::HashAccessors` and `Gorillib::Hashlike`, making it behave in almost every respect like a hash. Use this when you want something that will *behave* like a hash but *be* a record. If you want something to *be* a hash but *behave* like a record, use the `Gorillib::MashRecord`
101
-
102
- ### `Builder`
103
-
104
- * `Builder::GetsetField` --
105
-
106
- ### `HashRecord`
107
-
108
- ### `Model`
109
-
110
- * `Model::Naming`
111
- * `Model::Conversion` --
112
-
113
- ### active_model / active_model_lite
114
-
115
- From `active_model` or `active_model_lite`:
116
-
117
- * `Model::Callbacks` --
118
- * `Model::Dirty` --
119
- * `Model::Serialization` --
120
- * `Model::Validations` -- implies `Model::Errors`, `Model::Callbacks`
121
-
122
-
123
- ## Why, in a world with ActiveRecord, Datamapper, Hashie, Struct, ..., do we need yet another damn model framework?
124
-
125
- ActiveRecord and Datamapper excel in a world where data (and truth) live in the database. ActiveRecord sets the standard for elegant magic, but is fairly heavyweight (I don't want to include a full XML serialization suite just so I can validate records). This often means it's overkill for the myriad flyweight scripts, Goliath apps, and such that we deploy. Datamapper does a remarkable job of delivering power while still being light on its toes, but ultimately is too tightly bound to an ORM view of the world. Hashie, Structs and OStructs behave as both hashes and records. In my experience, this interface is too generous -- their use leads to mealymouthed code.
126
-
127
- More importantly, our data spends most of its time on the wire or being handled as an opaque blob of data; a good amount of time being handled as a generic bundle of properties; and (though most important) a relatively small amount of time as an active, assertive object. So type conversion and validation are fundamental actions, but shouldn't crud up my critical path or be required. Models should offer predictable and disciplined features, but be accessable as generic bags of facts.