verborghs-state_machine 0.9.4

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 (89) hide show
  1. data/CHANGELOG.rdoc +360 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +635 -0
  4. data/Rakefile +77 -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/assertions.rb +36 -0
  28. data/lib/state_machine/callback.rb +241 -0
  29. data/lib/state_machine/condition_proxy.rb +106 -0
  30. data/lib/state_machine/eval_helpers.rb +83 -0
  31. data/lib/state_machine/event.rb +267 -0
  32. data/lib/state_machine/event_collection.rb +122 -0
  33. data/lib/state_machine/extensions.rb +149 -0
  34. data/lib/state_machine/guard.rb +230 -0
  35. data/lib/state_machine/initializers/merb.rb +1 -0
  36. data/lib/state_machine/initializers/rails.rb +5 -0
  37. data/lib/state_machine/initializers.rb +4 -0
  38. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  40. data/lib/state_machine/integrations/active_model.rb +445 -0
  41. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  42. data/lib/state_machine/integrations/active_record.rb +522 -0
  43. data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
  44. data/lib/state_machine/integrations/data_mapper.rb +379 -0
  45. data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
  46. data/lib/state_machine/integrations/sequel.rb +356 -0
  47. data/lib/state_machine/integrations.rb +83 -0
  48. data/lib/state_machine/machine.rb +1645 -0
  49. data/lib/state_machine/machine_collection.rb +64 -0
  50. data/lib/state_machine/matcher.rb +123 -0
  51. data/lib/state_machine/matcher_helpers.rb +54 -0
  52. data/lib/state_machine/node_collection.rb +152 -0
  53. data/lib/state_machine/state.rb +260 -0
  54. data/lib/state_machine/state_collection.rb +112 -0
  55. data/lib/state_machine/transition.rb +399 -0
  56. data/lib/state_machine/transition_collection.rb +244 -0
  57. data/lib/state_machine.rb +421 -0
  58. data/lib/tasks/state_machine.rake +1 -0
  59. data/lib/tasks/state_machine.rb +27 -0
  60. data/test/files/en.yml +9 -0
  61. data/test/files/switch.rb +11 -0
  62. data/test/functional/state_machine_test.rb +980 -0
  63. data/test/test_helper.rb +4 -0
  64. data/test/unit/assertions_test.rb +40 -0
  65. data/test/unit/callback_test.rb +728 -0
  66. data/test/unit/condition_proxy_test.rb +328 -0
  67. data/test/unit/eval_helpers_test.rb +222 -0
  68. data/test/unit/event_collection_test.rb +324 -0
  69. data/test/unit/event_test.rb +795 -0
  70. data/test/unit/guard_test.rb +909 -0
  71. data/test/unit/integrations/active_model_test.rb +956 -0
  72. data/test/unit/integrations/active_record_test.rb +1918 -0
  73. data/test/unit/integrations/data_mapper_test.rb +1814 -0
  74. data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
  75. data/test/unit/integrations/sequel_test.rb +1492 -0
  76. data/test/unit/integrations_test.rb +50 -0
  77. data/test/unit/invalid_event_test.rb +7 -0
  78. data/test/unit/invalid_transition_test.rb +7 -0
  79. data/test/unit/machine_collection_test.rb +565 -0
  80. data/test/unit/machine_test.rb +2349 -0
  81. data/test/unit/matcher_helpers_test.rb +37 -0
  82. data/test/unit/matcher_test.rb +155 -0
  83. data/test/unit/node_collection_test.rb +207 -0
  84. data/test/unit/state_collection_test.rb +280 -0
  85. data/test/unit/state_machine_test.rb +31 -0
  86. data/test/unit/state_test.rb +848 -0
  87. data/test/unit/transition_collection_test.rb +2098 -0
  88. data/test/unit/transition_test.rb +1384 -0
  89. metadata +176 -0
