state_machines 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.idea/.name +1 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/cssxfire.xml +9 -0
- data/.idea/encodings.xml +5 -0
- data/.idea/misc.xml +5 -0
- data/.idea/modules.xml +12 -0
- data/.idea/scopes/scope_settings.xml +5 -0
- data/.idea/state_machine2.iml +34 -0
- data/.idea/vcs.xml +9 -0
- data/.idea/workspace.xml +1156 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/state_machines/assertions.rb +40 -0
- data/lib/state_machines/branch.rb +187 -0
- data/lib/state_machines/callback.rb +220 -0
- data/lib/state_machines/core.rb +25 -0
- data/lib/state_machines/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machines/core_ext.rb +2 -0
- data/lib/state_machines/error.rb +13 -0
- data/lib/state_machines/eval_helpers.rb +87 -0
- data/lib/state_machines/event.rb +246 -0
- data/lib/state_machines/event_collection.rb +141 -0
- data/lib/state_machines/extensions.rb +148 -0
- data/lib/state_machines/helper_module.rb +17 -0
- data/lib/state_machines/integrations/base.rb +100 -0
- data/lib/state_machines/integrations.rb +113 -0
- data/lib/state_machines/machine.rb +2234 -0
- data/lib/state_machines/machine_collection.rb +84 -0
- data/lib/state_machines/macro_methods.rb +520 -0
- data/lib/state_machines/matcher.rb +123 -0
- data/lib/state_machines/matcher_helpers.rb +54 -0
- data/lib/state_machines/node_collection.rb +221 -0
- data/lib/state_machines/path.rb +120 -0
- data/lib/state_machines/path_collection.rb +90 -0
- data/lib/state_machines/state.rb +276 -0
- data/lib/state_machines/state_collection.rb +112 -0
- data/lib/state_machines/state_context.rb +138 -0
- data/lib/state_machines/transition.rb +470 -0
- data/lib/state_machines/transition_collection.rb +245 -0
- data/lib/state_machines/version.rb +3 -0
- data/lib/state_machines/yard.rb +8 -0
- data/lib/state_machines.rb +3 -0
- data/spec/errors/default_spec.rb +14 -0
- data/spec/errors/with_message_spec.rb +39 -0
- data/spec/helpers/helper_spec.rb +14 -0
- data/spec/internal/app/models/auto_shop.rb +31 -0
- data/spec/internal/app/models/car.rb +19 -0
- data/spec/internal/app/models/model_base.rb +6 -0
- data/spec/internal/app/models/motorcycle.rb +9 -0
- data/spec/internal/app/models/traffic_light.rb +47 -0
- data/spec/internal/app/models/vehicle.rb +123 -0
- data/spec/machine_spec.rb +3167 -0
- data/spec/matcher_helpers_spec.rb +39 -0
- data/spec/matcher_spec.rb +157 -0
- data/spec/models/auto_shop_spec.rb +41 -0
- data/spec/models/car_spec.rb +90 -0
- data/spec/models/motorcycle_spec.rb +44 -0
- data/spec/models/traffic_light_spec.rb +56 -0
- data/spec/models/vehicle_spec.rb +580 -0
- data/spec/node_collection_spec.rb +371 -0
- data/spec/path_collection_spec.rb +271 -0
- data/spec/path_spec.rb +488 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/state_collection_spec.rb +352 -0
- data/spec/state_context_spec.rb +442 -0
- data/spec/state_machine_spec.rb +29 -0
- data/spec/state_spec.rb +970 -0
- data/spec/support/migration_helpers.rb +50 -0
- data/spec/support/models.rb +6 -0
- data/spec/transition_collection_spec.rb +2199 -0
- data/spec/transition_spec.rb +1558 -0
- data/state_machines.gemspec +23 -0
- metadata +194 -0
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'state_machines/transition'
|
2
|
+
require 'state_machines/branch'
|
3
|
+
require 'state_machines/assertions'
|
4
|
+
require 'state_machines/matcher_helpers'
|
5
|
+
require 'state_machines/error'
|
6
|
+
|
7
|
+
module StateMachines
|
8
|
+
# An invalid event was specified
|
9
|
+
class InvalidEvent < Error
|
10
|
+
# The event that was attempted to be run
|
11
|
+
attr_reader :event
|
12
|
+
|
13
|
+
def initialize(object, event_name) #:nodoc:
|
14
|
+
@event = event_name
|
15
|
+
|
16
|
+
super(object, "#{event.inspect} is an unknown state machine event")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# An event defines an action that transitions an attribute from one state to
|
21
|
+
# another. The state that an attribute is transitioned to depends on the
|
22
|
+
# branches configured for the event.
|
23
|
+
class Event
|
24
|
+
|
25
|
+
include MatcherHelpers
|
26
|
+
|
27
|
+
# The state machine for which this event is defined
|
28
|
+
attr_accessor :machine
|
29
|
+
|
30
|
+
# The name of the event
|
31
|
+
attr_reader :name
|
32
|
+
|
33
|
+
# The fully-qualified name of the event, scoped by the machine's namespace
|
34
|
+
attr_reader :qualified_name
|
35
|
+
|
36
|
+
# The human-readable name for the event
|
37
|
+
attr_writer :human_name
|
38
|
+
|
39
|
+
# The list of branches that determine what state this event transitions
|
40
|
+
# objects to when fired
|
41
|
+
attr_reader :branches
|
42
|
+
|
43
|
+
# A list of all of the states known to this event using the configured
|
44
|
+
# branches/transitions as the source
|
45
|
+
attr_reader :known_states
|
46
|
+
|
47
|
+
# Creates a new event within the context of the given machine
|
48
|
+
#
|
49
|
+
# Configuration options:
|
50
|
+
# * <tt>:human_name</tt> - The human-readable version of this event's name
|
51
|
+
def initialize(machine, name, options = {}) #:nodoc:
|
52
|
+
options.assert_valid_keys(:human_name)
|
53
|
+
|
54
|
+
@machine = machine
|
55
|
+
@name = name
|
56
|
+
@qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
|
57
|
+
@human_name = options[:human_name] || @name.to_s.tr('_', ' ')
|
58
|
+
reset
|
59
|
+
|
60
|
+
# Output a warning if another event has a conflicting qualified name
|
61
|
+
if conflict = machine.owner_class.state_machines.detect { |other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] }
|
62
|
+
name, other_machine = conflict
|
63
|
+
warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
|
64
|
+
else
|
65
|
+
add_actions
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Creates a copy of this event in addition to the list of associated
|
70
|
+
# branches to prevent conflicts across events within a class hierarchy.
|
71
|
+
def initialize_copy(orig) #:nodoc:
|
72
|
+
super
|
73
|
+
@branches = @branches.dup
|
74
|
+
@known_states = @known_states.dup
|
75
|
+
end
|
76
|
+
|
77
|
+
# Transforms the event name into a more human-readable format, such as
|
78
|
+
# "turn on" instead of "turn_on"
|
79
|
+
def human_name(klass = @machine.owner_class)
|
80
|
+
@human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
|
81
|
+
end
|
82
|
+
|
83
|
+
# Evaluates the given block within the context of this event. This simply
|
84
|
+
# provides a DSL-like syntax for defining transitions.
|
85
|
+
def context(&block)
|
86
|
+
instance_eval(&block)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates a new transition that determines what to change the current state
|
90
|
+
# to when this event fires.
|
91
|
+
#
|
92
|
+
# Since this transition is being defined within an event context, you do
|
93
|
+
# *not* need to specify the <tt>:on</tt> option for the transition. For
|
94
|
+
# example:
|
95
|
+
#
|
96
|
+
# state_machine do
|
97
|
+
# event :ignite do
|
98
|
+
# transition :parked => :idling, :idling => same, :if => :seatbelt_on? # Transitions to :idling if seatbelt is on
|
99
|
+
# transition all => :parked, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# See StateMachines::Machine#transition for a description of the possible
|
104
|
+
# configurations for defining transitions.
|
105
|
+
def transition(options)
|
106
|
+
raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
|
107
|
+
|
108
|
+
# Only a certain subset of explicit options are allowed for transition
|
109
|
+
# requirements
|
110
|
+
options.assert_valid_keys(:from, :to, :except_from, :except_to, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
|
111
|
+
|
112
|
+
branches << branch = Branch.new(options.merge(:on => name))
|
113
|
+
@known_states |= branch.known_states
|
114
|
+
branch
|
115
|
+
end
|
116
|
+
|
117
|
+
# Determines whether any transitions can be performed for this event based
|
118
|
+
# on the current state of the given object.
|
119
|
+
#
|
120
|
+
# If the event can't be fired, then this will return false, otherwise true.
|
121
|
+
#
|
122
|
+
# *Note* that this will not take the object context into account. Although
|
123
|
+
# a transition may be possible based on the state machine definition,
|
124
|
+
# object-specific behaviors (like validations) may prevent it from firing.
|
125
|
+
def can_fire?(object, requirements = {})
|
126
|
+
!transition_for(object, requirements).nil?
|
127
|
+
end
|
128
|
+
|
129
|
+
# Finds and builds the next transition that can be performed on the given
|
130
|
+
# object. If no transitions can be made, then this will return nil.
|
131
|
+
#
|
132
|
+
# Valid requirement options:
|
133
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
134
|
+
# are specified, then this will be the object's current state.
|
135
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
136
|
+
# specified, then this will match any to state.
|
137
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
138
|
+
# conditionals defined for each one. Default is true.
|
139
|
+
def transition_for(object, requirements = {})
|
140
|
+
requirements.assert_valid_keys(:from, :to, :guard)
|
141
|
+
requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
|
142
|
+
|
143
|
+
branches.each do |branch|
|
144
|
+
if match = branch.match(object, requirements)
|
145
|
+
# Branch allows for the transition to occur
|
146
|
+
from = requirements[:from]
|
147
|
+
to = if match[:to].is_a?(LoopbackMatcher)
|
148
|
+
from
|
149
|
+
else
|
150
|
+
values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }
|
151
|
+
|
152
|
+
match[:to].filter(values).first
|
153
|
+
end
|
154
|
+
|
155
|
+
return Transition.new(object, machine, name, from, to, !custom_from_state)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# No transition matched
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# Attempts to perform the next available transition on the given object.
|
164
|
+
# If no transitions can be made, then this will return false, otherwise
|
165
|
+
# true.
|
166
|
+
#
|
167
|
+
# Any additional arguments are passed to the StateMachines::Transition#perform
|
168
|
+
# instance method.
|
169
|
+
def fire(object, *args)
|
170
|
+
machine.reset(object)
|
171
|
+
|
172
|
+
if transition = transition_for(object)
|
173
|
+
transition.perform(*args)
|
174
|
+
else
|
175
|
+
on_failure(object)
|
176
|
+
false
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Marks the object as invalid and runs any failure callbacks associated with
|
181
|
+
# this event. This should get called anytime this event fails to transition.
|
182
|
+
def on_failure(object)
|
183
|
+
state = machine.states.match!(object)
|
184
|
+
machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)], [:state, state.human_name(object.class)]])
|
185
|
+
|
186
|
+
Transition.new(object, machine, name, state.name, state.name).run_callbacks(:before => false)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Resets back to the initial state of the event, with no branches / known
|
190
|
+
# states associated. This allows you to redefine an event in situations
|
191
|
+
# where you either are re-using an existing state machine implementation
|
192
|
+
# or are subclassing machines.
|
193
|
+
def reset
|
194
|
+
@branches = []
|
195
|
+
@known_states = []
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
def draw(graph, options = {})
|
200
|
+
fail NotImplementedError
|
201
|
+
end
|
202
|
+
|
203
|
+
# Generates a nicely formatted description of this event's contents.
|
204
|
+
#
|
205
|
+
# For example,
|
206
|
+
#
|
207
|
+
# event = StateMachines::Event.new(machine, :park)
|
208
|
+
# event.transition all - :idling => :parked, :idling => same
|
209
|
+
# event # => #<StateMachines::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
|
210
|
+
def inspect
|
211
|
+
transitions = branches.map do |branch|
|
212
|
+
branch.state_requirements.map do |state_requirement|
|
213
|
+
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
|
214
|
+
end * ', '
|
215
|
+
end
|
216
|
+
|
217
|
+
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
|
218
|
+
end
|
219
|
+
|
220
|
+
protected
|
221
|
+
# Add the various instance methods that can transition the object using
|
222
|
+
# the current event
|
223
|
+
def add_actions
|
224
|
+
# Checks whether the event can be fired on the current object
|
225
|
+
machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, *args|
|
226
|
+
machine.event(name).can_fire?(object, *args)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Gets the next transition that would be performed if the event were
|
230
|
+
# fired now
|
231
|
+
machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, *args|
|
232
|
+
machine.event(name).transition_for(object, *args)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Fires the event
|
236
|
+
machine.define_helper(:instance, qualified_name) do |machine, object, *args|
|
237
|
+
machine.event(name).fire(object, *args)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Fires the event, raising an exception if it fails
|
241
|
+
machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args|
|
242
|
+
object.send(qualified_name, *args) || raise(StateMachines::InvalidTransition.new(object, machine, name))
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'state_machines/node_collection'
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
# Represents a collection of events in a state machine
|
5
|
+
class EventCollection < NodeCollection
|
6
|
+
def initialize(machine) #:nodoc:
|
7
|
+
super(machine, :index => [:name, :qualified_name])
|
8
|
+
end
|
9
|
+
|
10
|
+
# Gets the list of events that can be fired on the given object.
|
11
|
+
#
|
12
|
+
# Valid requirement options:
|
13
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
14
|
+
# are specified, then this will be the object's current state.
|
15
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
16
|
+
# specified, then this will match any to state.
|
17
|
+
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
18
|
+
# are specified, then this will match any event.
|
19
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
20
|
+
# conditionals defined for each one. Default is true.
|
21
|
+
#
|
22
|
+
# == Examples
|
23
|
+
#
|
24
|
+
# class Vehicle
|
25
|
+
# state_machine :initial => :parked do
|
26
|
+
# event :park do
|
27
|
+
# transition :idling => :parked
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# event :ignite do
|
31
|
+
# transition :parked => :idling
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# events = Vehicle.state_machine(:state).events
|
37
|
+
#
|
38
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
39
|
+
# events.valid_for(vehicle) # => [#<StateMachines::Event name=:ignite transitions=[:parked => :idling]>]
|
40
|
+
#
|
41
|
+
# vehicle.state = 'idling'
|
42
|
+
# events.valid_for(vehicle) # => [#<StateMachines::Event name=:park transitions=[:idling => :parked]>]
|
43
|
+
def valid_for(object, requirements = {})
|
44
|
+
match(requirements).select {|event| event.can_fire?(object, requirements)}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Gets the list of transitions that can be run on the given object.
|
48
|
+
#
|
49
|
+
# Valid requirement options:
|
50
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
51
|
+
# are specified, then this will be the object's current state.
|
52
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
53
|
+
# specified, then this will match any to state.
|
54
|
+
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
55
|
+
# are specified, then this will match any event.
|
56
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
57
|
+
# conditionals defined for each one. Default is true.
|
58
|
+
#
|
59
|
+
# == Examples
|
60
|
+
#
|
61
|
+
# class Vehicle
|
62
|
+
# state_machine :initial => :parked do
|
63
|
+
# event :park do
|
64
|
+
# transition :idling => :parked
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# event :ignite do
|
68
|
+
# transition :parked => :idling
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# events = Vehicle.state_machine.events
|
74
|
+
#
|
75
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
76
|
+
# events.transitions_for(vehicle) # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
77
|
+
#
|
78
|
+
# vehicle.state = 'idling'
|
79
|
+
# events.transitions_for(vehicle) # => [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
|
80
|
+
#
|
81
|
+
# # Search for explicit transitions regardless of the current state
|
82
|
+
# events.transitions_for(vehicle, :from => :parked) # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
83
|
+
def transitions_for(object, requirements = {})
|
84
|
+
match(requirements).map {|event| event.transition_for(object, requirements)}.compact
|
85
|
+
end
|
86
|
+
|
87
|
+
# Gets the transition that should be performed for the event stored in the
|
88
|
+
# given object's event attribute. This also takes an additional parameter
|
89
|
+
# for automatically invalidating the object if the event or transition are
|
90
|
+
# invalid. By default, this is turned off.
|
91
|
+
#
|
92
|
+
# *Note* that if a transition has already been generated for the event, then
|
93
|
+
# that transition will be used.
|
94
|
+
#
|
95
|
+
# == Examples
|
96
|
+
#
|
97
|
+
# class Vehicle < ActiveRecord::Base
|
98
|
+
# state_machine :initial => :parked do
|
99
|
+
# event :ignite do
|
100
|
+
# transition :parked => :idling
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# vehicle = Vehicle.new # => #<Vehicle id: nil, state: "parked">
|
106
|
+
# events = Vehicle.state_machine.events
|
107
|
+
#
|
108
|
+
# vehicle.state_event = nil
|
109
|
+
# events.attribute_transition_for(vehicle) # => nil # Event isn't defined
|
110
|
+
#
|
111
|
+
# vehicle.state_event = 'invalid'
|
112
|
+
# events.attribute_transition_for(vehicle) # => false # Event is invalid
|
113
|
+
#
|
114
|
+
# vehicle.state_event = 'ignite'
|
115
|
+
# events.attribute_transition_for(vehicle) # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
116
|
+
def attribute_transition_for(object, invalidate = false)
|
117
|
+
return unless machine.action
|
118
|
+
|
119
|
+
result = machine.read(object, :event_transition) || if event_name = machine.read(object, :event)
|
120
|
+
if event = self[event_name.to_sym, :name]
|
121
|
+
event.transition_for(object) || begin
|
122
|
+
# No valid transition: invalidate
|
123
|
+
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
|
124
|
+
false
|
125
|
+
end
|
126
|
+
else
|
127
|
+
# Event is unknown: invalidate
|
128
|
+
machine.invalidate(object, :event, :invalid) if invalidate
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
def match(requirements) #:nodoc:
|
138
|
+
requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
|
2
|
+
module StateMachines
|
3
|
+
module ClassMethods
|
4
|
+
def self.extended(base) #:nodoc:
|
5
|
+
base.class_eval do
|
6
|
+
@state_machines = MachineCollection.new
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Gets the current list of state machines defined for this class. This
|
11
|
+
# class-level attribute acts like an inheritable attribute. The attribute
|
12
|
+
# is available to each subclass, each having a copy of its superclass's
|
13
|
+
# attribute.
|
14
|
+
#
|
15
|
+
# The hash of state machines maps <tt>:attribute</tt> => +machine+, e.g.
|
16
|
+
#
|
17
|
+
# Vehicle.state_machines # => {:state => #<StateMachines::Machine:0xb6f6e4a4 ...>}
|
18
|
+
def state_machines
|
19
|
+
@state_machines ||= superclass.state_machines.dup
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
# Runs one or more events in parallel. All events will run through the
|
25
|
+
# following steps:
|
26
|
+
# * Before callbacks
|
27
|
+
# * Persist state
|
28
|
+
# * Invoke action
|
29
|
+
# * After callbacks
|
30
|
+
#
|
31
|
+
# For example, if two events (for state machines A and B) are run in
|
32
|
+
# parallel, the order in which steps are run is:
|
33
|
+
# * A - Before transition callbacks
|
34
|
+
# * B - Before transition callbacks
|
35
|
+
# * A - Persist new state
|
36
|
+
# * B - Persist new state
|
37
|
+
# * A - Invoke action
|
38
|
+
# * B - Invoke action (only if different than A's action)
|
39
|
+
# * A - After transition callbacks
|
40
|
+
# * B - After transition callbacks
|
41
|
+
#
|
42
|
+
# *Note* that multiple events on the same state machine / attribute cannot
|
43
|
+
# be run in parallel. If this is attempted, an ArgumentError will be
|
44
|
+
# raised.
|
45
|
+
#
|
46
|
+
# == Halting callbacks
|
47
|
+
#
|
48
|
+
# When running multiple events in parallel, special consideration should
|
49
|
+
# be taken with regard to how halting within callbacks affects the flow.
|
50
|
+
#
|
51
|
+
# For *before* callbacks, any <tt>:halt</tt> error that's thrown will
|
52
|
+
# immediately cancel the perform for all transitions. As a result, it's
|
53
|
+
# possible for one event's transition to affect the continuation of
|
54
|
+
# another.
|
55
|
+
#
|
56
|
+
# On the other hand, any <tt>:halt</tt> error that's thrown within an
|
57
|
+
# *after* callback with only affect that event's transition. Other
|
58
|
+
# transitions will continue to run their own callbacks.
|
59
|
+
#
|
60
|
+
# == Example
|
61
|
+
#
|
62
|
+
# class Vehicle
|
63
|
+
# state_machine :initial => :parked do
|
64
|
+
# event :ignite do
|
65
|
+
# transition :parked => :idling
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# event :park do
|
69
|
+
# transition :idling => :parked
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
|
74
|
+
# event :enable do
|
75
|
+
# transition all => :active
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# event :disable do
|
79
|
+
# transition all => :off
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
85
|
+
# vehicle.state # => "parked"
|
86
|
+
# vehicle.alarm_state # => "active"
|
87
|
+
#
|
88
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
89
|
+
# vehicle.state # => "idling"
|
90
|
+
# vehicle.alarm_state # => "off"
|
91
|
+
#
|
92
|
+
# # If any event fails, the entire event chain fails
|
93
|
+
# vehicle.fire_events(:ignite, :enable_alarm) # => false
|
94
|
+
# vehicle.state # => "idling"
|
95
|
+
# vehicle.alarm_state # => "off"
|
96
|
+
#
|
97
|
+
# # Exception raised on invalid event
|
98
|
+
# vehicle.fire_events(:park, :invalid) # => StateMachines::InvalidEvent: :invalid is an unknown event
|
99
|
+
# vehicle.state # => "idling"
|
100
|
+
# vehicle.alarm_state # => "off"
|
101
|
+
def fire_events(*events)
|
102
|
+
self.class.state_machines.fire_events(self, *events)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Run one or more events in parallel. If any event fails to run, then
|
106
|
+
# a StateMachines::InvalidTransition exception will be raised.
|
107
|
+
#
|
108
|
+
# See StateMachines::InstanceMethods#fire_events for more information.
|
109
|
+
#
|
110
|
+
# == Example
|
111
|
+
#
|
112
|
+
# class Vehicle
|
113
|
+
# state_machine :initial => :parked do
|
114
|
+
# event :ignite do
|
115
|
+
# transition :parked => :idling
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# event :park do
|
119
|
+
# transition :idling => :parked
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
|
124
|
+
# event :enable do
|
125
|
+
# transition all => :active
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# event :disable do
|
129
|
+
# transition all => :off
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
135
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
136
|
+
#
|
137
|
+
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
|
138
|
+
def fire_events!(*events)
|
139
|
+
run_action = [true, false].include?(events.last) ? events.pop : true
|
140
|
+
fire_events(*(events + [run_action])) || raise(StateMachines::InvalidParallelTransition.new(self, events))
|
141
|
+
end
|
142
|
+
|
143
|
+
protected
|
144
|
+
def initialize_state_machines(options = {}, &block) #:nodoc:
|
145
|
+
self.class.state_machines.initialize_states(self, options, &block)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module StateMachines
|
2
|
+
# Represents a type of module that defines instance / class methods for a
|
3
|
+
# state machine
|
4
|
+
class HelperModule < Module #:nodoc:
|
5
|
+
def initialize(machine, kind)
|
6
|
+
@machine = machine
|
7
|
+
@kind = kind
|
8
|
+
end
|
9
|
+
|
10
|
+
# Provides a human-readable description of the module
|
11
|
+
def to_s
|
12
|
+
owner_class = @machine.owner_class
|
13
|
+
owner_class_name = owner_class.name && !owner_class.name.empty? ? owner_class.name : owner_class.to_s
|
14
|
+
"#{owner_class_name} #{@machine.name.inspect} #{@kind} helpers"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module StateMachines
|
2
|
+
module Integrations
|
3
|
+
# Provides a set of base helpers for managing individual integrations
|
4
|
+
module Base
|
5
|
+
module ClassMethods
|
6
|
+
# The default options to use for state machines using this integration
|
7
|
+
attr_reader :defaults
|
8
|
+
|
9
|
+
# The name of the integration
|
10
|
+
def integration_name
|
11
|
+
@integration_name ||= begin
|
12
|
+
name = self.name.split('::').last
|
13
|
+
name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
14
|
+
name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
15
|
+
name.downcase!
|
16
|
+
name.to_sym
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Whether this integration is available for the current library. This
|
21
|
+
# is only true if the ORM that the integration is for is currently
|
22
|
+
# defined.
|
23
|
+
def available?
|
24
|
+
matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
|
25
|
+
end
|
26
|
+
|
27
|
+
# The list of ancestor names that cause this integration to matched.
|
28
|
+
def matching_ancestors
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Whether the integration should be used for the given class.
|
33
|
+
def matches?(klass)
|
34
|
+
matches_ancestors?(klass.ancestors.map {|ancestor| ancestor.name})
|
35
|
+
end
|
36
|
+
|
37
|
+
# Whether the integration should be used for the given list of ancestors.
|
38
|
+
def matches_ancestors?(ancestors)
|
39
|
+
(ancestors & matching_ancestors).any?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Tracks the various version overrides for an integration
|
43
|
+
def versions
|
44
|
+
@versions ||= []
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates a new version override for an integration. When this
|
48
|
+
# integration is activated, each version that is marked as active will
|
49
|
+
# also extend the integration.
|
50
|
+
#
|
51
|
+
# == Example
|
52
|
+
#
|
53
|
+
# module StateMachines
|
54
|
+
# module Integrations
|
55
|
+
# module ORMLibrary
|
56
|
+
# version '0.2.x - 0.3.x' do
|
57
|
+
# def self.active?
|
58
|
+
# ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# def invalidate(object, attribute, message, values = [])
|
62
|
+
# # Override here...
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# In the above example, a version override is defined for the ORMLibrary
|
70
|
+
# integration when the version is between 0.2.x and 0.3.x.
|
71
|
+
def version(name, &block)
|
72
|
+
versions << mod = Module.new(&block)
|
73
|
+
mod
|
74
|
+
end
|
75
|
+
|
76
|
+
# The path to the locale file containing translations for this
|
77
|
+
# integration. This file will only exist for integrations that actually
|
78
|
+
# support i18n.
|
79
|
+
def locale_path
|
80
|
+
path = "#{File.dirname(__FILE__)}/#{integration_name}/locale.rb"
|
81
|
+
path if File.exist?(path)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Extends the given object with any version overrides that are currently
|
85
|
+
# active
|
86
|
+
def extended(base)
|
87
|
+
versions.each do |version|
|
88
|
+
base.extend(version) if version.active?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
extend ClassMethods
|
94
|
+
|
95
|
+
def self.included(base) #:nodoc:
|
96
|
+
base.class_eval { extend ClassMethods }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|