Created
February 28, 2012 18:45
-
-
Save jbarnette/1934258 to your computer and use it in GitHub Desktop.
Revisions
-
jbarnette created this gist
Feb 28, 2012 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,154 @@ require "overlord/searchable" module Overlord # Mark an ActiveRecord model as having a state machine. Requires a # string attribute called `state`. To set the default state for a # class, set a default column value for `state` in the database. # # Use Symbols for all keys and values in state definitions. 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! names.empty? ? [ANY] : names end end end end