Create a gist now

Instantly share code, notes, and snippets.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment