Merge branch 'master' of https://github.com/stiff/aasm into merging-stiff-master

Conflicts:
	README.md
	lib/aasm.rb
	lib/aasm/event.rb
	lib/aasm/transition.rb
	spec/unit/transition_spec.rb
This commit is contained in:
Thorsten Böttger 2014-09-10 00:22:28 +02:00
commit 0edcc51756
9 changed files with 141 additions and 81 deletions

View File

@ -94,8 +94,12 @@ class Job
state :sleeping, :initial => true, :before_enter => :do_something
state :running
event :run, :after => Proc.new { |user| notify_somebody(user) } do
transitions :from => :sleeping, :to => :running, :on_transition => Proc.new {|obj, *args| obj.set_process(*args) }
event :run, :after => :notify_somebody do
transitions :from => :sleeping, :to => :running, :after => Proc.new {|*args| set_process(*args) } do
before do
log('Preparing to run')
end
end
end
event :sleep do
@ -141,8 +145,8 @@ begin
new_state enter
event guards
transition guards
transition on_transition
...update state...
transition after
event success # if persist successful
old_state after_exit
new_state after_enter
@ -183,37 +187,37 @@ running the transition. If the guard returns `false` the transition will be
denied (raising `AASM::InvalidTransition` or returning `false` itself):
```ruby
class Job
class Cleaner
include AASM
aasm do
state :sleeping, :initial => true
state :running
state :idle, :initial => true
state :cleaning
event :run do
transitions :from => :sleeping, :to => :running
end
event :clean do
transitions :from => :running, :to => :cleaning
transitions :from => :idle, :to => :cleaning, :guard => :cleaning_needed?
end
event :sleep do
transitions :from => :running, :to => :sleeping, :guard => :cleaning_needed?
event :clean_if_needed do
transitions :from => :idle, :to => :cleaning do
guard do
cleaning_needed?
end
end
transitions :from => :idle, :to => :idle
end
end
def cleaning_needed?
false
end
end
job = Job.new
job.run
job.may_sleep? # => false
job.sleep # => raises AASM::InvalidTransition
job = Cleaner.new
job.may_clean? # => false
job.clean # => raises AASM::InvalidTransition
job.may_clean_if_needed? # => true
job.clean_if_needed! # idle
```
You can even provide a number of guards, which all have to succeed to proceed

View File

@ -5,6 +5,7 @@ require 'ostruct'
errors
configuration
base
dsl_helper
instance_base
transition
event

30
lib/aasm/dsl_helper.rb Normal file
View File

@ -0,0 +1,30 @@
module DslHelper
class Proxy
attr_accessor :options
def initialize(options, valid_keys, source)
@valid_keys = valid_keys
@source = source
@options = options
end
def method_missing(name, *args, &block)
if @valid_keys.include?(name)
options[name] = Array(options[name])
options[name] << block if block
options[name] += Array(args)
else
@source.send name, *args, &block
end
end
end
def add_options_from_dsl(options, valid_keys, &block)
proxy = Proxy.new(options, valid_keys, self)
proxy.instance_eval(&block)
proxy.options
end
end

View File

@ -1,5 +1,6 @@
module AASM
class Event
include DslHelper
attr_reader :name, :options
@ -7,7 +8,8 @@ module AASM
@name = name
@transitions = []
@guards = Array(options[:guard] || options[:guards])
update(options, &block)
@options = options # QUESTION: .dup ?
add_options_from_dsl(@options, [:after, :before, :error, :success], &block) if block
end
# a neutered version of fire - it doesn't actually fire the event, it just
@ -82,14 +84,6 @@ module AASM
definitions
end
def update(options = {}, &block)
@options = options
if block then
instance_eval(&block)
end
self
end
# Execute if test == false, otherwise return true/false depending on whether it would fire
def _fire(obj, test, to_state=nil, *args)
result = test ? false : nil
@ -141,12 +135,5 @@ module AASM
end
end
[:after, :before, :error, :success].each do |callback_name|
define_method callback_name do |*args, &block|
options[callback_name] = Array(options[callback_name])
options[callback_name] << block if block
options[callback_name] += Array(args)
end
end
end
end # AASM

View File

