Skip to content
Philip (flip) Kromer edited this page May 15, 2012 · 3 revisions

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'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 fields 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 values 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 called: ->{ 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)