Tussle of the State Machines

There’s no shortage of Ruby state machine libraries (assm, state_machine, etc.). However when we needed to implement dynamic state machine we didn’t find one.

The Problem

We needed a polymorphic class that could have different state machines triggered in it depending on some condition. Basically, here is what we wanted to achieve:

class Call
 include Mongoid::Document

 field :scheduled_at, type: DateTime 
 field :is_existing_customer, type: Boolean
 field :note

 belongs_to :callable, polymorphic: true

 case callable_type
  when 'Car'
   state_machine :state, :initial => :fresh, namespace: 'car' do
    event :schedule do
     transition [:fresh, :schedule] => :scheduled
    end
    # ...
    # ...
  end

  when 'Personal'
   state_machine :state, :initial => :fresh, namespace: 'Personal' do
   event :schedule do
    transition :fresh => :scheduled
   end
    #...
    #...
  end

  when 'any other'
   #...
  end
 end

Now the problem was that AASM State Machine does not support multiple state machine in a single class. So we tried to achive it through state_machine gem with namespaces. However, we could not have same state field even under the namespaced state machine in a single class.

The solution!

We wanted a state machine that could be easily integrated with other Ruby objects. So we decided to define a state machine as a separate class and selectively apply it to our Rails models. We were using MongoDB, so we embedded these objects.

class CarStateMachine
 include Mongoid::Document
 include AASM

 field :state
 embedded_in :call

 # no need for name space and we can use AASM directly
 state_machine :state, :initial => :fresh do
  #states: fresh, scheduled, lead, succeed
  event :schedule do
   transition [:fresh, :schedule] => :scheduled
  end
  #...
  #...
  end
 end
class PersonalStateMachine
 include Mongoid::Document
 include AASM

 field :state
 embedded_in :call

 #states: hello, meet, bye
 state_machine :state, :initial => :hello do
  event :wow do
   transition :hello => :meet
  end
  #...
  #...
 end
end

Now, the model can access these embedded objects using a call_state method, that returns the embedded object based on callable_type of model!

class Call
 include Mongoid::Document

 field :scheduled_at, type: DateTime
 field :is_existing_customer, type: Boolean
 field :note
 field :callable_type
 
 embeds_one :car_state_machine
 embeds_one :personal_state_machine
 
 # Method to access state machine
 def call_state
  case self.callable_type
   when 'Car'
    self.car_state_machine || self.build_car_state_machine
   when 'Personal'
    self.personal_state_machine || self.build_personal_state_machine
   end
  end
 end

Here is a sample output of a Call model using different state machines dynamically!

call = Call.first.callable_type # => "Car"
call.call_state.state # => 'fresh'
call.call_state.schedule! 
call.call_state.state # => 'scheduled'

#####

call = Call.last.callable_type # => "Personal"
call.call_state.state # => 'hello'
call.call_state.wow!
call.call_state.state # => 'meet'
Advertisements

About Pankaj Kumar Rawat

A Rubyst from Pune
This entry was posted in Ruby and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s