enum_state_machine 0.0.1 → 0.0.2
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 +4 -4
- data/.gitignore +12 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- metadata +83 -130
- data/.rvmrc +0 -1
- data/enum_state_machine.gemspec +0 -25
- data/lib/enum_state_machine.rb +0 -9
- data/lib/enum_state_machine/assertions.rb +0 -36
- data/lib/enum_state_machine/branch.rb +0 -225
- data/lib/enum_state_machine/callback.rb +0 -232
- data/lib/enum_state_machine/core.rb +0 -12
- data/lib/enum_state_machine/core_ext.rb +0 -2
- data/lib/enum_state_machine/core_ext/class/state_machine.rb +0 -5
- data/lib/enum_state_machine/error.rb +0 -13
- data/lib/enum_state_machine/eval_helpers.rb +0 -87
- data/lib/enum_state_machine/event.rb +0 -257
- data/lib/enum_state_machine/event_collection.rb +0 -141
- data/lib/enum_state_machine/extensions.rb +0 -149
- data/lib/enum_state_machine/graph.rb +0 -92
- data/lib/enum_state_machine/helper_module.rb +0 -17
- data/lib/enum_state_machine/initializers.rb +0 -4
- data/lib/enum_state_machine/initializers/rails.rb +0 -22
- data/lib/enum_state_machine/integrations.rb +0 -97
- data/lib/enum_state_machine/integrations/active_model.rb +0 -585
- data/lib/enum_state_machine/integrations/active_model/locale.rb +0 -11
- data/lib/enum_state_machine/integrations/active_model/observer.rb +0 -33
- data/lib/enum_state_machine/integrations/active_model/observer_update.rb +0 -42
- data/lib/enum_state_machine/integrations/active_model/versions.rb +0 -31
- data/lib/enum_state_machine/integrations/active_record.rb +0 -548
- data/lib/enum_state_machine/integrations/active_record/locale.rb +0 -20
- data/lib/enum_state_machine/integrations/active_record/versions.rb +0 -123
- data/lib/enum_state_machine/integrations/base.rb +0 -100
- data/lib/enum_state_machine/machine.rb +0 -2292
- data/lib/enum_state_machine/machine_collection.rb +0 -86
- data/lib/enum_state_machine/macro_methods.rb +0 -518
- data/lib/enum_state_machine/matcher.rb +0 -123
- data/lib/enum_state_machine/matcher_helpers.rb +0 -54
- data/lib/enum_state_machine/node_collection.rb +0 -222
- data/lib/enum_state_machine/path.rb +0 -120
- data/lib/enum_state_machine/path_collection.rb +0 -90
- data/lib/enum_state_machine/state.rb +0 -297
- data/lib/enum_state_machine/state_collection.rb +0 -112
- data/lib/enum_state_machine/state_context.rb +0 -138
- data/lib/enum_state_machine/state_enum.rb +0 -23
- data/lib/enum_state_machine/transition.rb +0 -470
- data/lib/enum_state_machine/transition_collection.rb +0 -245
- data/lib/enum_state_machine/version.rb +0 -3
- data/lib/enum_state_machine/yard.rb +0 -8
- data/lib/enum_state_machine/yard/handlers.rb +0 -12
- data/lib/enum_state_machine/yard/handlers/base.rb +0 -32
- data/lib/enum_state_machine/yard/handlers/event.rb +0 -25
- data/lib/enum_state_machine/yard/handlers/machine.rb +0 -344
- data/lib/enum_state_machine/yard/handlers/state.rb +0 -25
- data/lib/enum_state_machine/yard/handlers/transition.rb +0 -47
- data/lib/enum_state_machine/yard/templates.rb +0 -3
- data/lib/enum_state_machine/yard/templates/default/class/html/setup.rb +0 -30
- data/lib/enum_state_machine/yard/templates/default/class/html/state_machines.erb +0 -12
- data/lib/tasks/enum_state_machine.rake +0 -1
- data/lib/tasks/enum_state_machine.rb +0 -24
- data/lib/yard-enum_state_machine.rb +0 -2
- data/test/functional/state_machine_test.rb +0 -1066
- data/test/unit/integrations/active_model_test.rb +0 -1245
- data/test/unit/integrations/active_record_test.rb +0 -2551
- data/test/unit/integrations/base_test.rb +0 -104
- data/test/unit/integrations_test.rb +0 -71
- data/test/unit/invalid_event_test.rb +0 -20
- data/test/unit/invalid_parallel_transition_test.rb +0 -18
- data/test/unit/invalid_transition_test.rb +0 -115
- data/test/unit/machine_collection_test.rb +0 -603
- data/test/unit/machine_test.rb +0 -3395
- data/test/unit/state_machine_test.rb +0 -31
@@ -1,232 +0,0 @@
|
|
1
|
-
require 'enum_state_machine/branch'
|
2
|
-
require 'enum_state_machine/eval_helpers'
|
3
|
-
|
4
|
-
module EnumStateMachine
|
5
|
-
# Callbacks represent hooks into objects that allow logic to be triggered
|
6
|
-
# before, after, or around a specific set of transitions.
|
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 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
|
-
# == Examples
|
21
|
-
#
|
22
|
-
# When not bound to the object:
|
23
|
-
#
|
24
|
-
# class Vehicle
|
25
|
-
# state_machine do
|
26
|
-
# before_transition do |vehicle|
|
27
|
-
# vehicle.set_alarm
|
28
|
-
# end
|
29
|
-
# end
|
30
|
-
#
|
31
|
-
# def set_alarm
|
32
|
-
# ...
|
33
|
-
# end
|
34
|
-
# end
|
35
|
-
#
|
36
|
-
# When bound to the object:
|
37
|
-
#
|
38
|
-
# EnumStateMachine::Callback.bind_to_object = true
|
39
|
-
#
|
40
|
-
# class Vehicle
|
41
|
-
# state_machine do
|
42
|
-
# before_transition do
|
43
|
-
# self.set_alarm
|
44
|
-
# end
|
45
|
-
# end
|
46
|
-
#
|
47
|
-
# def set_alarm
|
48
|
-
# ...
|
49
|
-
# end
|
50
|
-
# end
|
51
|
-
attr_accessor :bind_to_object
|
52
|
-
|
53
|
-
# The application-wide terminator to use for callbacks when not
|
54
|
-
# explicitly defined. Terminators determine whether to cancel a
|
55
|
-
# callback chain based on the return value of the callback.
|
56
|
-
#
|
57
|
-
# See EnumStateMachine::Callback#terminator for more information.
|
58
|
-
attr_accessor :terminator
|
59
|
-
end
|
60
|
-
|
61
|
-
# The type of callback chain this callback is for. This can be one of the
|
62
|
-
# following:
|
63
|
-
# * +before+
|
64
|
-
# * +after+
|
65
|
-
# * +around+
|
66
|
-
# * +failure+
|
67
|
-
attr_accessor :type
|
68
|
-
|
69
|
-
# An optional block for determining whether to cancel the callback chain
|
70
|
-
# based on the return value of the callback. By default, the callback
|
71
|
-
# chain never cancels based on the return value (i.e. there is no implicit
|
72
|
-
# terminator). Certain integrations, such as ActiveRecord,
|
73
|
-
# change this default value.
|
74
|
-
#
|
75
|
-
# == Examples
|
76
|
-
#
|
77
|
-
# Canceling the callback chain without a terminator:
|
78
|
-
#
|
79
|
-
# class Vehicle
|
80
|
-
# state_machine do
|
81
|
-
# before_transition do |vehicle|
|
82
|
-
# throw :halt
|
83
|
-
# end
|
84
|
-
# end
|
85
|
-
# end
|
86
|
-
#
|
87
|
-
# Canceling the callback chain with a terminator value of +false+:
|
88
|
-
#
|
89
|
-
# class Vehicle
|
90
|
-
# state_machine do
|
91
|
-
# before_transition do |vehicle|
|
92
|
-
# false
|
93
|
-
# end
|
94
|
-
# end
|
95
|
-
# end
|
96
|
-
attr_reader :terminator
|
97
|
-
|
98
|
-
# The branch that determines whether or not this callback can be invoked
|
99
|
-
# based on the context of the transition. The event, from state, and
|
100
|
-
# to state must all match in order for the branch to pass.
|
101
|
-
#
|
102
|
-
# See EnumStateMachine::Branch for more information.
|
103
|
-
attr_reader :branch
|
104
|
-
|
105
|
-
# Creates a new callback that can get called based on the configured
|
106
|
-
# options.
|
107
|
-
#
|
108
|
-
# In addition to the possible configuration options for branches, the
|
109
|
-
# following options can be configured:
|
110
|
-
# * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
|
111
|
-
# If set to false, the object will be passed as a parameter instead.
|
112
|
-
# Default is integration-specific or set to the application default.
|
113
|
-
# * <tt>:terminator</tt> - A block/proc that determines what callback
|
114
|
-
# results should cause the callback chain to halt (if not using the
|
115
|
-
# default <tt>throw :halt</tt> technique).
|
116
|
-
#
|
117
|
-
# More information about how those options affect the behavior of the
|
118
|
-
# callback can be found in their attribute definitions.
|
119
|
-
def initialize(type, *args, &block)
|
120
|
-
@type = type
|
121
|
-
raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless [:before, :after, :around, :failure].include?(type)
|
122
|
-
|
123
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
124
|
-
@methods = args
|
125
|
-
@methods.concat(Array(options.delete(:do)))
|
126
|
-
@methods << block if block_given?
|
127
|
-
raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any?
|
128
|
-
|
129
|
-
options = {:bind_to_object => self.class.bind_to_object, :terminator => self.class.terminator}.merge(options)
|
130
|
-
|
131
|
-
# Proxy lambda blocks so that they're bound to the object
|
132
|
-
bind_to_object = options.delete(:bind_to_object)
|
133
|
-
@methods.map! do |method|
|
134
|
-
bind_to_object && method.is_a?(Proc) ? bound_method(method) : method
|
135
|
-
end
|
136
|
-
|
137
|
-
@terminator = options.delete(:terminator)
|
138
|
-
@branch = Branch.new(options)
|
139
|
-
end
|
140
|
-
|
141
|
-
# Gets a list of the states known to this callback by looking at the
|
142
|
-
# branch's known states
|
143
|
-
def known_states
|
144
|
-
branch.known_states
|
145
|
-
end
|
146
|
-
|
147
|
-
# Runs the callback as long as the transition context matches the branch
|
148
|
-
# requirements configured for this callback. If a block is provided, it
|
149
|
-
# will be called when the last method has run.
|
150
|
-
#
|
151
|
-
# If a terminator has been configured and it matches the result from the
|
152
|
-
# evaluated method, then the callback chain should be halted.
|
153
|
-
def call(object, context = {}, *args, &block)
|
154
|
-
if @branch.matches?(object, context)
|
155
|
-
run_methods(object, context, 0, *args, &block)
|
156
|
-
true
|
157
|
-
else
|
158
|
-
false
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
private
|
163
|
-
# Runs all of the methods configured for this callback.
|
164
|
-
#
|
165
|
-
# When running +around+ callbacks, this will evaluate each method and
|
166
|
-
# yield when the last method has yielded. The callback will only halt if
|
167
|
-
# one of the methods does not yield.
|
168
|
-
#
|
169
|
-
# For all other types of callbacks, this will evaluate each method in
|
170
|
-
# order. The callback will only halt if the resulting value from the
|
171
|
-
# method passes the terminator.
|
172
|
-
def run_methods(object, context = {}, index = 0, *args, &block)
|
173
|
-
if type == :around
|
174
|
-
if current_method = @methods[index]
|
175
|
-
yielded = false
|
176
|
-
evaluate_method(object, current_method, *args) do
|
177
|
-
yielded = true
|
178
|
-
run_methods(object, context, index + 1, *args, &block)
|
179
|
-
end
|
180
|
-
|
181
|
-
throw :halt unless yielded
|
182
|
-
else
|
183
|
-
yield if block_given?
|
184
|
-
end
|
185
|
-
else
|
186
|
-
@methods.each do |method|
|
187
|
-
result = evaluate_method(object, method, *args)
|
188
|
-
throw :halt if @terminator && @terminator.call(result)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
# Generates a method that can be bound to the object being transitioned
|
194
|
-
# when the callback is invoked
|
195
|
-
def bound_method(block)
|
196
|
-
type = self.type
|
197
|
-
arity = block.arity
|
198
|
-
arity += 1 if arity >= 0 # Make sure the object gets passed
|
199
|
-
arity += 1 if arity == 1 && type == :around # Make sure the block gets passed
|
200
|
-
|
201
|
-
method = if RUBY_VERSION >= '1.9'
|
202
|
-
lambda do |object, *args|
|
203
|
-
object.instance_exec(*args, &block)
|
204
|
-
end
|
205
|
-
else
|
206
|
-
# Generate a thread-safe unbound method that can be used on any object.
|
207
|
-
# This is a workaround for not having Ruby 1.9's instance_exec
|
208
|
-
unbound_method = Object.class_eval do
|
209
|
-
time = Time.now
|
210
|
-
method_name = "__bind_#{time.to_i}_#{time.usec}"
|
211
|
-
define_method(method_name, &block)
|
212
|
-
method = instance_method(method_name)
|
213
|
-
remove_method(method_name)
|
214
|
-
method
|
215
|
-
end
|
216
|
-
|
217
|
-
# Proxy calls to the method so that the method can be bound *and*
|
218
|
-
# the arguments are adjusted
|
219
|
-
lambda do |object, *args|
|
220
|
-
unbound_method.bind(object).call(*args)
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
# Proxy arity to the original block
|
225
|
-
(class << method; self; end).class_eval do
|
226
|
-
define_method(:arity) { arity }
|
227
|
-
end
|
228
|
-
|
229
|
-
method
|
230
|
-
end
|
231
|
-
end
|
232
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
module EnumStateMachine
|
2
|
-
# Graphing extensions aren't required, so they're loaded when referenced
|
3
|
-
autoload :Graph, 'enum_state_machine/graph'
|
4
|
-
end
|
5
|
-
|
6
|
-
# Load all of the core implementation required to use state_machine. This
|
7
|
-
# includes:
|
8
|
-
# * EnumStateMachine::MacroMethods which adds the state_machine DSL to your class
|
9
|
-
# * A set of initializers for setting state_machine defaults based on the current
|
10
|
-
# running environment (such as within Rails)
|
11
|
-
require 'enum_state_machine/macro_methods'
|
12
|
-
require 'enum_state_machine/initializers'
|
@@ -1,13 +0,0 @@
|
|
1
|
-
module EnumStateMachine
|
2
|
-
# An error occurred during a state machine invocation
|
3
|
-
class Error < StandardError
|
4
|
-
# The object that failed
|
5
|
-
attr_reader :object
|
6
|
-
|
7
|
-
def initialize(object, message = nil) #:nodoc:
|
8
|
-
@object = object
|
9
|
-
|
10
|
-
super(message)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
@@ -1,87 +0,0 @@
|
|
1
|
-
module EnumStateMachine
|
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
|
-
klass = (class << object; self; end)
|
57
|
-
args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
|
58
|
-
object.send(method, *args, &block)
|
59
|
-
when Proc, Method
|
60
|
-
args.unshift(object)
|
61
|
-
arity = method.arity
|
62
|
-
|
63
|
-
# Procs don't support blocks in < Ruby 1.9, so it's tacked on as an
|
64
|
-
# argument for consistency across versions of Ruby
|
65
|
-
if block_given? && Proc === method && arity != 0
|
66
|
-
if [1, 2].include?(arity)
|
67
|
-
# Force the block to be either the only argument or the 2nd one
|
68
|
-
# after the object (may mean additional arguments get discarded)
|
69
|
-
args = args[0, arity - 1] + [block]
|
70
|
-
else
|
71
|
-
# Tack the block to the end of the args
|
72
|
-
args << block
|
73
|
-
end
|
74
|
-
else
|
75
|
-
# These method types are only called with 0, 1, or n arguments
|
76
|
-
args = args[0, arity] if [0, 1].include?(arity)
|
77
|
-
end
|
78
|
-
|
79
|
-
method.is_a?(Proc) ? method.call(*args) : method.call(*args, &block)
|
80
|
-
when String
|
81
|
-
eval(method, object.instance_eval {binding}, &block)
|
82
|
-
else
|
83
|
-
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -1,257 +0,0 @@
|
|
1
|
-
require 'enum_state_machine/transition'
|
2
|
-
require 'enum_state_machine/branch'
|
3
|
-
require 'enum_state_machine/assertions'
|
4
|
-
require 'enum_state_machine/matcher_helpers'
|
5
|
-
require 'enum_state_machine/error'
|
6
|
-
|
7
|
-
module EnumStateMachine
|
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
|
-
include Assertions
|
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
|
-
assert_valid_keys(options, :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 EnumStateMachine::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
|
-
assert_valid_keys(options, :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
|
-
assert_valid_keys(requirements, :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 EnumStateMachine::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
|
-
# Draws a representation of this event on the given graph. This will
|
199
|
-
# create 1 or more edges on the graph for each branch (i.e. transition)
|
200
|
-
# configured.
|
201
|
-
#
|
202
|
-
# Configuration options:
|
203
|
-
# * <tt>:human_name</tt> - Whether to use the event's human name for the
|
204
|
-
# node's label that gets drawn on the graph
|
205
|
-
def draw(graph, options = {})
|
206
|
-
valid_states = machine.states.by_priority.map {|state| state.name}
|
207
|
-
branches.each do |branch|
|
208
|
-
branch.draw(graph, options[:human_name] ? human_name : name, valid_states)
|
209
|
-
end
|
210
|
-
|
211
|
-
true
|
212
|
-
end
|
213
|
-
|
214
|
-
# Generates a nicely formatted description of this event's contents.
|
215
|
-
#
|
216
|
-
# For example,
|
217
|
-
#
|
218
|
-
# event = EnumStateMachine::Event.new(machine, :park)
|
219
|
-
# event.transition all - :idling => :parked, :idling => same
|
220
|
-
# event # => #<EnumStateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
|
221
|
-
def inspect
|
222
|
-
transitions = branches.map do |branch|
|
223
|
-
branch.state_requirements.map do |state_requirement|
|
224
|
-
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
|
225
|
-
end * ', '
|
226
|
-
end
|
227
|
-
|
228
|
-
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
|
229
|
-
end
|
230
|
-
|
231
|
-
protected
|
232
|
-
# Add the various instance methods that can transition the object using
|
233
|
-
# the current event
|
234
|
-
def add_actions
|
235
|
-
# Checks whether the event can be fired on the current object
|
236
|
-
machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, *args|
|
237
|
-
machine.event(name).can_fire?(object, *args)
|
238
|
-
end
|
239
|
-
|
240
|
-
# Gets the next transition that would be performed if the event were
|
241
|
-
# fired now
|
242
|
-
machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, *args|
|
243
|
-
machine.event(name).transition_for(object, *args)
|
244
|
-
end
|
245
|
-
|
246
|
-
# Fires the event
|
247
|
-
machine.define_helper(:instance, qualified_name) do |machine, object, *args|
|
248
|
-
machine.event(name).fire(object, *args)
|
249
|
-
end
|
250
|
-
|
251
|
-
# Fires the event, raising an exception if it fails
|
252
|
-
machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args|
|
253
|
-
object.send(qualified_name, *args) || raise(EnumStateMachine::InvalidTransition.new(object, machine, name))
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
end
|