pluginaweek-state_machine 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. data/CHANGELOG.rdoc +273 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +429 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +251 -0
  33. data/lib/state_machine/event_collection.rb +113 -0
  34. data/lib/state_machine/extensions.rb +158 -0
  35. data/lib/state_machine/guard.rb +219 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +444 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +10 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +325 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +292 -0
  43. data/lib/state_machine/machine.rb +1431 -0
  44. data/lib/state_machine/machine_collection.rb +146 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +367 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +129 -0
  60. data/test/unit/event_collection_test.rb +293 -0
  61. data/test/unit/event_test.rb +605 -0
  62. data/test/unit/guard_test.rb +862 -0
  63. data/test/unit/integrations/active_record_test.rb +1001 -0
  64. data/test/unit/integrations/data_mapper_test.rb +694 -0
  65. data/test/unit/integrations/sequel_test.rb +486 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +710 -0
  70. data/test/unit/machine_test.rb +1910 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1113 -0
  78. metadata +161 -0
@@ -0,0 +1,36 @@
1
+ module StateMachine
2
+ # Provides a set of helper methods for making assertions about the content
3
+ # of various objects
4
+ module Assertions
5
+ # Validates that the given hash *only* includes the specified valid keys.
6
+ # If any invalid keys are found, an ArgumentError will be raised.
7
+ #
8
+ # == Examples
9
+ #
10
+ # options = {:name => 'John Smith', :age => 30}
11
+ #
12
+ # assert_valid_keys(options, :name) # => ArgumentError: Invalid key(s): age
13
+ # assert_valid_keys(options, 'name', 'age') # => ArgumentError: Invalid key(s): age, name
14
+ # assert_valid_keys(options, :name, :age) # => nil
15
+ def assert_valid_keys(hash, *valid_keys)
16
+ invalid_keys = hash.keys - valid_keys
17
+ raise ArgumentError, "Invalid key(s): #{invalid_keys.join(', ')}" unless invalid_keys.empty?
18
+ end
19
+
20
+ # Validates that the given hash only includes at *most* one of a set of
21
+ # exclusive keys. If more than one key is found, an ArgumentError will be
22
+ # raised.
23
+ #
24
+ # == Examples
25
+ #
26
+ # options = {:only => :on, :except => :off}
27
+ # assert_exclusive_keys(options, :only) # => nil
28
+ # assert_exclusive_keys(options, :except) # => nil
29
+ # assert_exclusive_keys(options, :only, :except) # => ArgumentError: Conflicting keys: only, except
30
+ # assert_exclusive_keys(options, :only, :except, :with) # => ArgumentError: Conflicting keys: only, except
31
+ def assert_exclusive_keys(hash, *exclusive_keys)
32
+ conflicting_keys = exclusive_keys & hash.keys
33
+ raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,189 @@
1
+ require 'state_machine/guard'
2
+ require 'state_machine/eval_helpers'
3
+
4
+ module StateMachine
5
+ # Callbacks represent hooks into objects that allow logic to be triggered
6
+ # before or after a specific transition occurs.
7
+ class Callback
8
+ include EvalHelpers
9
+
10
+ class << self
11
+ # Determines whether to automatically bind the callback to the object
12
+ # being transitioned. This only applies to callbacks that are defined as
13
+ # lambda blocks (or Procs). Some integrations, such as DataMapper, handle
14
+ # callbacks by executing them bound to the object involved, while other
15
+ # integrations, such as ActiveRecord, pass the object as an argument to
16
+ # the callback. This can be configured on an application-wide basis by
17
+ # setting this configuration to +true+ or +false+. The default value
18
+ # is +false+.
19
+ #
20
+ # *Note* that the DataMapper and Sequel integrations automatically
21
+ # configure this value on a per-callback basis, so it does not have to
22
+ # be enabled application-wide.
23
+ #
24
+ # == Examples
25
+ #
26
+ # When not bound to the object:
27
+ #
28
+ # class Vehicle
29
+ # state_machine do
30
+ # before_transition do |vehicle|
31
+ # vehicle.set_alarm
32
+ # end
33
+ # end
34
+ #
35
+ # def set_alarm
36
+ # ...
37
+ # end
38
+ # end
39
+ #
40
+ # When bound to the object:
41
+ #
42
+ # StateMachine::Callback.bind_to_object = true
43
+ #
44
+ # class Vehicle
45
+ # state_machine do
46
+ # before_transition do
47
+ # self.set_alarm
48
+ # end
49
+ # end
50
+ #
51
+ # def set_alarm
52
+ # ...
53
+ # end
54
+ # end
55
+ attr_accessor :bind_to_object
56
+
57
+ # The application-wide terminator to use for callbacks when not
58
+ # explicitly defined. Terminators determine whether to cancel a
59
+ # callback chain based on the return value of the callback.
60
+ #
61
+ # See StateMachine::Callback#terminator for more information.
62
+ attr_accessor :terminator
63
+ end
64
+
65
+ # An optional block for determining whether to cancel the callback chain
66
+ # based on the return value of the callback. By default, the callback
67
+ # chain never cancels based on the return value (i.e. there is no implicit
68
+ # terminator). Certain integrations, such as ActiveRecord and Sequel,
69
+ # change this default value.
70
+ #
71
+ # == Examples
72
+ #
73
+ # Canceling the callback chain without a terminator:
74
+ #
75
+ # class Vehicle
76
+ # state_machine do
77
+ # before_transition do |vehicle|
78
+ # throw :halt
79
+ # end
80
+ # end
81
+ # end
82
+ #
83
+ # Canceling the callback chain with a terminator value of +false+:
84
+ #
85
+ # class Vehicle
86
+ # state_machine do
87
+ # before_transition do |vehicle|
88
+ # false
89
+ # end
90
+ # end
91
+ # end
92
+ attr_reader :terminator
93
+
94
+ # The guard that determines whether or not this callback can be invoked
95
+ # based on the context of the transition. The event, from state, and
96
+ # to state must all match in order for the guard to pass.
97
+ #
98
+ # See StateMachine::Guard for more information.
99
+ attr_reader :guard
100
+
101
+ # Creates a new callback that can get called based on the configured
102
+ # options.
103
+ #
104
+ # In addition to the possible configuration options for guards, the
105
+ # following options can be configured:
106
+ # * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
107
+ # If set to false, the object will be passed as a parameter instead.
108
+ # Default is integration-specific or set to the application default.
109
+ # * <tt>:terminator</tt> - A block/proc that determines what callback
110
+ # results should cause the callback chain to halt (if not using the
111
+ # default <tt>throw :halt</tt> technique).
112
+ #
113
+ # More information about how those options affect the behavior of the
114
+ # callback can be found in their attribute definitions.
115
+ def initialize(*args, &block)
116
+ options = args.last.is_a?(Hash) ? args.pop : {}
117
+ @methods = args
118
+ @methods.concat(Array(options.delete(:do)))
119
+ @methods << block if block_given?
120
+
121
+ raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any?
122
+
123
+ options = {:bind_to_object => self.class.bind_to_object, :terminator => self.class.terminator}.merge(options)
124
+
125
+ # Proxy lambda blocks so that they're bound to the object
126
+ bind_to_object = options.delete(:bind_to_object)
127
+ @methods.map! do |method|
128
+ bind_to_object && method.is_a?(Proc) ? bound_method(method) : method
129
+ end
130
+
131
+ @terminator = options.delete(:terminator)
132
+ @guard = Guard.new(options)
133
+ end
134
+
135
+ # Gets a list of the states known to this callback by looking at the
136
+ # guard's known states
137
+ def known_states
138
+ guard.known_states
139
+ end
140
+
141
+ # Runs the callback as long as the transition context matches the guard
142
+ # requirements configured for this callback.
143
+ #
144
+ # If a terminator has been configured and it matches the result from the
145
+ # evaluated method, then the callback chain should be halted
146
+ def call(object, context = {}, *args)
147
+ if @guard.matches?(object, context)
148
+ @methods.each do |method|
149
+ result = evaluate_method(object, method, *args)
150
+ throw :halt if @terminator && @terminator.call(result)
151
+ end
152
+
153
+ true
154
+ else
155
+ false
156
+ end
157
+ end
158
+
159
+ private
160
+ # Generates a method that can be bound to the object being transitioned
161
+ # when the callback is invoked
162
+ def bound_method(block)
163
+ arity = block.arity
164
+
165
+ if RUBY_VERSION >= '1.9'
166
+ lambda do |object, *args|
167
+ object.instance_exec(*(arity == 0 ? [] : args), &block)
168
+ end
169
+ else
170
+ # Generate a thread-safe unbound method that can be used on any object.
171
+ # This is a workaround for not having Ruby 1.9's instance_exec
172
+ unbound_method = Object.class_eval do
173
+ time = Time.now
174
+ method_name = "__bind_#{time.to_i}_#{time.usec}"
175
+ define_method(method_name, &block)
176
+ method = instance_method(method_name)
177
+ remove_method(method_name)
178
+ method
179
+ end
180
+
181
+ # Proxy calls to the method so that the method can be bound *and*
182
+ # the arguments are adjusted
183
+ lambda do |object, *args|
184
+ unbound_method.bind(object).call(*(arity == 0 ? [] : args))
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,94 @@
1
+ require 'state_machine/eval_helpers'
2
+
3
+ module StateMachine
4
+ # Represents a type of module in which class-level methods are proxied to
5
+ # another class, injecting a custom <tt>:if</tt> condition along with method.
6
+ #
7
+ # This is used for being able to automatically include conditionals which
8
+ # check the current state in class-level methods that have configuration
9
+ # options.
10
+ #
11
+ # == Examples
12
+ #
13
+ # class Vehicle
14
+ # class << self
15
+ # attr_accessor :validations
16
+ #
17
+ # def validate(options, &block)
18
+ # validations << options
19
+ # end
20
+ # end
21
+ #
22
+ # self.validations = []
23
+ # attr_accessor :state, :simulate
24
+ #
25
+ # def moving?
26
+ # self.class.validations.all? {|validation| validation[:if].call(self)}
27
+ # end
28
+ # end
29
+ #
30
+ # In the above class, a simple set of validation behaviors have been defined.
31
+ # Each validation consists of a configuration like so:
32
+ #
33
+ # Vehicle.validate :unless => :simulate
34
+ # Vehicle.validate :if => lambda {|vehicle| ...}
35
+ #
36
+ # In order to scope conditions, a condition proxy can be created to the
37
+ # Vehicle class. For example,
38
+ #
39
+ # proxy = StateMachine::ConditionProxy.new(Vehicle, lambda {|vehicle| vehicle.state == 'first_gear'})
40
+ # proxy.validate(:unless => :simulate)
41
+ #
42
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
43
+ # vehicle.moving? # => false
44
+ #
45
+ # vehicle.state = 'first_gear'
46
+ # vehicle.moving? # => true
47
+ #
48
+ # vehicle.simulate = true
49
+ # vehicle.moving? # => false
50
+ class ConditionProxy < Module
51
+ include EvalHelpers
52
+
53
+ # Creates a new proxy to the given class, merging in the given condition
54
+ def initialize(klass, condition)
55
+ @klass = klass
56
+ @condition = condition
57
+ end
58
+
59
+ # Hooks in condition-merging to methods that don't exist in this module
60
+ def method_missing(*args, &block)
61
+ # Get the configuration
62
+ if args.last.is_a?(Hash)
63
+ options = args.last
64
+ else
65
+ args << options = {}
66
+ end
67
+
68
+ # Get any existing condition that may need to be merged
69
+ if_condition = options.delete(:if)
70
+ unless_condition = options.delete(:unless)
71
+
72
+ # Provide scope access to configuration in case the block is evaluated
73
+ # within the object instance
74
+ proxy = self
75
+ proxy_condition = @condition
76
+
77
+ # Replace the configuration condition with the one configured for this
78
+ # proxy, merging together any existing conditions
79
+ options[:if] = lambda do |*args|
80
+ # Block may be executed within the context of the actual object, so
81
+ # it'll either be the first argument or the executing context
82
+ object = args.first || self
83
+
84
+ proxy.evaluate_method(object, proxy_condition) &&
85
+ Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
86
+ !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
87
+ end
88
+
89
+ # Evaluate the method on the original class with the condition proxied
90
+ # through
91
+ @klass.send(*args, &block)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,67 @@
1
+ module StateMachine
2
+ # Provides a set of helper methods for evaluating methods within the context
3
+ # of an object.
4
+ module EvalHelpers
5
+ # Evaluates one of several different types of methods within the context
6
+ # of the given object. Methods can be one of the following types:
7
+ # * Symbol
8
+ # * Method / Proc
9
+ # * String
10
+ #
11
+ # == Examples
12
+ #
13
+ # Below are examples of the various ways that a method can be evaluated
14
+ # on an object:
15
+ #
16
+ # class Person
17
+ # def initialize(name)
18
+ # @name = name
19
+ # end
20
+ #
21
+ # def name
22
+ # @name
23
+ # end
24
+ # end
25
+ #
26
+ # class PersonCallback
27
+ # def self.run(person)
28
+ # person.name
29
+ # end
30
+ # end
31
+ #
32
+ # person = Person.new('John Smith')
33
+ #
34
+ # evaluate_method(person, :name) # => "John Smith"
35
+ # evaluate_method(person, PersonCallback.method(:run)) # => "John Smith"
36
+ # evaluate_method(person, Proc.new {|person| person.name}) # => "John Smith"
37
+ # evaluate_method(person, lambda {|person| person.name}) # => "John Smith"
38
+ # evaluate_method(person, '@name') # => "John Smith"
39
+ #
40
+ # == Additional arguments
41
+ #
42
+ # Additional arguments can be passed to the methods being evaluated. If
43
+ # the method defines additional arguments other than the object context,
44
+ # then all arguments are required.
45
+ #
46
+ # For example,
47
+ #
48
+ # person = Person.new('John Smith')
49
+ #
50
+ # evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
51
+ # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
52
+ # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
53
+ def evaluate_method(object, method, *args)
54
+ case method
55
+ when Symbol
56
+ object.method(method).arity == 0 ? object.send(method) : object.send(method, *args)
57
+ when Proc, Method
58
+ args.unshift(object)
59
+ [0, 1].include?(method.arity) ? method.call(*args.slice(0, method.arity)) : method.call(*args)
60
+ when String
61
+ eval(method, object.instance_eval {binding})
62
+ else
63
+ raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,251 @@
1
+ require 'state_machine/transition'
2
+ require 'state_machine/guard'
3
+ require 'state_machine/assertions'
4
+ require 'state_machine/matcher_helpers'
5
+
6
+ module StateMachine
7
+ # An invalid event was specified
8
+ class InvalidEvent < StandardError
9
+ end
10
+
11
+ # An event defines an action that transitions an attribute from one state to
12
+ # another. The state that an attribute is transitioned to depends on the
13
+ # guards configured for the event.
14
+ class Event
15
+ include Assertions
16
+ include MatcherHelpers
17
+
18
+ # The state machine for which this event is defined
19
+ attr_accessor :machine
20
+
21
+ # The name of the event
22
+ attr_reader :name
23
+
24
+ # The fully-qualified name of the event, scoped by the machine's namespace
25
+ attr_reader :qualified_name
26
+
27
+ # The list of guards that determine what state this event transitions
28
+ # objects to when fired
29
+ attr_reader :guards
30
+
31
+ # A list of all of the states known to this event using the configured
32
+ # guards/transitions as the source
33
+ attr_reader :known_states
34
+
35
+ # Creates a new event within the context of the given machine
36
+ def initialize(machine, name) #:nodoc:
37
+ @machine = machine
38
+ @name = name
39
+ @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
40
+ @guards = []
41
+ @known_states = []
42
+
43
+ add_actions
44
+ end
45
+
46
+ # Creates a copy of this event in addition to the list of associated
47
+ # guards to prevent conflicts across events within a class hierarchy.
48
+ def initialize_copy(orig) #:nodoc:
49
+ super
50
+ @guards = @guards.dup
51
+ @known_states = @known_states.dup
52
+ end
53
+
54
+ # Creates a new transition that determines what to change the current state
55
+ # to when this event fires.
56
+ #
57
+ # == Defining transitions
58
+ #
59
+ # The options for a new transition uses the Hash syntax to map beginning
60
+ # states to ending states. For example,
61
+ #
62
+ # transition :parked => :idling, :idling => :first_gear
63
+ #
64
+ # In this case, when the event is fired, this transition will cause the
65
+ # state to be +idling+ if it's current state is +parked+ or +first_gear+
66
+ # if it's current state is +idling+.
67
+ #
68
+ # To help defining these implicit transitions, a set of helpers are available
69
+ # for defining slightly more complex matching:
70
+ # * <tt>all</tt> - Matches every state in the machine
71
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
72
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
73
+ # * <tt>same</tt> - Matches the same state being transitioned from
74
+ #
75
+ # See StateMachine::MatcherHelpers for more information.
76
+ #
77
+ # Examples:
78
+ #
79
+ # transition all => nil # Transitions to nil regardless of the current state
80
+ # transition all => :idling # Transitions to :idling regardless of the current state
81
+ # transition all - [:idling, :first_gear] => :idling # Transitions every state but :idling and :first_gear to :idling
82
+ # transition nil => :idling # Transitions to :idling from the nil state
83
+ # transition :parked => :idling # Transitions to :idling if :parked
84
+ # transition [:parked, :stalled] => :idling # Transitions to :idling if :parked or :stalled
85
+ #
86
+ # transition :parked => same # Loops :parked back to :parked
87
+ # transition [:parked, :stalled] => same # Loops either :parked or :stalled back to the same state
88
+ # transition all - :parked => same # Loops every state but :parked back to the same state
89
+ #
90
+ # == Verbose transitions
91
+ #
92
+ # Transitions can also be defined use an explicit set of deprecated
93
+ # configuration options:
94
+ # * <tt>:from</tt> - A state or array of states that can be transitioned from.
95
+ # If not specified, then the transition can occur for *any* state.
96
+ # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
97
+ # then the transition will simply loop back (i.e. the state will not change).
98
+ # * <tt>:except_from</tt> - A state or array of states that *cannot* be
99
+ # transitioned from.
100
+ #
101
+ # Examples:
102
+ #
103
+ # transition :to => nil
104
+ # transition :to => :idling
105
+ # transition :except_from => [:idling, :first_gear], :to => :idling
106
+ # transition :from => nil, :to => :idling
107
+ # transition :from => [:parked, :stalled], :to => :idling
108
+ #
109
+ # transition :from => :parked
110
+ # transition :from => [:parked, :stalled]
111
+ # transition :except_from => :parked
112
+ #
113
+ # Notice that the above examples are the verbose equivalent of the examples
114
+ # described initially.
115
+ #
116
+ # == Conditions
117
+ #
118
+ # In addition to the state requirements for each transition, a condition
119
+ # can also be defined to help determine whether that transition is
120
+ # available. These options will work on both the normal and verbose syntax.
121
+ #
122
+ # Configuration options:
123
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
124
+ # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
125
+ # The condition should return or evaluate to true or false.
126
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
127
+ # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
128
+ # The condition should return or evaluate to true or false.
129
+ #
130
+ # Examples:
131
+ #
132
+ # transition :parked => :idling, :if => :moving?
133
+ # transition :parked => :idling, :unless => :stopped?
134
+ #
135
+ # transition :from => :parked, :to => :idling, :if => :moving?
136
+ # transition :from => :parked, :to => :idling, :unless => :stopped?
137
+ #
138
+ # == Order of operations
139
+ #
140
+ # Transitions are evaluated in the order in which they're defined. As a
141
+ # result, if more than one transition applies to a given object, then the
142
+ # first transition that matches will be performed.
143
+ def transition(options)
144
+ raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
145
+
146
+ # Only a certain subset of explicit options are allowed for transition
147
+ # requirements
148
+ assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
149
+
150
+ guards << guard = Guard.new(options)
151
+ @known_states |= guard.known_states
152
+ guard
153
+ end
154
+
155
+ # Determines whether any transitions can be performed for this event based
156
+ # on the current state of the given object.
157
+ #
158
+ # If the event can't be fired, then this will return false, otherwise true.
159
+ def can_fire?(object)
160
+ !transition_for(object).nil?
161
+ end
162
+
163
+ # Finds and builds the next transition that can be performed on the given
164
+ # object. If no transitions can be made, then this will return nil.
165
+ def transition_for(object)
166
+ from = machine.states.match(object).name
167
+
168
+ guards.each do |guard|
169
+ if match = guard.match(object, :from => from)
170
+ # Guard allows for the transition to occur
171
+ to = match[:to].values.empty? ? from : match[:to].values.first
172
+
173
+ return Transition.new(object, machine, name, from, to)
174
+ end
175
+ end
176
+
177
+ # No transition matched
178
+ nil
179
+ end
180
+
181
+ # Attempts to perform the next available transition on the given object.
182
+ # If no transitions can be made, then this will return false, otherwise
183
+ # true.
184
+ #
185
+ # Any additional arguments are passed to the StateMachine::Transition#perform
186
+ # instance method.
187
+ def fire(object, *args)
188
+ machine.reset(object)
189
+
190
+ if transition = transition_for(object)
191
+ transition.perform(*args)
192
+ else
193
+ machine.invalidate(object, machine.attribute, :invalid_transition, [[:event, name]])
194
+ false
195
+ end
196
+ end
197
+
198
+ # Draws a representation of this event on the given graph. This will
199
+ # create 1 or more edges on the graph for each guard (i.e. transition)
200
+ # configured.
201
+ #
202
+ # A collection of the generated edges will be returned.
203
+ def draw(graph)
204
+ valid_states = machine.states.by_priority.map {|state| state.name}
205
+ guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
206
+ end
207
+
208
+ # Generates a nicely formatted description of this event's contents.
209
+ #
210
+ # For example,
211
+ #
212
+ # event = StateMachine::Event.new(machine, :park)
213
+ # event.transition all - :idling => :parked, :idling => same
214
+ # event # => #<StateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
215
+ def inspect
216
+ transitions = guards.map do |guard|
217
+ guard.state_requirements.map do |state_requirement|
218
+ "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
219
+ end * ', '
220
+ end
221
+
222
+ "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
223
+ end
224
+
225
+ protected
226
+ # Add the various instance methods that can transition the object using
227
+ # the current event
228
+ def add_actions
229
+ # Checks whether the event can be fired on the current object
230
+ machine.define_instance_method("can_#{qualified_name}?") do |machine, object|
231
+ machine.event(name).can_fire?(object)
232
+ end
233
+
234
+ # Gets the next transition that would be performed if the event were
235
+ # fired now
236
+ machine.define_instance_method("#{qualified_name}_transition") do |machine, object|
237
+ machine.event(name).transition_for(object)
238
+ end
239
+
240
+ # Fires the event
241
+ machine.define_instance_method(qualified_name) do |machine, object, *args|
242
+ machine.event(name).fire(object, *args)
243
+ end
244
+
245
+ # Fires the event, raising an exception if it fails
246
+ machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
247
+ object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match(object).name.inspect}")
248
+ end
249
+ end
250
+ end
251
+ end