public
Created — forked from jbarnette/gist:1934258

  • Download Gist
stateful.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
require "overlord/searchable"
 
module Overlord
module Stateful
ANY = Object.new
 
def self.included(model)
model.validates :state, presence: true
model.extend Macro
 
model.stateful do
state :active
state :inactive
state :deleted
 
on :activate do
move :inactive => :active
end
 
on :deactivate do
move :active => :inactive
end
 
on :delete do
move any => :deleted
end
end
 
if model < Overlord::Searchable
Sunspot.setup model do
string :state, stored: true
end
end
end
 
module Macro
def machine
@machine ||= Machine.new self
end
 
def stateful(&block)
machine.instance_eval(&block)
end
end
 
class Context < Struct.new(:instance, :event, :src, :dest, :args)
def trigger(hooks)
hooks.values_at(dest, ANY).compact.flatten.each do |hook|
instance.instance_exec self, &hook
end
end
end
 
class Event
def initialize(model, name)
@moves = {}
 
model.send :define_method, "#{name}!" do |*args|
model.machine.fire self, name, *args
end
end
 
def any
ANY
end
 
def dest current
@moves[current.to_sym] || @moves[any]
end
 
def move pair
Array(pair.keys.first).each do |s|
@moves[s] = pair.values.first
end
end
end
 
class Machine
def initialize model
@model = model
@entered = Hash.new { |h, k| h[k] = [] }
@entering = Hash.new { |h, k| h[k] = [] }
@events = Hash.new { |h, k| h[k] = Event.new @model, k }
@persisted = Hash.new { |h, k| h[k] = [] }
end
 
def entered(*names, &block)
munge(names).each { |n| @entered[n] << block }
end
 
def entering(*names, &block)
munge(names).each { |n| @entering[n] << block }
end
 
def fire(instance, name, *args)
raise "No [#{name}] event." unless @events.include? name
 
src = instance.state
dest = @events[name].dest src
 
unless dest
raise "Can't [#{name}] while [#{instance.state}]: #{instance}"
end
 
ctx = Context.new instance, name, src.to_sym, dest, args
 
ActiveRecord::Base.transaction do
ctx.trigger @entering
 
instance.state = ctx.dest.to_s
ctx.trigger @entered
 
instance.save!
end
 
ctx.trigger @persisted
 
instance
end
 
def on(*names, &block)
names.flatten.each { |n| @events[n].instance_eval(&block) }
end
 
def persisted(*names, &block)
munge(names).each { |n| @persisted[n] << block}
end
 
def state(name)
name = name.to_s
 
unless @model.respond_to? name
@model.scope name, @model.where(state: name)
@model.send(:define_method, "#{name}?") { name == state }
end
end
 
private
 
def munge(names)
names.flatten!
if names.empty? then [ANY] else names end
end
end
end
end

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.