#392 A Tour of State Machines pro
Here I show how three popular state machine gems can be used to clean up a list of boolean/datetime fields. I also show a custom solution which keeps track of the history of events.
- Download:
- source codeProject Files in Zip (127 KB)
- mp4Full Size H.264 Video (31.5 MB)
- m4vSmaller H.264 Video (16 MB)
- webmFull Size VP8 Video (19 MB)
- ogvFull Size Theora Video (36.3 MB)
Resources
- State Machines on Ruby Toolbox
- Finite-state machine on Wikipedia
- state_machine
- aasm
- workflow
- transitions
- state_machine-audit_trail
state_machine
Gemfile
gem 'state_machine'
migrations/create_order.rb
create_table :orders do |t| t.string :state end
models/order.rb
class Order < ActiveRecord::Base scope :open_orders, -> { with_state(:open) } attr_accessor :invalid_payment state_machine initial: :incomplete do event :purchase do transition :incomplete => :open end event :cancel do transition :open => :canceled end event :resume do transition :canceled => :open end event :ship do transition :open => :shipped end before_transition :incomplete => :open do |order| # process payment ... !order.invalid_payment end end end
rails c
o = Order.create o.state o.incomplete? o.can_cancel? o.can_purchase? o.purchase o.state_events
AASM
Gemfile
gem 'aasm'
migrations/create_order.rb
create_table :orders do |t| t.string :aasm_state end
models/order.rb
class Order < ActiveRecord::Base include AASM scope :open_orders, -> { where(aasm_state: "open") } attr_accessor :invalid_payment aasm do state :incomplete, initial: true state :open state :canceled state :shipped event :purchase, before: :process_purchase do transitions from: :incomplete, to: :open, guard: :valid_payment? end event :cancel do transitions from: :open, to: :canceled end event :resume do transitions from: :canceled, to: :open end event :ship do transitions from: :open, to: :shipped end end def process_purchase # process payment ... end def valid_payment? !invalid_payment end end
Workflow
Gemfile
gem 'workflow'
migrations/create_order.rb
create_table :orders do |t| t.string :workflow_state end
models/order.rb
class Order < ActiveRecord::Base include Workflow scope :open_orders, -> { where(workflow_state: "open") } workflow do state :incomplete do event :purchase, transition_to: :open end state :open do event :cancel, transition_to: :canceled event :ship, transition_to: :shipped end state :canceled do event :resume, transition_to: :open end state :shipped end def purchase(valid_payment = true) # process payment ... halt unless valid_payment end end
From Scratch
migrations/create_order_events.rb
create_table :order_events do |t| t.belongs_to :order t.string :state t.timestamps end
models/order.rb
class Order < ActiveRecord::Base has_many :events, class_name: "OrderEvent" STATES = %w[incomplete open canceled shipped] delegate :incomplete?, :open?, :canceled?, :shipped?, to: :current_state def self.open_orders joins(:events).merge OrderEvent.with_last_state("open") end def current_state (events.last.try(:state) || STATES.first).inquiry end def purchase(valid_payment = true) if incomplete? # process purchase ... events.create! state: "open" if valid_payment end end def cancel events.create! state: "canceled" if open? end def resume events.create! state: "open" if canceled? end def ship events.create! state: "shipped" if open? end end
models/order_event.rb
class OrderEvent < ActiveRecord::Base belongs_to :order attr_accessible :state validates_presence_of :order_id validates_inclusion_of :state, in: Order::STATES def self.with_last_state(state) order("id desc").group("order_id").having(state: state) end end