callback :on_transition renamed to :after and changed its binding (see #59)

This commit is contained in:
Thorsten Böttger 2014-09-12 14:30:36 +02:00
parent 1c23eaf350
commit 72b0257734
13 changed files with 197 additions and 89 deletions

View File

@ -11,6 +11,9 @@
thanks to [@ejlangev](https://github.com/ejlangev))
* **DSL change**: `after_commit` hooks are now event-based (see [issue #112](https://github.com/aasm/aasm/issues/112))
* **DSL change**: event and state callbacks have been re-ordered; state callbacks are not run anymore if any guard fails
* **DSL change**: `:on_transition` renamed to `:after`
* **DSL change**: `:on_transition` renamed to `:after`
* **DSL change**: transition `:after` binding changed (see [issue #59](https://github.com/aasm/aasm/issues/59), thanks to [@stiff](https://github.com/stiff))
## 3.9.0 (not yet released)

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
@ -208,37 +212,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
@ -317,7 +321,6 @@ class Job < ActiveRecord::Base
end
```
<<<<<<< HEAD
If you want to make sure that the _AASM_ column for storing the state is not directly assigned,
configure _AASM_ to not allow direct assignment, like this:
@ -351,7 +354,6 @@ job.aasm_state # => 'sleeping'
You can use
[enumerations](http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html)
in Rails 4.1+ for your state column:
>>>>>>> master
```ruby
class Job < ActiveRecord::Base
@ -380,7 +382,6 @@ setting --- AASM auto-detects this situation and enabled enum
support. If anything goes wrong, you can disable enum functionality
and fall back to the default behavior by setting ```:enum```
to ```false```.
>>>>>>> master
### Sequel

View File

@ -11,6 +11,53 @@ all (event- and transition-based) guards are run to check whether the state call
can be run or not.
### Callback `:on_transition` renamed to `:after` and changed its binding
The transition callback `:on_transition` has been renamed to `:after` in order
to make clear it is being called (namely _after_ doing the transition).
Furthermore, in alignment with the other callbacks, it's not receiving the object
at hand as first parameter and binds the current object to self.
In summary, change from
```ruby
aasm do
...
transitions :from => :from_state, :to => :to_state, :on_transition => :do_something
...
end
...
def some_other_method(arg)
...
end
def do_something(obj, arg1, arg2)
obj.some_other_method(arg1)
end
```
to
```ruby
aasm do
...
transitions :from => :from_state, :to => :to_state, :after => :do_something
...
end
...
def some_other_method(arg)
...
end
def do_something(arg1, arg2)
some_other_method(arg1) # run on the object as self
end
```
### `after_commit` hooks are now event-based
The `after_commit` hooks have been move from the state level to the event level.

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,30 @@
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 = Array(opts[:after])
@after = @after[0] if @after.size == 1
@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 +37,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

@ -24,7 +24,7 @@ class CallbackNewDsl
:after_exit => :after_exit_closed
event :close, :before => :before, :after => :after, :guard => :event_guard do
transitions :to => :closed, :from => [:open], :guard => :transition_guard, :on_transition => :transitioning
transitions :to => :closed, :from => [:open], :guard => :transition_guard, :after => :transitioning
end
event :open, :before => :before, :after => :after do
@ -75,7 +75,7 @@ class CallbackNewDslArgs
:after_exit => :after_exit_closed
event :close, :before => :before, :after => :after do
transitions :to => :closed, :from => [:open], :on_transition => :transition_proc
transitions :to => :closed, :from => [:open], :after => :transition_proc
end
event :open, :before => :before, :after => :after do
@ -112,8 +112,8 @@ class CallbackWithStateArg
state :out_to_lunch
event :close, :before => :before_method, :after => :after_method do
transitions :to => :closed, :from => [:open], :on_transition => :transition_method
transitions :to => :out_to_lunch, :from => [:open], :on_transition => :transition_method2
transitions :to => :closed, :from => [:open], :after => :transition_method
transitions :to => :out_to_lunch, :from => [:open], :after => :transition_method2
end
end

View File

@ -12,7 +12,7 @@ class DoubleDefiner
# simulating a reload
state :finished, :before_enter => :do_enter
event :finish do
transitions :from => :started, :to => :finished, :on_transition => :do_on_transition
transitions :from => :started, :to => :finished, :after => :do_on_transition
end
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

@ -280,24 +280,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')
@ -212,8 +237,8 @@ describe AASM::Transition, '- when executing the transition with an :on_transtio
end
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)
opts = {:from => 'foo', :to => 'bar', :after => :test}
transition = AASM::Transition.new(opts)
args = {:arg1 => '1', :arg2 => '2'}
obj = double('object', :aasm => AASM::InstanceBase.new('object'))
@ -221,7 +246,7 @@ 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')
end