|
require 'active_support/core_ext/class/attribute_accessors' |
|
require 'state_machine' |
|
|
|
module Checkout |
|
def self.included(klass) |
|
klass.class_eval do |
|
attr_accessor :state |
|
attr_accessor :transitions |
|
attr_accessor :previous_states |
|
|
|
cattr_accessor :checkout_flow |
|
|
|
def self.checkout_flow(&block) |
|
if block_given? |
|
@checkout_flow = block |
|
else |
|
@checkout_flow |
|
end |
|
end |
|
|
|
def initialize |
|
super |
|
machine |
|
end |
|
|
|
def transitions |
|
@transitions ||= [] |
|
end |
|
|
|
def add_transition(options) |
|
self.transitions << { options.delete(:from) => options.delete(:to) }.merge(options) |
|
end |
|
|
|
# TODO: Delegate |
|
def next! |
|
machine.next! |
|
end |
|
|
|
def machine |
|
@machine ||= begin |
|
order = self |
|
checkout_flow = order.class.checkout_flow |
|
self.state = :cart |
|
self.previous_states = [:cart] |
|
order.instance_eval(&checkout_flow) |
|
|
|
StateMachine.new(order, :initial => :cart) do |
|
order.transitions.each { |attrs| transition(attrs) } |
|
|
|
# Persist the state on the order |
|
after_transition do |
|
order.state = order.machine.state |
|
order.save |
|
end |
|
end |
|
end |
|
end |
|
|
|
|
|
def go_to_state(name, options={}) |
|
if options[:if] |
|
previous_states.each do |state| |
|
add_transition({:from => state, :to => name, :on => :next}.merge(options)) |
|
end |
|
self.previous_states << name |
|
else |
|
previous_states.each do |state| |
|
add_transition({:from => state, :to => name, :on => :next}.merge(options)) |
|
end |
|
self.previous_states = [name] |
|
end |
|
end |
|
|
|
def remove(options={}) |
|
if transition = find_transition(options) |
|
self.transitions.delete(transition) |
|
end |
|
end |
|
|
|
def find_transition(options={}) |
|
self.transitions.detect do |transition| |
|
transition[options[:from].to_sym] == options[:to].to_sym |
|
end |
|
end |
|
end |
|
end |
|
|
|
class StateMachine |
|
def self.new(object, *args, &block) |
|
machine = Class.new do |
|
def definition |
|
self.class.state_machine |
|
end |
|
end |
|
machine.state_machine(*args, &block) |
|
machine.new |
|
end |
|
end |
|
end |
|
|
|
class Order |
|
include Checkout |
|
|
|
def payment_required? |
|
false |
|
end |
|
|
|
def confirmation_required? |
|
false |
|
end |
|
|
|
def save |
|
#noop |
|
end |
|
|
|
end |
|
|
|
describe Order do |
|
let(:order) { Order.new } |
|
context "with default state machine" do |
|
before do |
|
Order.class_eval do |
|
checkout_flow do |
|
go_to_state :address |
|
go_to_state :delivery |
|
go_to_state :payment, :if => lambda { payment_required? } |
|
go_to_state :confirm, :if => lambda { confirmation_required? } |
|
go_to_state :complete |
|
remove :from => :delivery, :to => :confirm |
|
end |
|
end |
|
end |
|
|
|
it "has the following transitions" do |
|
transitions = [ |
|
{ :address => :delivery }, |
|
{ :delivery => :payment }, |
|
{ :payment => :confirm }, |
|
{ :confirm => :complete }, |
|
{ :payment => :complete }, |
|
{ :delivery => :complete } |
|
] |
|
transitions.each do |transition| |
|
transition = order.find_transition(:from => transition.keys.first, :to => transition.values.first) |
|
transition.should_not be_nil |
|
end |
|
end |
|
|
|
it "does not have a transition from delivery to confirm" do |
|
transition = order.find_transition(:from => :delivery, :to => :confirm) |
|
transition.should be_nil |
|
end |
|
|
|
it "starts out at cart" do |
|
order.state.should == :cart |
|
end |
|
|
|
it "transitions to address" do |
|
order.next! |
|
order.state.should == "address" |
|
end |
|
|
|
context "from address" do |
|
before do |
|
order.machine.state = 'address' |
|
end |
|
|
|
it "transitions to delivery" do |
|
order.next! |
|
order.state.should == "delivery" |
|
end |
|
end |
|
|
|
context "from delivery" do |
|
before do |
|
order.machine.state = 'delivery' |
|
end |
|
|
|
context "with payment required" do |
|
before do |
|
order.stub :payment_required? => true |
|
end |
|
|
|
it "transitions to payment" do |
|
order.next! |
|
order.state.should == 'payment' |
|
end |
|
end |
|
|
|
context "without payment required" do |
|
before do |
|
order.stub :payment_required? => false |
|
end |
|
|
|
it "transitions to complete" do |
|
order.next! |
|
order.state.should == "complete" |
|
end |
|
end |
|
end |
|
|
|
context "from payment" do |
|
before do |
|
order.machine.state = 'payment' |
|
end |
|
|
|
context "with confirmation required" do |
|
before do |
|
order.stub :confirmation_required? => true |
|
end |
|
|
|
it "transitions to confirm" do |
|
order.next! |
|
order.state.should == "confirm" |
|
end |
|
end |
|
|
|
context "without confirmation required" do |
|
before do |
|
order.stub :confirmation_required? => false |
|
end |
|
|
|
it "transitions to complete" do |
|
order.next! |
|
order.state.should == "complete" |
|
end |
|
end |
|
end |
|
end |
|
end |
Ryan - I'm happy with this now, great stuff.