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'

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 )

Facebook photo

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

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.