@ -1,33 +1,29 @@
module AASM
class Transition
include DslHelper
attr_reader :from, :to, :opts
alias_method :options, :opts
def initialize(opts)
@from = opts[:from]
@to = opts[:to]
@guards = Array(opts[:guard] || opts[:guards])
@on_transition = opts[:on_transition]
def initialize(opts, &block)
add_options_from_dsl(opts, [:on_transition, :guard, :after], &block) if block
@from, @to, @guards = opts[:from], opts[:to], Array(opts[:guard] || opts[:guards])
if opts[:on_transition]
warn '[DEPRECATION] :on_transition is deprecated, use :after instead'
opts[:after] = Array(opts[:after]) + Array(opts[:on_transition])
end
@after = opts[:after]
@opts = opts
end
# TODO: should be named allowed? or similar
def perform(obj, *args)
@guards.each do |guard|
case guard
when Symbol, String
return false unless obj.send(guard, *args)
when Proc
return false unless guard.call(obj, *args)
end
end
true
invoke_callbacks_compatible_with_guard(@guards, obj, args, :guard => true)
end
def execute(obj, *args)
@on_transition.is_a?(Array) ?
@on_transition.each {|ot| _execute(obj, ot, *args)} :
_execute(obj, @on_transition, *args)
invoke_callbacks_compatible_with_guard(@after, obj, args)
end
def ==(obj)
@ -40,15 +36,28 @@ module AASM
private
def _execute(obj, on_transition, *args)
obj.aasm.from_state = @from if obj.aasm.respond_to?(:from_state=)
obj.aasm.to_state = @to if obj.aasm.respond_to?(:to_state=)
def invoke_callbacks_compatible_with_guard(code, record, args, options={})
if record.respond_to?(:aasm)
record.aasm.from_state = @from if record.aasm.respond_to?(:from_state=)
record.aasm.to_state = @to if record.aasm.respond_to?(:to_state=)
end
case on_transition
when Proc
on_transition.arity == 0 ? on_transition.call : on_transition.call(obj, *args)
case code
when Symbol, String
obj.send(:method, on_transition.to_sym).arity == 0 ? obj.send(on_transition) : obj.send(on_transition, *args)
# QUESTION : record.send(code, *args) ?
arity = record.send(:method, code.to_sym).arity
arity == 0 ? record.send(code) : record.send(code, *args)
when Proc
# QUESTION : record.instance_exec(*args, &code) ?
code.arity == 0 ? record.instance_exec(&code) : record.instance_exec(*args, &code)
when Array
if options[:guard] # guard callbacks
code.all? {|a| invoke_callbacks_compatible_with_guard(a, record, args)}
else # after callbacks
code.map {|a| invoke_callbacks_compatible_with_guard(a, record, args)}
end
else
true
end
end

View File

@ -12,7 +12,11 @@ class AuthMachine
state :waiting
event :register do
transitions :from => :passive, :to => :pending, :guard => Proc.new {|u| u.can_register? }
transitions :from => :passive, :to => :pending do
guard do
can_register?
end
end
end
event :activate do
@ -33,8 +37,8 @@ class AuthMachine
end
event :unsuspend do
transitions :from => :suspended, :to => :active, :guard => Proc.new {|u| u.has_activated? }
transitions :from => :suspended, :to => :pending, :guard => Proc.new {|u| u.has_activation_code? }
transitions :from => :suspended, :to => :active, :guard => Proc.new { has_activated? }
transitions :from => :suspended, :to => :pending, :guard => :has_activation_code?
transitions :from => :suspended, :to => :passive
end

View File

@ -12,9 +12,9 @@ class ParametrisedEvent
end
event :dress do
transitions :from => :sleeping, :to => :working, :on_transition => :wear_clothes
transitions :from => :showering, :to => [:working, :dating], :on_transition => Proc.new { |obj, *args| obj.wear_clothes(*args) }
transitions :from => :showering, :to => :prettying_up, :on_transition => [:condition_hair, :fix_hair]
transitions :from => :sleeping, :to => :working, :after => :wear_clothes
transitions :from => :showering, :to => [:working, :dating], :after => Proc.new { |*args| wear_clothes(*args) }
transitions :from => :showering, :to => :prettying_up, :after => [:condition_hair, :fix_hair]
end
end

View File

@ -262,24 +262,24 @@ describe 'parametrised events' do
expect(pe.aasm.current_state).to eq(:showering)
end
it 'should transition to default state when on_transition invoked' do
it 'should transition to default state when :after transition invoked' do
pe.dress!(nil, 'purple', 'dressy')
expect(pe.aasm.current_state).to eq(:working)
end
it 'should call on_transition method with args' do
it 'should call :after transition method with args' do
pe.wakeup!(:showering)
expect(pe).to receive(:wear_clothes).with('blue', 'jeans')
pe.dress!(:working, 'blue', 'jeans')
end
it 'should call on_transition proc' do
it 'should call :after transition proc' do
pe.wakeup!(:showering)
expect(pe).to receive(:wear_clothes).with('purple', 'slacks')
pe.dress!(:dating, 'purple', 'slacks')
end
it 'should call on_transition with an array of methods' do
it 'should call :after transition with an array of methods' do
pe.wakeup!(:showering)
expect(pe).to receive(:condition_hair)
expect(pe).to receive(:fix_hair)

