From b6faecf47f97de3311f92add8c82d42dd89308d1 Mon Sep 17 00:00:00 2001 From: Brian Kidd Date: Fri, 1 Oct 2010 13:33:35 -0400 Subject: [PATCH 1/6] added an arity check for the :on_transition method to solve the n for 0 errors when your :on_transition method does NOT accept arguments. --- spec/unit/state_transition_spec.rb | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/spec/unit/state_transition_spec.rb b/spec/unit/state_transition_spec.rb index d0812c5..2027b93 100644 --- a/spec/unit/state_transition_spec.rb +++ b/spec/unit/state_transition_spec.rb @@ -82,3 +82,82 @@ describe AASM::SupportingClasses::StateTransition, '- when performing guard chec st.perform(obj) end end + +describe AASM::SupportingClasses::StateTransition, '- 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}} + st = AASM::SupportingClasses::StateTransition.new(opts) + args = {:arg1 => '1', :arg2 => '2'} + obj = mock('object') + + opts[:on_transition].should_receive(:call).with(any_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 {||}} + st = AASM::SupportingClasses::StateTransition.new(opts) + args = {:arg1 => '1', :arg2 => '2'} + obj = mock('object') + + opts[:on_transition].should_receive(:call).with(no_args) + + st.execute(obj, args) + end +end + +describe AASM::SupportingClasses::StateTransition, '- when executing the transition with an :on_transtion method call' do + it 'should accept a String for the method name' do + opts = {:from => 'foo', :to => 'bar', :on_transition => 'test'} + st = AASM::SupportingClasses::StateTransition.new(opts) + args = {:arg1 => '1', :arg2 => '2'} + obj = mock('object') + + obj.should_receive(:test) + + st.execute(obj, args) + end + + it 'should accept a Symbol for the method name' do + opts = {:from => 'foo', :to => 'bar', :on_transition => :test} + st = AASM::SupportingClasses::StateTransition.new(opts) + args = {:arg1 => '1', :arg2 => '2'} + obj = mock('object') + + obj.should_receive(:test) + + st.execute(obj, args) + end + + it 'should pass args if the target method accepts them' do + opts = {:from => 'foo', :to => 'bar', :on_transition => :test} + st = AASM::SupportingClasses::StateTransition.new(opts) + args = {:arg1 => '1', :arg2 => '2'} + obj = mock('object') + + obj.class.class_eval do + define_method(:test) {|*args| 'success'} + end + + return_value = st.execute(obj, args) + + return_value.should == 'success' + end + + it 'should NOT pass args if the target method does NOT accept them' do + opts = {:from => 'foo', :to => 'bar', :on_transition => :test} + st = AASM::SupportingClasses::StateTransition.new(opts) + args = {:arg1 => '1', :arg2 => '2'} + obj = mock('object') + + obj.class.class_eval do + define_method(:test) {|*args| 'success'} + end + + return_value = st.execute(obj, args) + + return_value.should == 'success' + end + +end From 43b8c08c9a5ac2a2de21bac4d619baf29edf2341 Mon Sep 17 00:00:00 2001 From: Brian Kidd Date: Fri, 1 Oct 2010 13:39:59 -0400 Subject: [PATCH 2/6] added an arity check for the :on_transition method to solve the n for 0 errors when your :on_transition method does NOT accept arguments. --- lib/aasm/state_transition.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/aasm/state_transition.rb b/lib/aasm/state_transition.rb index c5dce1d..991347b 100644 --- a/lib/aasm/state_transition.rb +++ b/lib/aasm/state_transition.rb @@ -35,11 +35,18 @@ class AASM::SupportingClasses::StateTransition private def _execute(obj, on_transition, *args) - case on_transition - when Symbol, String - obj.send(on_transition, *args) - when Proc + if on_transition.is_a?(Proc) + if on_transition.arity != 0 on_transition.call(obj, *args) + else + on_transition.call + end + elsif on_transition.is_a?(Symbol) || on_transition.is_a?(String) + if obj.send(:method, on_transition.to_sym).arity != 0 + obj.send(on_transition, *args) + else + obj.send(on_transition) + end end end From 7339c68c2886992e169f7037607fc55706ca14df Mon Sep 17 00:00:00 2001 From: bokmann Date: Tue, 5 Oct 2010 11:47:53 -0400 Subject: [PATCH 3/6] changes that allow you to ask "machine.may_event?", and get back the boolean answer from any pending transition guards --- lib/aasm/aasm.rb | 13 +++++++++++++ lib/aasm/event.rb | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/aasm/aasm.rb b/lib/aasm/aasm.rb index b08c522..852aac2 100644 --- a/lib/aasm/aasm.rb +++ b/lib/aasm/aasm.rb @@ -49,6 +49,13 @@ module AASM sm.events[name] = AASM::SupportingClasses::Event.new(name, options, &block) end + # an addition over standard aasm so that, before firing an event, you can ask + # may_event? and get back a boolean that tells you whether the guard method + # on the transition will let this happen. + define_method("may_#{name.to_s}?") do |*args| + aasm_test_event(name) + end + define_method("#{name.to_s}!") do |*args| aasm_fire_event(name, true, *args) end @@ -100,6 +107,12 @@ module AASM aasm_events_for_state(aasm_current_state) end + # filters the results of events_for_current_state so that only those that + # are really currently possible (given transition guards) are shown. + def aasm_permissible_events_for_current_state + aasm_events_for_current_state.select{ |e| self.send(("may_" + e.to_s + "?").to_sym) } + end + def aasm_events_for_state(state) events = self.class.aasm_events.values.select {|event| event.transitions_from_state?(state) } events.map {|event| event.name} diff --git a/lib/aasm/event.rb b/lib/aasm/event.rb index 22133ae..2241e05 100644 --- a/lib/aasm/event.rb +++ b/lib/aasm/event.rb @@ -7,6 +7,24 @@ class AASM::SupportingClasses::Event update(options, &block) end + # a neutered version of fire - it doesn't actually fir the event, it just + # executes the transition guards to determine if a transition is even + # an option given current conditions. + def may_fire?(obj, to_state=nil) + transitions = @transitions.select { |t| t.from == obj.aasm_current_state } + return false if transitions.size == 0 + + result = false + transitions.each do |transition| + next if to_state and !Array(transition.to).include?(to_state) + if transition.perform(obj) + result = true + break + end + end + result + end + def fire(obj, to_state=nil, *args) transitions = @transitions.select { |t| t.from == obj.aasm_current_state } raise AASM::InvalidTransition, "Event '#{name}' cannot transition from '#{obj.aasm_current_state}'" if transitions.size == 0 From 1215d729042f8b48fe0b5e05400d038d6bda7c16 Mon Sep 17 00:00:00 2001 From: bokmann Date: Tue, 5 Oct 2010 12:16:49 -0400 Subject: [PATCH 4/6] forgot to copy a method from my other fork --- lib/aasm/aasm.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/aasm/aasm.rb b/lib/aasm/aasm.rb index 852aac2..378fad5 100644 --- a/lib/aasm/aasm.rb +++ b/lib/aasm/aasm.rb @@ -53,7 +53,7 @@ module AASM # may_event? and get back a boolean that tells you whether the guard method # on the transition will let this happen. define_method("may_#{name.to_s}?") do |*args| - aasm_test_event(name) + aasm_test_event(name, *args) end define_method("#{name.to_s}!") do |*args| @@ -154,6 +154,11 @@ module AASM obj end + def aasm_test_event(name, *args) + event = self.class.aasm_events[name] + event.may_fire?(self, *args) + end + def aasm_fire_event(name, persist, *args) event = self.class.aasm_events[name] begin From c92e8fee189e49e50e111641c21582dca461348b Mon Sep 17 00:00:00 2001 From: bokmann Date: Tue, 5 Oct 2010 12:17:40 -0400 Subject: [PATCH 5/6] tests the new 'may' functionality --- test/functional/auth_machine_test.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/functional/auth_machine_test.rb b/test/functional/auth_machine_test.rb index f9d8ed8..b90a12a 100644 --- a/test/functional/auth_machine_test.rb +++ b/test/functional/auth_machine_test.rb @@ -29,6 +29,11 @@ class AuthMachine transitions :from => [:passive, :pending, :active, :suspended], :to => :deleted end + # a dummy event that can never happen + aasm_event :unpassify do + transitions :from => :passive, :to => :active, :guard => Proc.new {|u| false } + end + aasm_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? } @@ -89,6 +94,29 @@ class AuthMachineTest < Test::Unit::TestCase end context 'when being unsuspended' do + + should 'be able to be unsuspended' do + @auth = AuthMachine.new + @auth.activate! + @auth.suspend! + assert @auth.may_unsuspend? + end + + should 'not be able to be unsuspended into active' do + @auth = AuthMachine.new + @auth.suspend! + assert_equal false, @auth.may_unsuspend?(:active) + end + + should 'not be able to be unpassified' do + @auth = AuthMachine.new + @auth.activate! + @auth.suspend! + @auth.unsuspend! + + assert_equal false, @auth.may_unpassify? + end + should 'be active if previously activated' do @auth = AuthMachine.new @auth.activate! From 67cd00cfbff52d24ab8d0aaacf465add28daa116 Mon Sep 17 00:00:00 2001 From: Brian Kidd Date: Fri, 8 Oct 2010 12:35:30 -0400 Subject: [PATCH 6/6] -refactored _execute() for much better readability :-) -added dependency for for ruby-debug-completion -turned on auto-eval for ruby-debug -added the ability to run debugger on the last line of a method: http://dancingpenguinsoflight.com/2009/09/an-improved-ruby-debugger-invocation/ --- Rakefile | 25 +++++++++++++------------ lib/aasm/state_transition.rb | 17 +++++------------ spec/spec_helper.rb | 3 +++ test/test_helper.rb | 4 ++-- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/Rakefile b/Rakefile index e6f8d17..d382f70 100644 --- a/Rakefile +++ b/Rakefile @@ -4,20 +4,21 @@ require 'rake' begin require 'jeweler' Jeweler::Tasks.new do |gem| - gem.name = "aasm" + gem.name = 'aasm' gem.summary = %Q{State machine mixin for Ruby objects} gem.description = %Q{AASM is a continuation of the acts as state machine rails plugin, built for plain Ruby objects.} - gem.homepage = "http://rubyist.github.com/aasm/" - gem.authors = ["Scott Barron", "Scott Petersen", "Travis Tilley"] - gem.email = "scott@elitists.net, ttilley@gmail.com" - gem.add_development_dependency "rspec" - gem.add_development_dependency "shoulda" + gem.homepage = 'http://rubyist.github.com/aasm/' + gem.authors = ['Scott Barron', 'Scott Petersen', 'Travis Tilley'] + gem.email = 'scott@elitists.net, ttilley@gmail.com' + gem.add_development_dependency 'ruby-debug-completion' + gem.add_development_dependency 'rspec' + gem.add_development_dependency 'shoulda' gem.add_development_dependency 'sdoc' # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings end Jeweler::GemcutterTasks.new rescue LoadError - puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" + puts 'Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler' end require 'spec/rake/spectask' @@ -38,7 +39,7 @@ begin end rescue LoadError task :rcov do - abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" + abort 'RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov' end end @@ -66,7 +67,7 @@ begin end rescue LoadError task :reek do - abort "Reek is not available. In order to run reek, you must: sudo gem install reek" + abort 'Reek is not available. In order to run reek, you must: sudo gem install reek' end end @@ -78,7 +79,7 @@ begin end rescue LoadError task :roodi do - abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi" + abort 'Roodi is not available. In order to run roodi, you must: sudo gem install roodi' end end @@ -91,7 +92,7 @@ begin if File.exist?('VERSION') version = File.read('VERSION') else - version = "" + version = '' end rdoc.rdoc_dir = 'rdoc' @@ -103,6 +104,6 @@ begin rdoc.template = 'direct' end rescue LoadError - puts "aasm makes use of the sdoc gem. Install it with: sudo gem install sdoc" + puts 'aasm makes use of the sdoc gem. Install it with: sudo gem install sdoc' end diff --git a/lib/aasm/state_transition.rb b/lib/aasm/state_transition.rb index 991347b..4b57e72 100644 --- a/lib/aasm/state_transition.rb +++ b/lib/aasm/state_transition.rb @@ -35,18 +35,11 @@ class AASM::SupportingClasses::StateTransition private def _execute(obj, on_transition, *args) - if on_transition.is_a?(Proc) - if on_transition.arity != 0 - on_transition.call(obj, *args) - else - on_transition.call - end - elsif on_transition.is_a?(Symbol) || on_transition.is_a?(String) - if obj.send(:method, on_transition.to_sym).arity != 0 - obj.send(on_transition, *args) - else - obj.send(on_transition) - end + case on_transition + when Proc + on_transition.arity == 0 ? on_transition.call : on_transition.call(obj, *args) + when Symbol, String + obj.send(:method, on_transition.to_sym).arity == 0 ? obj.send(on_transition) : obj.send(on_transition, *args) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9b9df8e..53fbc18 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,9 @@ require 'aasm' require 'spec' require 'spec/autorun' +require 'ruby-debug'; Debugger.settings[:autoeval] = true; debugger; rubys_debugger = 'annoying' +require 'ruby-debug/completion' + Spec::Runner.configure do |config| end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5dc5fe1..6910119 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,8 +23,8 @@ class Test::Unit::TestCase end begin - require 'ruby-debug' - Debugger.start + require 'ruby-debug'; Debugger.settings[:autoeval] = true; debugger; rubys_debugger = 'annoying' + require 'ruby-debug/completion' rescue LoadError end