-
Notifications
You must be signed in to change notification settings - Fork 6
model
- light, predictable magic; you can define records without requiring everything & the kitchen sink.
- 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.
- Compatible with the Avro schema format's terminology & conceptual model
- Upwards compatible with ActiveSupport / ActiveModel
- All four obey the basic contract of a Gorillib::Record
- Encourages assertive code -- no
method_missing
or complex proxy soup.
To make a class a record, simply include Gorillib::Record
.
```ruby
class Place
include Gorillib::Record
field :name, String
field :geo, GeoCoordinates, :doc => 'geographic location of the place'
end
```
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:
```ruby
field :geo, GeoCoordinates, :doc => 'geographic location of the place'
```
You can list the fields, the field names (in the order they were defined), and ask if a field has been defined:
```ruby
Place.fields #=> {:name=>field(:name, Integer), :geo=>field(:geo, Geocoordinates)}
Place.field_names #=> [:name, :geo]
Place.has_field?(:name) #=> true
```
Subclasses inherit their parent's fields, just as you'd expect:
```ruby
class Stadium < Place
field :capacity, Integer, :doc => 'quantity of seats'
end
Stadium.field_names #=> [:name, :geo, :capacity]
# Add a field to the parent and it shows up on the children, no sweat:
Place.field :country_id, String
Place.field_names #=> [:name, :geo, :country_id]
Stadium.field_names #=> [:name, :geo, :capacity, :country_id]
```
Defining a field defines accessor methods:
```ruby
lunch_spot = Place.receive({ :name => "Torchy's Tacos", :country_id => "us",
:geo => { :latitude => "30.295", :longitude => "-97.745" }})
(A class defines fields
; instances receive value
s for those fields; the collection of an instance's values
form its attributes
)
Every record responds to and guarantees uniform behavior for these methods:
- Class methods from
Gorillib::Record
--field
,fields
,field_names
,has_field?
,metamodel
- Instance methods from
Gorillib::Record
--read_attribute
,write_attribute
,unset_attribute
,attribute_set?
,read_unset_attribute
,attributes
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):
- Class methods from
Gorillib::Record
--receive
,inspect
- Instance methods from
Gorillib::Record
--receive!
,update
,inspect
,to_s
,==
- Metamodel methods (eg, field named 'foo') --
receive_foo
,foo=
,foo
These are the only
-
Object#blank?
,Hash#symbolize_keys
,Object#try
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.
external methods fixup gate accessor gate
Klass.receive => receive_foo(val) => write_attribute(:foo, val)
receive!(:foo => val) => receive_foo(val) => write_attribute(:foo, val)
receive_foo(val) => write_attribute(:foo, val)
update_attributes(:foo => val) => write_attribute(:foo, val)
foo=(val) => write_attribute(:foo, val)
attributes => read_attribute(:foo)
foo => read_attribute(:foo)
attribute_set?(:foo)
unset_attribute(:foo)
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.
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.
You can mark a field's methods (:reader
, :writer
, :receiver
) as public, private or protected, or even prevent its creation in the first place:
field :monogram, String, :writer => false, :receiver => :protected # a read-only field
Visibility can be :public
, :protected
, :private
, true
(meaning :public
) or false
(in which case no method is manufactured at all).
Extra attributes passed to receive!
are collected in @extra_attributes
, but nothing is done with them.
- default values
- nil by default
- simple value is returned directly
- Proc is
call
ed:->{ Time.now }
- to return a block as default, just wrap it in a dummy block:
->{ ->(obj,fn){ } }
- 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.
- to return a block as default, just wrap it in a dummy block:
- how to invoke?
- foo_default
- attribute_default(:foo)
- field(:foo).default(self)