View File

@ -61,6 +61,30 @@ describe AASM::Transition do
expect(st.opts).to eq(opts)
end
it 'should set on_transition with deprecation warning' do
opts = {:from => 'foo', :to => 'bar'}
st = AASM::Transition.allocate
st.should_receive(:warn).with('[DEPRECATION] :on_transition is deprecated, use :after instead')
st.send :initialize, opts do
guard :gg
on_transition :after_callback
end
st.opts[:after].should == [:after_callback]
end
it 'should set after and guard from dsl' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = AASM::Transition.new(opts) do
guard :gg
after :after_callback
end
st.opts[:guard].should == ['g', :gg]
st.opts[:after].should == [:after_callback] # TODO fix this bad code coupling
end
it 'should pass equality check if from and to are the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = AASM::Transition.new(opts)
@ -124,7 +148,7 @@ describe AASM::Transition, '- when performing guard checks' do
end
it 'should call the proc passing the object if the guard is a proc' do
opts = {:from => 'foo', :to => 'bar', :guard => Proc.new {|o| o.test}}
opts = {:from => 'foo', :to => 'bar', :guard => Proc.new { test }}
st = AASM::Transition.new(opts)
obj = double('object')
@ -136,31 +160,32 @@ end
describe AASM::Transition, '- when executing the transition with a Proc' do
it 'should call a Proc on the object with args' do
opts = {:from => 'foo', :to => 'bar', :on_transition => Proc.new {|o| o.test}}
opts = {:from => 'foo', :to => 'bar', :after => Proc.new {|a| test(a) }}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => 'aasm')
expect(opts[:on_transition]).to receive(:call).with(any_args)
obj.should_receive(:test).with(args)
st.execute(obj, args)
end
it 'should call a Proc on the object without args' do
opts = {:from => 'foo', :to => 'bar', :on_transition => Proc.new {||}}
prc = Proc.new {||}
opts = {:from => 'foo', :to => 'bar', :after => prc }
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => 'aasm')
expect(opts[:on_transition]).to receive(:call).with(no_args)
obj.should_receive(:instance_exec).with(no_args) # FIXME bad spec
st.execute(obj, args)
end
end
describe AASM::Transition, '- when executing the transition with an :on_transtion method call' do
describe AASM::Transition, '- when executing the transition with an :after method call' do
it 'should accept a String for the method name' do
opts = {:from => 'foo', :to => 'bar', :on_transition => 'test'}
opts = {:from => 'foo', :to => 'bar', :after => 'test'}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => 'aasm')
@ -171,7 +196,7 @@ describe AASM::Transition, '- when executing the transition with an :on_transtio
end
it 'should accept a Symbol for the method name' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
opts = {:from => 'foo', :to => 'bar', :after => :test}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => 'aasm')
@ -182,7 +207,7 @@ describe AASM::Transition, '- when executing the transition with an :on_transtio
end
it 'should pass args if the target method accepts them' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
opts = {:from => 'foo', :to => 'bar', :after => :test}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => 'aasm')
@ -197,7 +222,7 @@ describe AASM::Transition, '- when executing the transition with an :on_transtio
end
it 'should NOT pass args if the target method does NOT accept them' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
opts = {:from => 'foo', :to => 'bar', :after => :test}
st = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => 'aasm')
@ -213,7 +238,7 @@ describe AASM::Transition, '- when executing the transition with an :on_transtio
it 'should allow accessing the from_state and the to_state' do
opts = {:from => 'foo', :to => 'bar', :on_transition => :test}
st = AASM::Transition.new(opts)
transition = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => AASM::InstanceBase.new('object'))
@ -221,9 +246,9 @@ describe AASM::Transition, '- when executing the transition with an :on_transtio
"from: #{aasm.from_state} to: #{aasm.to_state}"
end
return_value = st.execute(obj, args)
return_value = transition.execute(obj, args)
expect(return_value).to eq('from: foo to: bar')
expect(return_value).to eq(['from: foo to: bar'])
end
end