Skip to content

Instantly share code, notes, and snippets.

@jbarnette
Created February 28, 2012 18:45
Show Gist options
  • Select an option

  • Save jbarnette/1934258 to your computer and use it in GitHub Desktop.

Select an option

Save jbarnette/1934258 to your computer and use it in GitHub Desktop.

Revisions

  1. jbarnette created this gist Feb 28, 2012.
    154 changes: 154 additions & 0 deletions gistfile1.txt
    Original 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