hanswurst: Be a couch potato and play different roles
hanswurst is discontinued in favour of https://github.com/metakeule/thingtank using CouchRest Model and a slightly different concept
Hanswurst is a library that uses couch potato to create arbitrary objects that may have different roles. The roles determine the properties and they can be mixed and matched at will.
Installation
hanswurst is only tested with ruby 1.9.2 and above.
Install it as a gem:
sudo gem install hanswurst
or in rvm:
gem install hanswurst
Examples:
To simplify the class building we let them all inherit from a role class
class Role
include CouchPotato::Persistence # add the couch potato features
include Hanswurst::Shares # allows to share roles
include Hanswurst::Delegates # delegate methods to methods of a property that is a role
include Hanswurst::Doc # access the doc from inside the role
# simplify saving
def save
CouchPotato.database.save_document _doc
end
end
Imagine a Caesar is born
class Birth < Role
property :date
property :place
validates_presence_of :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 < Birth
end
class Person < Role
property :name
property :gender
shares :birth => Birth # requires the shared property :birth which is a Birth
shares :dead => Dead # requires the shared property :dead which is a Dead
end
julius = Hanswurst.new
julius.person = Person.new # the shared property :birth is propagated
julius.person.gender = "m"
julius.person.name = 'Gaius Iulius Caesar'
# we could directly setup the properties and don't have to do 'julius.birth = Birth.new' first
julius.birth.date = "100 BC"
julius.birth.place = "Rome"
julius.person.save # doen't matter on with role you call save, see Role#save above
julius_id = julius._id
later...
julius = CouchPotato.database.load_document julius_id
julius.person.name # => 'Gaius Iulius Caesar'
julius.birth.place # => "Rome"
when he is adult, he wants to marry. now things are getting a bit more complicated:
# he needs a marriage and a women
class Marriage < Role
property :date # the date of the marriage
property :end # when the marriage ended
property :spouse # doc_id of the spouse
property :state # current state of the marriage, e.g. 'married', 'widowed', 'divorced'
# ensure that 'spouse' is a fkey (doc_id) of a Person and that we have only one of them
validates :spouse, :hanswurst => {:class => Person, :fkey => true, :max => 1}
end
# and we don't want to load the spouses document manually just to get her name. so here is a role that does it for us
class Spouse < Role
shares :marriage => Marriage # depends on the shared marriage role
# convenience method to get name of the current spouse by doc.spouse.name
def name
CouchPotato.database.load_document(id).person.name if (id=marriage_spouse)
end
def marriage_spouse
m = marriage()
return m.spouse if m and m.status == 'married'
return nil
end
def marriage()
_doc.marriage # here we access the shared marriage role via the _doc (Hanswurst document)
end
end
now we could easily get julius married
conny = Hanswurst.new
conny.person = Person.new :gender => "f", :name => 'Cornelia'
conny.save
julius.marriage = Marriage.new
julius.marriage.date = "84 BC"
julius.marriage.spouse = conny._id
julius.marriage.state = 'married'
julius.spouse.name # => 'Cornelia'
while that is nice, let see if we could make it more comfortable:
class Marriage
shares :spouse => Spouse # we need a spouse as role of the doc
# marry a person that is or is not already saved or part of a Hanswurst
def marry(person)
# attach it to a hanswurst, if it is not already
person = Hanswurst.new(:person => person).person unless person.respond_to? :_doc # if it responds to :_doc it already has a hanswurst doc attached to it
# save the person if it is not already
person.save unless person._doc._id
# assign the doc_id to spouse
self.spouse = person._doc._id
self.state = 'married'
# now look if she is already married to him and make her marry him
unless self._doc && self._doc._id && person._doc.marriage_spouse == self._doc._id
person._doc.marriage = Marriage.new :date => self.date
person._doc.marriage.marry self
person.save
end
end
end
it now becomes much less work and Cornelia also knows that she is married to Julius
conny = Person.new(:gender => "f", :name => 'Cornelia')
julius.marriage = Marriage.new :date => "84 BC"
julius.marriage.marry conny
julius.spouse.name # => 'Cornelia'
conny.spouse.name # => 'Gaius Iulius Caesar'
julius could even marry a second time, i.e. marriage becomes an Array of Marriage objects
marriage = Marriage.new :date => "68-65 BC"
marriage.marry Person.new(:gender => "f", :name => 'Pompeia')
julius.marriage << marriage
# ouch!
julius.spouse.name # => Error
we should let Spouse know that Marriage might be an array, so simply overwrite marriage with
class Spouse
def marriage()
[_doc.marriage].flatten.last
end
end
now it works:
julius.spouse.name # => 'Pompeia'
julius.marriage.size # => 2
julius.marriage.first.spouse.name # => 'Cornelia'
# oops!
julius.marriage.first.status # => 'married'
julius is still married with Cornelia but he should not
if Cornelia died before his second marriage, it should'nt be a problem:
class Dead
# all callbacks of roles are called and defined like corresponding callbacks of the doc
before_save do
if _doc.spouse && marriage=_doc.spouse.marriage
marriage.end = self.date # marriage ends with date of dead
marriage.state = 'widowed'
end
end
end
class Person
def die(date)
_doc.dead = Dead.new :date => date
end
end
# ok first replay the marriage of conny and julius to prevent errors
conny.marriage = nil
conny.save
julius.marriage = Marriage.new :date => "84 BC"
julius.marriage.marry conny
# now conny dies
conny.die "68-65 BC"
# and julius may marry Pompeia
marriage = Marriage.new :date => "68-65 BC"
marriage.marry Person.new(:gender => "f", :name => 'Pompeia')
julius.marriage << marriage
julius.marriage.first.status # => 'widowed'
julius.marriage.first.spouse.name # => 'Cornelia'
julius.marriage.last.status # => 'married'
julius.marriage.last.spouse.name # => 'Pompeia'
julius.spouse.name # => 'Pompeia'
since julius is immortal, no one should be able to destroy him:
class Undestroyable < Role
before_save do
false # never allow to destroy
end
end
julius.immortal = Undestroyable.new
CouchPotato.database.destroy_document julius
CouchPotato.database.load_document julius._id # => julius is still there
All views are attached to the hanswurst design document You may create general views:
Hanswurst.view :all, :key => :created_at
or views specific for a role
Hanswurst.view_for :immortal, :all, :key => :created_at
# execute them this way
CouchPotato.database.view Hanswurst.immortal_all
the same works with lists.
You may subclass Hanswurst to do further separation and mix the native properties of Hanswursts / its subclasses with the roles properties.
Contributing to hanswurst
- 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 (c) 2012 Marc Rene Arns. See LICENSE.txt for further details.