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.
- data/CHANGELOG.rdoc +273 -0
- data/LICENSE +20 -0
- data/README.rdoc +466 -0
- data/Rakefile +98 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +429 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +189 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +251 -0
- data/lib/state_machine/event_collection.rb +113 -0
- data/lib/state_machine/extensions.rb +158 -0
- data/lib/state_machine/guard.rb +219 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +444 -0
- data/lib/state_machine/integrations/active_record/locale.rb +10 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +325 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +292 -0
- data/lib/state_machine/machine.rb +1431 -0
- data/lib/state_machine/machine_collection.rb +146 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +249 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +367 -0
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +30 -0
- data/test/classes/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +941 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +455 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +129 -0
- data/test/unit/event_collection_test.rb +293 -0
- data/test/unit/event_test.rb +605 -0
- data/test/unit/guard_test.rb +862 -0
- data/test/unit/integrations/active_record_test.rb +1001 -0
- data/test/unit/integrations/data_mapper_test.rb +694 -0
- data/test/unit/integrations/sequel_test.rb +486 -0
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +710 -0
- data/test/unit/machine_test.rb +1910 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +795 -0
- data/test/unit/transition_test.rb +1113 -0
- 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
|