# Hobo Lifecycles

In the REST style, which is popular with Rails coders, we view our objects a bit like documents: you can post them to a website, get them again later, make changes to them and delete them. Of course, these objects also have behaviour, which we typically implement by hooking functionality to the create / update / delete events (e.g. using callbacks such as `after_create` in ActiveRecord).

This works great for many situations, but some objects are *not* best thought of as documents that we create and edit. In particular, web apps often contain objects that model some kind of *process*. A good example is *friendship* in a social app. Here's a description of how friendship might work:

 * Any user can **request** friendship with another user
 * The other user can **accept** or **reject** (or perhaps **ignore**) the request.
 * The friendship is only **active** once it's been accepted
 * An active friendship can be **cancelled** by either user.

Not a create, update or delete in sight. Those bold words capture the way we think about the friendship much better. Of course we *could* implement friendship in a RESTful style, but we'd be doing just that -- *implementing* it, not *declaring* it. The life-cycle of the friendship would be hidden in our code, scattered across a bunch of callbacks, permission methods and state variables. Experience has shown this type of code to be tedious to write, *extremely* error prone and fragile when changing.

Hobo lifecycles is a mechanism for declaring the life-cycle of a model in a natural manner. It's a bit like `acts_as_state_machine`, but Hobo-style :-)

First the junk to get us started:

    doctest_require: 'rubygems'
    doctest_require: 'active_record'
    >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobo/lib'
    >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobo/lib'
    >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobofields/lib'
    >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobosupport/lib'
    >> require 'hobo'
    >> require 'hobo/model'
    >> ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
                                               :database => "lifecycle_doctest")


A user model for our example:

       >>
        class User < ActiveRecord::Base
          hobo_model
          fields do
            name :string
          end
        end

Now the friendship. For now we'll just declare the *invite* part of the lifecycle. We first declare the *states* -- there's only one for now. We then declare the *invite* action. This is the action that first creates the friendship, so we declare it with `create`:

    >>
     class Friendship < ActiveRecord::Base
       hobo_model
       belongs_to :requester, :class_name => "User"
       belongs_to :requestee, :class_name => "User"

       lifecycle do
         state :requested
         create :request, :params => [ :requestee ], :become => :requested, :user_becomes => :requester
       end
     end

Now let's get the DB ready:

        doctest_require: '../../../hobofields/lib/hobo_fields/migration_generator'
        >> up, down = HoboFields::MigrationGenerator.run
        >> ActiveRecord::Migration.class_eval up
        >> User.delete_all
        >> Friendship.delete_all

We need some users to be friends:

        >> tom = User.create(:name => "Tom")
        >> bob = User.create(:name => "Bob")

Tom is allowed to request friendship:

        >> Friendship::Lifecycle.can_request?(tom)
        => true

Tom does so:

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> f.requester.name
        => "Tom"
        >> f.requestee.name
        => "Bob"
        >> f.state
        => "requested"

To continue modeling the friendship lifecycle, we add some *transitions*:

        >>
         class Friendship
            lifecycle do
              state :active
              transition :accept, { :requested => :active}, :available_to => :requestee 
              transition :reject, { :requested => :destroy}, :available_to => :requestee 
            end
         end

Note:

 * Part of the transition declaration is *who* that transition is for. These two were only for the `requestee`:

 * `:destroy` is a special pseudo state: entering this state causes the record to be destroyed

We can test which transitions are available:

        >> f.lifecycle.available_transitions_for(tom).*.name
        => []
        >> f.lifecycle.available_transitions_for(bob).*.name
        => ["accept", "reject"]
        >> f.lifecycle.can_accept?(tom)
        => false
        >> f.lifecycle.can_reject?(tom)
        => false
        >> f.lifecycle.can_accept?(bob)
        => true
        >> f.lifecycle.can_reject?(bob)
        => true

Accept the friendship

        >> f.lifecycle.accept(bob)
        >> f.state
        => "active"

And now there's nowhere to go:

        >> f.lifecycle.available_transitions_for(tom).*.name
        => []
        >> f.lifecycle.available_transitions_for(bob).*.name
        => []

Cleanup

        >> Friendship.delete_all

Let's try a rejected friendship:

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> f.state
        => "requested"
        >> f.lifecycle.can_reject?(bob)
        => true
        >> Friendship.count
        => 1
        >> f.lifecycle.reject(bob)
        >> Friendship.count
        => 0

Cleanup

        >> User.delete_all
        >> Friendship.delete_all
        >> Friendship::Lifecycle.reset

## A bigger example

We'll run through the same example again, but we'll add some features

Transitions and states can have actions associated with them. A common use might be to send an email. We'll simulate that with a global variable `$emails`

        >> $emails = []

We'll extend the lifecycle to allow:

 * the requester to back out of the request

 * the requestee to ignore the request

 * either party to cancel the active friendship

Here is the entire lifecycle

        >>
    class Friendship < ActiveRecord::Base
      hobo_model
      belongs_to :requester, :class_name => "User"
      belongs_to :requestee, :class_name => "User"

      lifecycle do
            state :requested, :active, :ignored

        create :requester, :request, :params => [ :requestee ], :become => :requested do
          $emails << "Dear #{requestee.name}, #{requester.name} wants to be friends with you"
        end


                transition :requestee, :accept, { :requested => :active } do
          $emails << "Dear #{requester.name}, #{requestee.name} is now your friend :-)"
        end

                transition :requestee, :reject, { :requested => :destroy } do
          $emails << "Dear #{requester.name}, #{requestee.name} blew you out :-("
        end

                transition :requestee, :ignore, { :requested => :ignored }

                transition :requester, :retract, { :requested => :destroy } do
          $emails << "Dear #{requestee.name}, #{requester.name} reconsidered"
        end

                transition [ :requester, :requestee ], :cancel, { :active => :destroy }
                # TODO: send the email - for this we need the acting user to be passed to the block

      end

    end

Check the simple accept still works, and sends emails

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> $emails.last
        => "Dear Bob, Tom wants to be friends with you"
        >> f.lifecycle.accept(bob)
        >> $emails.last
        => "Dear Tom, Bob is now your friend :-)"
        >> f.lifecycle.active?
        => true

Rejection:

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> f.lifecycle.reject(bob)
        >> $emails.last
        => "Dear Tom, Bob blew you out :-("
        >> f.state
        => "destroy"

Retraction:

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> f.lifecycle.can_retract?(bob)
        => false
        >> f.lifecycle.retract(tom)
        >> $emails.last
        => "Dear Bob, Tom reconsidered"
        >> f.lifecycle.active?

Ignoring

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> $emails = []
        >> f.lifecycle.ignore(bob)
        >> $emails # Ignoring shouldn't send any email
        => []
        >> f.state
        => "ignored"

Requester cancels

        >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
        >> f.lifecycle.can_cancel?(tom)
        => false
        >> f.lifecycle.accept(bob)
        >> f.lifecycle.cancel(tom)
        >> f.state
        => "destroy"