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 (c) 2012 Marc Rene Arns. See LICENSE.txt for further details.