# gorillib/record -- construct lightweight structured data classes ## Goals * 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](http://avro.apache.org/)'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. ___________________________________________________________________________ ## Gorillib::Record ### Defining a record 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 ``` ### Field 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] ``` ### Reading, writing and unsetting values 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" }}) ### Attributes (A class defines `fields`; instances receive `value`s for those fields; the collection of an instance's `values` form its `attributes`) ## Contract 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` ### `read_attribute`, `write_attribute` and friends 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. ### method visibility 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 Extra attributes passed to `receive!` are collected in `@extra_attributes`, but nothing is done with them. ## Record::Default * 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. - how to invoke? - foo_default - attribute_default(:foo) - field(:foo).default(self)