@@ -0,0 +1,106 @@
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
+ wrap_condition(*args, options)
70
+
71
+ # Evaluate the method on the original class with the condition proxied
72
+ # through
73
+ @klass.send(*args, &block)
74
+ end
75
+
76
+ protected
77
+ def wrap_condition(options)
78
+ if_condition = options.delete(:if)
79
+ unless_condition = options.delete(:unless)
80
+
81
+ # Provide scope access to configuration in case the block is evaluated
82
+ # within the object instance
83
+ proxy = self
84
+ proxy_condition = @condition
85
+
86
+ # Replace the configuration condition with the one configured for this
87
+ # proxy, merging together any existing conditions
88
+ options[:if] = lambda do |*args|
89
+ # Block may be executed within the context of the actual object, so
90
+ # it'll either be the first argument or the executing context
91
+ object = args.first || self
92
+
93
+ proxy.evaluate_method(object, proxy_condition) &&
94
+ Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
95
+ !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
96
+ end
97
+
98
+ # Needed for active record when presence => { :if => :something? }
99
+ options.each do |key, value|
100
+ if value.is_a?(Hash)
101
+ wrap_condition(value)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,83 @@
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, &block)
54
+ case method
55
+ when Symbol
56
+ object.method(method).arity == 0 ? object.send(method, &block) : object.send(method, *args, &block)
57
+ when Proc, Method
58
+ args.unshift(object)
59
+ arity = method.arity
60
+ limit = [0, 1].include?(arity) ? arity : args.length
61
+
62
+ # Procs don't support blocks in < Ruby 1.8.6, so it's tacked on as an
63
+ # argument for consistency across versions of Ruby (even though 1.9
64
+ # supports yielding within blocks)
65
+ if block_given? && Proc === method && arity != 0
66
+ if [1, 2].include?(arity)
67
+ limit = arity
68
+ args.insert(limit - 1, block)
69
+ else
70
+ limit += 1 unless limit < 0
71
+ args.push(block)
72
+ end
73
+ end
74
+
75
+ method.call(*args[0, limit], &block)
76
+ when String
77
+ eval(method, object.instance_eval {binding}, &block)
78
+ else
79
+ raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,267 @@
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 human-readable name for the event
28
+ attr_writer :human_name
29
+
30
+ # The list of guards that determine what state this event transitions
31
+ # objects to when fired
32
+ attr_reader :guards
33
+
34
+ # A list of all of the states known to this event using the configured
35
+ # guards/transitions as the source
36
+ attr_reader :known_states
37
+
38
+ # Creates a new event within the context of the given machine
39
+ #
40
+ # Configuration options:
41
+ # * <tt>:human_name</tt> - The human-readable version of this event's name
42
+ def initialize(machine, name, options = {}) #:nodoc:
43
+ assert_valid_keys(options, :human_name)
44
+
45
+ @machine = machine
46
+ @name = name
47
+ @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
48
+ @human_name = options[:human_name] || @name.to_s.tr('_', ' ')
49
+ @guards = []
50
+ @known_states = []
51
+
52
+ add_actions
53
+ end
54
+
55
+ # Creates a copy of this event in addition to the list of associated
56
+ # guards to prevent conflicts across events within a class hierarchy.
57
+ def initialize_copy(orig) #:nodoc:
58
+ super
59
+ @guards = @guards.dup
60
+ @known_states = @known_states.dup
61
+ end
62
+
63
+ # Transforms the event name into a more human-readable format, such as
64
+ # "turn on" instead of "turn_on"
65
+ def human_name(klass = @machine.owner_class)
66
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
67
+ end
68
+
69
+ # Creates a new transition that determines what to change the current state
70
+ # to when this event fires.
71
+ #
72
+ # == Defining transitions
73
+ #
74
+ # The options for a new transition uses the Hash syntax to map beginning
75
+ # states to ending states. For example,
76
+ #
77
+ # transition :parked => :idling, :idling => :first_gear
78
+ #
79
+ # In this case, when the event is fired, this transition will cause the
80
+ # state to be +idling+ if it's current state is +parked+ or +first_gear+
81
+ # if it's current state is +idling+.
82
+ #
83
+ # To help define these implicit transitions, a set of helpers are available
84
+ # for slightly more complex matching:
85
+ # * <tt>all</tt> - Matches every state in the machine
86
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
87
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
88
+ # * <tt>same</tt> - Matches the same state being transitioned from
89
+ #
90
+ # See StateMachine::MatcherHelpers for more information.
91
+ #
92
+ # Examples:
93
+ #
94
+ # transition all => nil # Transitions to nil regardless of the current state
95
+ # transition all => :idling # Transitions to :idling regardless of the current state
96
+ # transition all - [:idling, :first_gear] => :idling # Transitions every state but :idling and :first_gear to :idling
97
+ # transition nil => :idling # Transitions to :idling from the nil state
98
+ # transition :parked => :idling # Transitions to :idling if :parked
99
+ # transition [:parked, :stalled] => :idling # Transitions to :idling if :parked or :stalled
100
+ #
101
+ # transition :parked => same # Loops :parked back to :parked
102
+ # transition [:parked, :stalled] => same # Loops either :parked or :stalled back to the same state
103
+ # transition all - :parked => same # Loops every state but :parked back to the same state
104
+ #
105
+ # == Verbose transitions
106
+ #
107
+ # Transitions can also be defined use an explicit set of deprecated
108
+ # configuration options:
109
+ # * <tt>:from</tt> - A state or array of states that can be transitioned from.
110
+ # If not specified, then the transition can occur for *any* state.
111
+ # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
112
+ # then the transition will simply loop back (i.e. the state will not change).
113
+ # * <tt>:except_from</tt> - A state or array of states that *cannot* be
114
+ # transitioned from.
115
+ #
116
+ # Examples:
117
+ #
118
+ # transition :to => nil
119
+ # transition :to => :idling
120
+ # transition :except_from => [:idling, :first_gear], :to => :idling
121
+ # transition :from => nil, :to => :idling
122
+ # transition :from => [:parked, :stalled], :to => :idling
123
+ #
124
+ # transition :from => :parked
125
+ # transition :from => [:parked, :stalled]
126
+ # transition :except_from => :parked
127
+ #
128
+ # Notice that the above examples are the verbose equivalent of the examples
129
+ # described initially.
130
+ #
131
+ # == Conditions
132
+ #
133
+ # In addition to the state requirements for each transition, a condition
134
+ # can also be defined to help determine whether that transition is
135
+ # available. These options will work on both the normal and verbose syntax.
136
+ #
137
+ # Configuration options:
138
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
139
+ # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
140
+ # The condition should return or evaluate to true or false.
141
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
142
+ # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
143
+ # The condition should return or evaluate to true or false.
144
+ #
145
+ # Examples:
146
+ #
147
+ # transition :parked => :idling, :if => :moving?
148
+ # transition :parked => :idling, :unless => :stopped?
149
+ #
150
+ # transition :from => :parked, :to => :idling, :if => :moving?
151
+ # transition :from => :parked, :to => :idling, :unless => :stopped?
152
+ #
153
+ # == Order of operations
154
+ #
155
+ # Transitions are evaluated in the order in which they're defined. As a
156
+ # result, if more than one transition applies to a given object, then the
157
+ # first transition that matches will be performed.
158
+ def transition(options)
159
+ raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
160
+
161
+ # Only a certain subset of explicit options are allowed for transition
162
+ # requirements
163
+ 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?
164
+
165
+ guards << guard = Guard.new(options.merge(:on => name))
166
+ @known_states |= guard.known_states
167
+ guard
168
+ end
169
+
170
+ # Determines whether any transitions can be performed for this event based
171
+ # on the current state of the given object.
172
+ #
173
+ # If the event can't be fired, then this will return false, otherwise true.
174
+ def can_fire?(object)
175
+ !transition_for(object).nil?
176
+ end
177
+
178
+ # Finds and builds the next transition that can be performed on the given
179
+ # object. If no transitions can be made, then this will return nil.
180
+ def transition_for(object, requirements = {})
181
+ requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
182
+
183
+ guards.each do |guard|
184
+ if match = guard.match(object, requirements)
185
+ # Guard allows for the transition to occur
186
+ from = requirements[:from]
187
+ to = match[:to].values.empty? ? from : match[:to].values.first
188
+
189
+ return Transition.new(object, machine, name, from, to, !custom_from_state)
190
+ end
191
+ end
192
+
193
+ # No transition matched
194
+ nil
195
+ end
196
+
197
+ # Attempts to perform the next available transition on the given object.
198
+ # If no transitions can be made, then this will return false, otherwise
199
+ # true.
200
+ #
201
+ # Any additional arguments are passed to the StateMachine::Transition#perform
202
+ # instance method.
203
+ def fire(object, *args)
204
+ machine.reset(object)
205
+
206
+ if transition = transition_for(object)
207
+ transition.perform(*args)
208
+ else
209
+ machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)]])
210
+ false
211
+ end
212
+ end
213
+
214
+ # Draws a representation of this event on the given graph. This will
215
+ # create 1 or more edges on the graph for each guard (i.e. transition)
216
+ # configured.
217
+ #
218
+ # A collection of the generated edges will be returned.
219
+ def draw(graph)
220
+ valid_states = machine.states.by_priority.map {|state| state.name}
221
+ guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
222
+ end
223
+
224
+ # Generates a nicely formatted description of this event's contents.
225
+ #
226
+ # For example,
227
+ #
228
+ # event = StateMachine::Event.new(machine, :park)
229
+ # event.transition all - :idling => :parked, :idling => same
230
+ # event # => #<StateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
231
+ def inspect
232
+ transitions = guards.map do |guard|
233
+ guard.state_requirements.map do |state_requirement|
234
+ "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
235
+ end * ', '
236
+ end
237
+
238
+ "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
239
+ end
240
+
241
+ protected
242
+ # Add the various instance methods that can transition the object using
243
+ # the current event
244
+ def add_actions
245
+ # Checks whether the event can be fired on the current object
246
+ machine.define_instance_method("can_#{qualified_name}?") do |machine, object|
247
+ machine.event(name).can_fire?(object)
248
+ end
249
+
250
+ # Gets the next transition that would be performed if the event were
251
+ # fired now
252
+ machine.define_instance_method("#{qualified_name}_transition") do |machine, object|
253
+ machine.event(name).transition_for(object)
254
+ end
255
+
256
+ # Fires the event
257
+ machine.define_instance_method(qualified_name) do |machine, object, *args|
258
+ machine.event(name).fire(object, *args)
259
+ end
260
+
261
+ # Fires the event, raising an exception if it fails
262
+ machine.define_instance_method("#{qualified_name}!") do |machine, object, *args|
263
+ object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.name} via :#{name} from #{machine.states.match!(object).name.inspect}")
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,122 @@
1
+ module StateMachine
2
+ # Represents a collection of events in a state machine
3
+ class EventCollection < NodeCollection
4
+ def initialize(machine) #:nodoc:
5
+ super(machine, :index => [:name, :qualified_name])
6
+ end
7
+
8
+ # Gets the list of events that can be fired on the given object.
9
+ #
10
+ # == Examples
11
+ #
12
+ # class Vehicle
13
+ # state_machine :initial => :parked do
14
+ # event :park do
15
+ # transition :idling => :parked
16
+ # end
17
+ #
18
+ # event :ignite do
19
+ # transition :parked => :idling
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # events = Vehicle.state_machine(:state).events
25
+ #
26
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
27
+ # events.valid_for(vehicle) # => [#<StateMachine::Event name=:ignite transitions=[:parked => :idling]>]
28
+ #
29
+ # vehicle.state = 'idling'
30
+ # events.valid_for(vehicle) # => [#<StateMachine::Event name=:park transitions=[:idling => :parked]>]
31
+ def valid_for(object)
32
+ select {|event| event.can_fire?(object)}
33
+ end
34
+
35
+ # Gets the list of transitions that can be run on the given object.
36
+ #
37
+ # Valid requirement options:
38
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
39
+ # are specified, then this will be the object's current state.
40
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
41
+ # specified, then this will match any to state.
42
+ # * <tt>:on</tt> - One or more events that fire the transition. If none
43
+ # are specified, then this will match any event.
44
+ #
45
+ # == Examples
46
+ #
47
+ # class Vehicle
48
+ # state_machine :initial => :parked do
49
+ # event :park do
50
+ # transition :idling => :parked
51
+ # end
52
+ #
53
+ # event :ignite do
54
+ # transition :parked => :idling
55
+ # end
56
+ # end
57
+ # end
58
+ #
59
+ # events = Vehicle.state_machine.events
60
+ #
61
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
62
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
63
+ #
64
+ # vehicle.state = 'idling'
65
+ # events.transitions_for(vehicle) # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
66
+ #
67
+ # # Search for explicit transitions regardless of the current state
68
+ # events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
69
+ def transitions_for(object, requirements = {})
70
+ map {|event| event.transition_for(object, requirements)}.compact
71
+ end
72
+
73
+ # Gets the transition that should be performed for the event stored in the
74
+ # given object's event attribute. This also takes an additional parameter
75
+ # for automatically invalidating the object if the event or transition are
76
+ # invalid. By default, this is turned off.
77
+ #
78
+ # *Note* that if a transition has already been generated for the event, then
79
+ # that transition will be used.
80
+ #
81
+ # == Examples
82
+ #
83
+ # class Vehicle < ActiveRecord::Base
84
+ # state_machine :initial => :parked do
85
+ # event :ignite do
86
+ # transition :parked => :idling
87
+ # end
88
+ # end
89
+ # end
90
+ #
91
+ # vehicle = Vehicle.new # => #<Vehicle id: nil, state: "parked">
92
+ # events = Vehicle.state_machine.events
93
+ #
94
+ # vehicle.state_event = nil
95
+ # events.attribute_transition_for(vehicle) # => nil # Event isn't defined
96
+ #
97
+ # vehicle.state_event = 'invalid'
98
+ # events.attribute_transition_for(vehicle) # => false # Event is invalid
99
+ #
100
+ # vehicle.state_event = 'ignite'
101
+ # events.attribute_transition_for(vehicle) # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
102
+ def attribute_transition_for(object, invalidate = false)
103
+ return unless machine.action
104
+
105
+ result = machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
106
+ if event = self[event_name.to_sym, :name]
107
+ event.transition_for(object) || begin
108
+ # No valid transition: invalidate
109
+ machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
110
+ false
111
+ end
112
+ else
113
+ # Event is unknown: invalidate
114
+ machine.invalidate(object, :event, :invalid) if invalidate
115
+ false
116
+ end
117
+ end
118
+
119
+ result
120
+ end
121
+ end
122
+ end