now abanoned, triplicate is a stab at doing "model-less" document validation and processing in ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.



by Matthew Lyon

A document presenter/processor library that attempts to minimize abstraction leakage. Forms, not Models.


One of the big selling points of NoSQL-style document storage systems is a schema-free design that promises to free us from ever doing a "migration" or anything resembling ALTER TABLE ever again. The APIs for systems such as CouchDB and MongoDB allow JSON-like nested, open-key hash/dict structures for our data, a freedom from the flat, restricted-key hash/dict structures that SQL table rows impose on us.

Yet raw hash/dict structures are fragile things and the real world is a tough place. We expect that certain values will conform to certain datatypes or match certain constraints. We deal with untrustworthy user input and need to restrict write access, or we're sending our document out into the wide world and need to mask certain information. We may need to store objects of types our persistence systems don't know how to deal with, and need to serialize and reconstitute them.

Thus we have Models. Models enforce our business logic for us, they handle our persistence layer for us, and they add domain-specific logic to our data for us. What's not to love?

"Objects are not Abstract Data Types" -- Barbara Liskov

If we're not careful our models can become our data in our minds, and conflating the model object and the data it is meant to represent is a slippery slope. Before long we use our relational or document database to persist our objects instead of our data -- if it weren't for incompatibilities between ruby versions, we might as well just Marshal.dump our objects into a blob field and be done with it.

Heeding the "Thin Controller / Fat Model" mantra made popular in the Rails community can lead to some downright monstrous model classes that do everything we might conceivably need with the model's data. While thin Controllers are definitely a good idea, making fat Models instead only shifts the problems around. If we're not careful we end up doing the OO equivalent of telling a box to ship itself to Germany -- something that a box has no business knowing how to do. With our love for monkeypatching, the Ruby community in particular is especially prone to this kind of abstraction leakage.

"There should never be more than one reason for a class to change" -- Robert Martin

The complexity this approach to Model design encourages also makes code difficult to maintain. If a model for an invoice knows how to calculate its amounts based on its data, figure out how to ship itself to various locations, or format its data for display or printing, then it has multiple responsibilities and has become fragile.

Triplicate aims to solve the complexity problem by providing a minimal layer of presentation and processing logic over your documents. The presentation layer is concerned with representing your data in various ways, and the processing layer makes sure it conforms to various rules and handing it off to various Observers.


  • rubydoctest if you wish to run the tests embedded in the documentation

TODO / Sketchpad

  • setters, direct values & procs
  • references
    • caching entire document or list of fields
    • way to read references
    • referencedBy
  • read-only fields
  • free-write access
  • overflow fields
  • filters
    • proxy documents? or too edge case
  • process blocks to be called on #process!
  • "prevent blind updates"
  • display fields (perhaps read-only, with defaults as procs?)
  • state machine

display :thing, ->{ "#{something}: #{else}" }

field :type, default: 'User', protect: true protected_field: hidden_field :type, 'User'

field :another, protect: true field :created_at, set: ->(val){ val || }, protect: true field :updated_at, set: ->{ }, protect: true

field :updated_at do coerce Time protect on_process { } end

field :cost_adjustments do protect collection coerce :cost_adjustment do field :label field :amount do validate match: /^(\d+)%?$/ end display :percent, ->{ amount =~ /%/ } display :total, ->{ percent ? parent.pretotal * amount/100.0 : amount } end end

field :amount, :match => /^(\d+)%?$/ display :percent, ->{ amount.match(/%$/) } display :total, ->{ percent ? parent.pretotal * amount/100.0 : amount }

overflow :etcetera