couchrest docs with multiple characters
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.

thingtank: couchrest docs with multiple characters

Build Status

ThingTank is a library that uses couchrest and couchrest model to create arbitrary objects that may have multiple characters. The characters determine the properties and they can be mixed and matched at will.


thingtank is tested with ruby 1.8.7, 1.9.2 and above.

Install it as a gem:

sudo gem install thingtank

or in rvm:

gem install thingtank


Imagine a Caesar is born

class Born < ThingTank::Character
  property :birth_date, :alias => :born_at
  property :birth_place
  validates_presence_of :birth_date # make sure a date is given

just in case he might die....we might want to have a date and maybe even a place

class Dead < ThingTank::Character
  property :date_of_death
  property :place_of_death
  validates_presence_of :date_of_death

and then he needs a name

class Person < ThingTank::Character
  property :name
  property :gender
  validates_presence_of :name

now we can create julius

julius = ThingTank.create :gender => :m, :name => 'Gaius Iulius Caesar'
julius["birth_date"] = "100 BC"
julius["birth_place"] = "Rome"

julius.could_be? Person # => true Person # => false Person Person # => true["gender"] # => nil (gender is not a property of born)["birth_date"] # => "100BC"

id =


julius = ThingTank.get id
julius["birth_date"] # => "100BC" Person # => true

when he is adult, he wants to marry. now things are getting a bit more complicated:

# he needs a marriage and a women
class Married < ThingTank::Character
  property :date              # the date of the marriage
  property :end               # when the marriage ended
  property :spouse            # doc_id of the spouse
  property :state             # state of the marriage
  validates_presence_of :date
  validates_presence_of :spouse
  validate :spouse_should_be_a_person
  # ensure that 'spouse' is a doc_id of a Person
  def spouse_should_be_a_person
    Person.get(self["spouse"]).valid? # loads doc as character Person and validates, same as ThingTank.get(self["spouse"]).as(Person).valid?

we want easy access to the name of the spouse

class Spouse < ThingTank::Character
  property :married    
  property :married_state

  validate :spouse_should_be_married
  validates :married, :character => Married # doc must have "married" character

  def married
    self["married"] # contains a Married character

  def spouse_should_be_married
    married["spouse"] == self["_id"]

  def name
    self["married_state"] == "married" ?
      Person.get(married["spouse"]).name :

  def ex
    self["married_state"] == "divorced" ?
      Person.get(married["spouse"]).name :

now we could easily get julius married

conny = ThingTank.create :gender => "f", :name => 'Cornelia', :characters => ['Person']
julius["married"] = {"date" => "84 BC", "spouse" =>}
julius["married_state"] = "married"
julius.with("married").is(Married).valid? # => true  # "married" is a property that has the Married character
julius.has(Spouse).valid? # #has is an alias of #is
julius.has(Spouse).name # => 'Cornelia'

while that is nice, let see if we could make it more comfortable:

class Married
  # marry a doc or hash
  def marry(person)
    person = if person.is_a?(Hash) # should have a doc_id

    # assign the doc_id to spouse
    self["spouse"] = person["_id"]
    self["state"] = 'married'
    _doc["married_state"] = 'married'

    unless person["married"] && person.last_character(Married, "married").spouse == _doc["_id"]
      person.add_character(Married, "married") do |m| = self["date"]
        m.marry _doc

  def divorce(date)
    self["state"] = 'divorced'
    self['end'] = date
    spouse = Spouse.get(self["spouse"])
    if spouse.married_state == "married"


class Spouse
  def married(&code)
    _doc.last_character Married, "married", &code

  def divorce(date)
    self["married_state"] = 'divorced'
    married { |m| m.divorce(date) }

class Person
  def marry(date, person)
    _doc.add_character Married, "married",  do |m| = "84 BC"
      m.marry person

it now becomes much less work and Cornelia also knows that she is married to Julius "84 BC", :gender => "f", :name => 'Cornelia'
julius.has(Spouse).name # => 'Cornelia'
julius.has(Spouse).married_state # => 'married'
conny_id = julius.last_character(Married,"married").spouse
conny = ThingTank.get conny_id
conny.has(Spouse).married_state # =>   'married'

julius could even marry a second time, i.e. marriage becomes an Array of Marriage objects "68-65 BC", :gender => "f", :name => 'Pompeia'
Person.get(julius["married"].first["spouse"]).name # => 'Cornelia'
Person.get(julius["married"].last["spouse"]).name #  =>   'Pompeia'
julius.has(Spouse).name # =>  'Pompeia'
julius["married"].first["state"] # =>  'married'
julius["married"].last["state"] # => 'married'
julius["married"].size # => 2

