thingtank: couchrest docs with multiple characters¶ ↑
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.
Installation¶ ↑
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
Examples:¶ ↑
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 end
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 end
and then he needs a name
class Person < ThingTank::Character property :name property :gender validates_presence_of :name end
now we can create julius
julius = ThingTank.create :gender => :m, :name => 'Gaius Iulius Caesar' julius["birth_date"] = "100 BC" julius["birth_place"] = "Rome" julius.is(Born) julius.could_be? Person # => true julius.is? Person # => false julius.is Person julius.is? Person # => true julius.is(Born)["gender"] # => nil (gender is not a property of born) julius.is(Born)["birth_date"] # => "100BC" id = julius.id
later…
julius = ThingTank.get id julius["birth_date"] # => "100BC" julius.is? 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? end end
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 end def spouse_should_be_married married["spouse"] == self["_id"] end def name self["married_state"] == "married" ? Person.get(married["spouse"]).name : nil end def ex self["married_state"] == "divorced" ? Person.get(married["spouse"]).name : nil end end
now we could easily get julius married
conny = ThingTank.create :gender => "f", :name => 'Cornelia', :characters => ['Person'] julius["married"] = {"date" => "84 BC", "spouse" => conny.id} 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.save julius.reload 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 = ThingTank.new(person) if person.is_a?(Hash) person.save # 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| m.date = self["date"] m.marry _doc end person.is(Spouse) person.save end end def divorce(date) self["state"] = 'divorced' self['end'] = date _doc.save spouse = Spouse.get(self["spouse"]) if spouse.married_state == "married" spouse.divorce(date) end end end class Spouse def married(&code) _doc.last_character Married, "married", &code end def divorce(date) self["married_state"] = 'divorced' married { |m| m.divorce(date) } end end class Person def marry(date, person) _doc.add_character Married, "married", do |m| m.date = "84 BC" m.marry person end end end
it now becomes much less work and Cornelia also knows that she is married to Julius
julius.as(Person).marry "84 BC", :gender => "f", :name => 'Cornelia' julius.save julius.reload 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
julius.as(Person).marry "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.is?(Spouse) && _doc['married_state'] == 'married' Spouse.get(_doc.last_character(Married, 'married').spouse).widowed(self["date_of_death"]) end true end end class Person def dies(date) _doc.is(Dead) do |d| d.date_of_death = date end end end class Married def widow(date) self["state"] = 'widowed' self['end'] = date _doc.save end end class Spouse def widowed(date) self["married_state"] = 'widowed' married { |m| m.widow(date) } end end julius.as(Person).marry "84 BC", :gender => "f", :name => 'Cornelia' julius.save julius.reload conny = ThingTank.get julius["married"]["spouse"] conny.save conny.reload conny.as(Person).dies "68-65 BC" conny.save julius.reload julius.as(Person).marry "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 end julius.is(Undestroyable) julius.save # save the character id = julius.id julius = ThingTank.get id julius.as(Undestroyable).destroy ThingTank.get(id).nil? # => julius is still there julius.destroy 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.
Hints:¶ ↑
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 doc.id 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¶ ↑
Copyright © 2012 Marc Rene Arns. See LICENSE.txt for further details.