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'