# gorillib's structured data classes -- Record, Model, Builder and Bucket ## Overview Gorillib provides these general flavors of model: * `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. ```ruby class Place include Gorillib::Record # fields can be simple... field :name, String field :country_id, String, :doc => 'Country code (2-letter alpha) containing the place' # ... or complext field :geo, GeoCoordinates, :doc => 'geographic location of the place' end class GeoCoordinates include Gorillib::Record field :latitude, Float, :doc => 'latitude in decimal degrees; negative numbers are south of the equator' field :longitude, Float, :doc => 'longitude in decimal degrees; negative numbers are west of Greenwich' end # It's simple to instantiate complex nested data structures lunch_spot = Place.receive({ :name => "Torchy's Tacos", :country_id => "us", :geo => { :latitude => "30.295", :longitude => "-97.745" }}) ``` * `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 ```ruby class GeoCoordinates include Gorillib::Model field :latitude, Float, :doc => 'latitude in decimal degrees; negative numbers are south of the equator', :validates => { :numericality => { :>= => -90, :<= => 90 } } field :longitude, Float, :doc => 'longitude in decimal degrees; negative numbers are west of Greenwich', :validates => { :numericality => { :>= => -180, :<= => 180 } } end position = GeoCoordinates.from_tuple(30.295, -97.745) # A Gorillib::Model obeys the ActiveModel contract GeoCoordinates.model_name.human # => 'Geo coordinates' ``` * `Gorillib::Builder`: **foundation models for elegant ruby DSLs** (Domain-Specific Languages). Relaxes ruby syntax to enable highly-readable specification of complex behavior. ```ruby workflow(:bake_pie) do step :make_crust step :add_filling step :bake step :cool end ``` * `Gorillib::Bucket` (?name?): **record-style access to freeform hashes**. Provide a disciplined interface on top of arbitrarily-structured data. Used by [[Configliere]] and others. ```ruby # pre-defining a field gives you special powers... Settings.define :port, Integer, :doc => 'API server port number', :default => 80 # but you can still store or read anything you'd like... Settings[:shout] = "SAN DIMAS HIGH SCHOOL FOOTBALL RULES" # and treat the object as a hash when you'd like to conn = Connections.open(Settings.merge(user_overrides)) ``` ## Decisions * **initializer** - *does not* inject an initializer; if you want one, do `alias_method :initialize, :receive!` * **frills** -- - *does inject*: `inspect` on the class, `inspect`, `to_s` on the instance - *does inject*: accessors (`foo`, `foo=`) for each field. - *does inject*: `==` on the instance - *does not define*: `schema`, `initialize` * **field defaults** - evaluated on first read, at which point its value is fixed on the record. * **define_metamodel_record** -- visibility=false does not remove an existing method from the metamodel * ?? **hash vs mash** -- does `attributes` return a mash or hash? * are these basic functionality: - **extra_attributes** -- ?? - **default** -- ?? * Record: - class methods -- `field`, `fields`, `field_names`, `has_field?`, `metamodel`, `receive`, `inspect` - instance methods -- `read_attribute`, `write_attribute`, `unset_attribute`, `attribute_set?`, `attributes`, `receive!`, `update`, `inspect`, `to_s`, `==` - with each attribute -- `receive_foo`, `foo=`, `foo` * Builder: - defining classes before they're used ## Features ### `Record` * `Record::Ordered` -- sort order on fields (and thus record) * `Record::FieldAliasing` -- aliases for fields and receivers * `Record::Defaults` -- default values * `Record::Schema` -- icss schema * `Record::HashAccessors` -- adds `[]`, `[]=`, `delete`, `keys`, `#has_key?`, `to_hash`, and `update`. This allows it to be `hashlike`, but you must include that explicitly. * `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` ### `Builder` * `Builder::GetsetField` -- ### `HashRecord` ### `Model` * `Model::Naming` * `Model::Conversion` -- ### active_model / active_model_lite From `active_model` or `active_model_lite`: * `Model::Callbacks` -- * `Model::Dirty` -- * `Model::Serialization` -- * `Model::Validations` -- implies `Model::Errors`, `Model::Callbacks` ## Why, in a world with ActiveRecord, Datamapper, Hashie, Struct, ..., do we need yet another damn model framework? 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. 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.