Permalink
Browse files

Add fire_#{name}_event instance method for firing an arbitrary event …

…on a state machine
  • Loading branch information...
1 parent cd77aba commit 8f71eb17028f9b7d6d3355da7351a681769daf48 @obrie obrie committed Nov 13, 2011
Showing with 71 additions and 3 deletions.
  1. +1 −0 CHANGELOG.md
  2. +3 −1 README.md
  3. +7 −1 lib/state_machine.rb
  4. +5 −0 lib/state_machine/machine.rb
  5. +21 −1 test/functional/state_machine_test.rb
  6. +34 −0 test/unit/machine_test.rb
View
@@ -1,5 +1,6 @@
# master
+* Add fire_#{name}_event instance method for firing an arbitrary event on a state machine
* Improve InvalidTransition exception messages to include the failure reason(s) in ORM integrations
* Don't allow around_transitions to attempt to be called in multiple execution contexts when run in jruby
* Allow :from option to be used in transitions defined within state contexts
View
@@ -230,7 +230,9 @@ vehicle.speed # => 10
vehicle.moving? # => true
vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
-vehicle.shift_up # => true
+# A generic event helper is available to fire without going through the event's instance method
+vehicle.fire_state_event(:shift_up) # => true
+
# Call state-driven behavior that's undefined for the state raises a NoMethodError
vehicle.speed # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
View
@@ -141,6 +141,9 @@ module MacroMethods
# transitions that can be made on the current object's state
# * <tt>state_paths(requirements = {})</tt> - Gets the list of sequences of
# transitions that can be run from the current object's state
+ # * <tt>fire_state_event(name, *args)</tt> - Fires an arbitrary event with
+ # the given argument list. This is essentially the same as calling the
+ # actual event method itself.
#
# The <tt>state_events</tt>, <tt>state_transitions</tt>, and <tt>state_paths</tt>
# helpers all take an optional set of requirements for determining what's
@@ -188,7 +191,7 @@ module MacroMethods
# vehicle.state_events(:to => :parked) # => []
#
# vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
- # vehicle.ignite
+ # vehicle.ignite # => true
# vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
#
# vehicle.state_transitions(:on => :ignite) # => []
@@ -202,6 +205,9 @@ module MacroMethods
# # [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
# # #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
# # ]
+ #
+ # # Fire arbitrary events
+ # vehicle.fire_state_event(:park) # => true
#
# == Attribute initialization
#
@@ -2000,6 +2000,11 @@ def define_event_helpers
machine.events.transitions_for(object, *args)
end
+ # Fire an arbitrary event for this machine
+ define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
+ machine.events.fetch(event).fire(object, *args)
+ end
+
# Add helpers for tracking the event / transition to invoke when the
# action is called
if action
@@ -40,7 +40,7 @@ def save
end
class Vehicle < ModelBase
- attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed
+ attr_accessor :auto_shop, :seatbelt_on, :insurance_premium, :force_idle, :callbacks, :saved, :time_elapsed, :last_transition_args
def initialize(attributes = {})
attributes = {
@@ -58,6 +58,7 @@ def initialize(attributes = {})
# Defines the state machine for the state of the vehicled
state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked}, :action => :save do
+ before_transition {|vehicle, transition| vehicle.last_transition_args = transition.args}
before_transition :parked => any, :do => :put_on_seatbelt
before_transition any => :stalled, :do => :increase_insurance_premium
after_transition any => :parked, :do => lambda {|vehicle| vehicle.seatbelt_on = false}
@@ -340,6 +341,25 @@ def test_should_have_a_list_of_possible_paths
]], @vehicle.state_paths(:to => :first_gear)
end
+ def test_should_allow_generic_event_to_fire
+ assert @vehicle.fire_state_event(:ignite)
+ assert_equal 'idling', @vehicle.state
+ end
+
+ def test_should_pass_arguments_through_to_generic_event_runner
+ @vehicle.fire_state_event(:ignite, 1, 2, 3)
+ assert_equal [1, 2, 3], @vehicle.last_transition_args
+ end
+
+ def test_should_allow_skipping_action_through_generic_event_runner
+ @vehicle.fire_state_event(:ignite, false)
+ assert_equal false, @vehicle.saved
+ end
+
+ def test_should_raise_error_with_invalid_event_through_generic_event_runer
+ assert_raise(IndexError) { @vehicle.fire_state_event(:invalid) }
+ end
+
def test_should_allow_ignite
assert @vehicle.ignite
assert_equal 'idling', @vehicle.state
View
@@ -125,6 +125,10 @@ def test_should_define_a_path_reader_for_the_attribute
assert @object.respond_to?(:state_paths)
end
+ def test_should_define_an_event_runner_for_the_attribute
+ assert @object.respond_to?(:fire_state_event)
+ end
+
def test_should_not_define_an_event_attribute_reader
assert !@object.respond_to?(:state_event)
end
@@ -222,6 +226,10 @@ def test_should_define_a_transition_reader_for_the_attribute
assert @object.respond_to?(:status_transitions)
end
+ def test_should_define_an_event_runner_for_the_attribute
+ assert @object.respond_to?(:fire_status_event)
+ end
+
def test_should_define_a_human_attribute_name_reader_for_the_attribute
assert @klass.respond_to?(:human_status_name)
end
@@ -1504,6 +1512,10 @@ def state_transitions
def state_paths
[[{:parked => :idling}]]
end
+
+ def fire_state_event
+ true
+ end
end
@klass = Class.new(@superclass)
@@ -1582,10 +1594,15 @@ def test_should_not_redefine_attribute_paths_reader
assert_equal [[{:parked => :idling}]], @object.state_paths
end
+ def test_should_not_redefine_event_runner
+ assert_equal true, @object.fire_state_event
+ end
+
def test_should_output_warning
expected = [
'Instance method "state_events"',
'Instance method "state_transitions"',
+ 'Instance method "fire_state_event"',
'Instance method "state_paths"',
'Class method "human_state_name"',
'Class method "human_state_event_name"',
@@ -1669,6 +1686,10 @@ def state_transitions
def state_paths
[[{:parked => :idling}]]
end
+
+ def fire_state_event
+ true
+ end
end
StateMachine::Integrations.const_set('Custom', Module.new do
@@ -1746,6 +1767,10 @@ def test_should_not_redefine_attribute_paths_reader
assert_equal [[{:parked => :idling}]], @object.state_paths
end
+ def test_should_not_redefine_event_runner
+ assert_equal true, @object.fire_state_event
+ end
+
def test_should_allow_super_chaining
@klass.class_eval do
def self.with_state(*states)
@@ -1805,6 +1830,10 @@ def state_transitions
def state_paths
super
end
+
+ def fire_state_event(event)
+ super
+ end
end
assert_equal [], @klass.with_state
@@ -1824,6 +1853,7 @@ def state_paths
assert_equal [], @object.state_events
assert_equal [], @object.state_transitions
assert_equal [], @object.state_paths
+ assert_equal false, @object.fire_state_event(:ignite)
end
def test_should_not_output_warning
@@ -2958,6 +2988,10 @@ def test_should_define_a_path_reader_for_the_attribute
assert @object.respond_to?(:state_paths)
end
+ def test_should_define_an_event_runner_for_the_attribute
+ assert @object.respond_to?(:fire_state_event)
+ end
+
def test_should_define_a_human_attribute_name_reader
assert @klass.respond_to?(:human_state_name)
end

0 comments on commit 8f71eb1

Please sign in to comment.