# ouch, two women!

julius is still married with Cornelia but he should not

if Cornelia died before his second marriage, it would not be a problem:

class Dead
  # all callbacks of characters are called and defined like corresponding callbacks of the doc
  before_save do
    if && _doc['married_state'] == 'married'
      Spouse.get(_doc.last_character(Married, 'married').spouse).widowed(self["date_of_death"])

class Person
  def dies(date) do |d|
      d.date_of_death = date

class Married
  def widow(date)
    self["state"] = 'widowed'
    self['end'] = date

class Spouse
  def widowed(date)
    self["married_state"] = 'widowed'
    married { |m| m.widow(date) }
end "84 BC", :gender => "f", :name => 'Cornelia'
conny = ThingTank.get julius["married"]["spouse"]
conny.reload "68-65 BC"
julius.reload "68-65 BC", :gender => "f", :name => 'Pompeia'
julius["married"].size # => 2
Person.get(julius["married"].first["spouse"]).name # => 'Cornelia'
julius["married"].first["state"]   # => 'widowed'

since julius is immortal, no one should be able to destroy him:

class Undestroyable < ThingTank::Character
  before_destroy do
    false # never allow to destroy
end # save the character

id =
julius = ThingTank.get id
ThingTank.get(id).nil? # => julius is still there


ThingTank.get(id).nil? # => julius is still there

You may subclass ThingTank to do further separation and mix the native properties of ThingTanks / its subclasses with the characters properties.


The main idea is that every couch document could have many characters at the same time. The implementation is that the document is an object of ThingTank or a subclass of it. ThingTank is compatible to couchrest_model with some helper methods. Most of the time you don't want to define properties for the ThingTank class or a subclass. You may store any key => value freely within the document by using the [] and []= methods.

The concept is inspired by the way the go language defines interfaces. With ThingTank you may thing of the main doc as a Hash. Then the characters are "interfaces", that define which properties a doc would need to fullfill the character ("to have the character"). But that alone is not suffient. You also need to tell the doc that it will have the character from now on. This information will be stored in the "characters" property of the doc. You might remove this statement with or without affecting the properties the character cares about. But only if the doc should have the character you might interact with the subseet of his properties that the character cares about via the character. A doc might combine different characters at the same time. There might be even different characters that care about the same properties. If so you should take care that there are no conflicting actions taking place at the same update.

The characters are all subclasses of the ThingTank::Character class and are compatible with couchrest_model as well but they won't interact with the database directly but only via their document (instance of ThingTank). The characters is where all validation, callbacks, properties and additional methods should go into. They do all the hard work, but they aren't loaded automatically but only if you call them via the ThingTank#as method. Then the properties that they care about are copied from the doc to the character instance. When you interact with the character object its properties go out of sync with the original doc so here are some hints how to handle this dirty state and how to get them back to the doc.

Characters may also interact via the doc, but care has to be taken in order to save the changes back to doc properly and to inform the affected character about the changes.

  • Use Character#to_doc to return the changed date from the character to the doc without saving the doc.
  • Use Character#reload to load the (possibly changed) data from the doc to the character object.
  • Use Character#reload! to load the (possibly changed) data from the database. The doc with be reloaded from the database and then fill the character object.
  • Use Character#save or ThingTank#save to save the doc with all changes
  • Use ThingTank#add_character to add a Character and pass it a code block, so that all the changes go back to the doc at the end of the code block
  • Use ThingTank#as to work with a certain character of the doc and pass it a code block, so that all the changes go back to the doc at the end of the code block
  • Use ThingTank#is if the properties for the character are already in the doc and you just want it to let it have the character
  • Use ThingTank#get to get the doc out of the database (and then use ThingTank#as to handle it as a character)
  • Views are stored and called via the ThingTank class
  • Pass ThingTank#add_character as second parameter the name of the property if you want the doc to have a property that has a certain character
  • Use ThingTank#with to use the character of a certain property
  • Use the _doc method within a character method to access the doc and for the id
  • Look at the examples and tests

Contributing to thingtank

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
  • Fork the project
  • Start a feature/bugfix branch
  • Commit and push until you are happy with your contribution
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.


Copyright (c) 2012 Marc Rene Arns. See LICENSE.txt for further details.