state_machines 0.20.0 → 0.31.0
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/README.md +154 -18
- data/lib/state_machines/branch.rb +30 -17
- data/lib/state_machines/callback.rb +12 -13
- data/lib/state_machines/core.rb +0 -1
- data/lib/state_machines/error.rb +5 -4
- data/lib/state_machines/eval_helpers.rb +178 -49
- data/lib/state_machines/event.rb +31 -32
- data/lib/state_machines/event_collection.rb +4 -5
- data/lib/state_machines/extensions.rb +5 -5
- data/lib/state_machines/helper_module.rb +1 -1
- data/lib/state_machines/integrations/base.rb +1 -1
- data/lib/state_machines/integrations.rb +11 -14
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +25 -11
- data/lib/state_machines/machine/configuration.rb +124 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +75 -619
- data/lib/state_machines/machine_collection.rb +18 -14
- data/lib/state_machines/macro_methods.rb +2 -2
- data/lib/state_machines/matcher.rb +6 -6
- data/lib/state_machines/matcher_helpers.rb +1 -1
- data/lib/state_machines/node_collection.rb +18 -17
- data/lib/state_machines/path.rb +2 -4
- data/lib/state_machines/path_collection.rb +2 -3
- data/lib/state_machines/state.rb +14 -7
- data/lib/state_machines/state_collection.rb +3 -3
- data/lib/state_machines/state_context.rb +6 -7
- data/lib/state_machines/stdio_renderer.rb +16 -16
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +290 -27
- data/lib/state_machines/transition.rb +57 -46
- data/lib/state_machines/transition_collection.rb +22 -25
- data/lib/state_machines/version.rb +1 -1
- metadata +23 -9
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'syntax_validator'
|
4
|
+
|
3
5
|
module StateMachines
|
4
6
|
# Provides a set of helper methods for evaluating methods within the context
|
5
7
|
# of an object.
|
@@ -45,6 +47,11 @@ module StateMachines
|
|
45
47
|
# the method defines additional arguments other than the object context,
|
46
48
|
# then all arguments are required.
|
47
49
|
#
|
50
|
+
# For guard conditions in state machines, event arguments can be passed
|
51
|
+
# automatically based on the guard's arity:
|
52
|
+
# - Guards with arity 1 receive only the object (backward compatible)
|
53
|
+
# - Guards with arity -1 or > 1 receive object + event arguments
|
54
|
+
#
|
48
55
|
# For example,
|
49
56
|
#
|
50
57
|
# person = Person.new('John Smith')
|
@@ -52,64 +59,186 @@ module StateMachines
|
|
52
59
|
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
53
60
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
54
61
|
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
|
55
|
-
|
62
|
+
#
|
63
|
+
# With event arguments for guards:
|
64
|
+
#
|
65
|
+
# # Single parameter guard (backward compatible)
|
66
|
+
# guard = lambda {|obj| obj.valid? }
|
67
|
+
# evaluate_method_with_event_args(object, guard, [arg1, arg2]) # => calls guard.call(object)
|
68
|
+
#
|
69
|
+
# # Multi-parameter guard (receives event args)
|
70
|
+
# guard = lambda {|obj, *args| obj.valid? && args[0] == :force }
|
71
|
+
# evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object, :force)
|
72
|
+
def evaluate_method(object, method, *args, **, &block)
|
56
73
|
case method
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
args << block
|
73
|
-
end
|
74
|
+
in Symbol => sym
|
75
|
+
klass = (class << object; self; end)
|
76
|
+
args = [] if (klass.method_defined?(sym) || klass.private_method_defined?(sym)) && object.method(sym).arity.zero?
|
77
|
+
object.send(sym, *args, **, &block)
|
78
|
+
in Proc => proc
|
79
|
+
args.unshift(object)
|
80
|
+
arity = proc.arity
|
81
|
+
# Handle blocks for Procs
|
82
|
+
case [block_given?, arity]
|
83
|
+
in [true, arity] if arity != 0
|
84
|
+
case arity
|
85
|
+
in 1 | 2
|
86
|
+
# Force the block to be either the only argument or the second one
|
87
|
+
# after the object (may mean additional arguments get discarded)
|
88
|
+
args = args[0, arity - 1] + [block]
|
74
89
|
else
|
75
|
-
#
|
76
|
-
args
|
90
|
+
# insert the block to the end of the args
|
91
|
+
args << block
|
77
92
|
end
|
93
|
+
in [_, 0 | 1]
|
94
|
+
# These method types are only called with 0, 1, or n arguments
|
95
|
+
args = args[0, arity]
|
96
|
+
else
|
97
|
+
# No changes needed for other cases
|
98
|
+
end
|
78
99
|
|
79
100
|
# Call the Proc with the arguments
|
80
|
-
|
101
|
+
proc.call(*args, **)
|
81
102
|
|
82
|
-
|
83
|
-
|
84
|
-
|
103
|
+
in Method => meth
|
104
|
+
args.unshift(object)
|
105
|
+
arity = meth.arity
|
85
106
|
|
86
|
-
|
87
|
-
|
88
|
-
|
107
|
+
# Methods handle blocks via &block, not as arguments
|
108
|
+
# Only limit arguments if necessary based on arity
|
109
|
+
args = args[0, arity] if [0, 1].include?(arity)
|
89
110
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
+
# Call the Method with the arguments and pass the block
|
112
|
+
meth.call(*args, **, &block)
|
113
|
+
in String => str
|
114
|
+
# Input validation for string evaluation
|
115
|
+
validate_eval_string(str)
|
116
|
+
|
117
|
+
case [block_given?, StateMachines::Transition.pause_supported?]
|
118
|
+
in [true, true]
|
119
|
+
eval(str, object.instance_eval { binding }, &block)
|
120
|
+
in [true, false]
|
121
|
+
# Support for JRuby and Truffle Ruby, which don't support binding blocks
|
122
|
+
# Need to check with @headius, if jruby 10 does now.
|
123
|
+
eigen = class << object; self; end
|
124
|
+
eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
125
|
+
def __temp_eval_method__(*args, &b)
|
126
|
+
#{str}
|
127
|
+
end
|
128
|
+
RUBY
|
129
|
+
result = object.__temp_eval_method__(*args, &block)
|
130
|
+
eigen.send(:remove_method, :__temp_eval_method__)
|
131
|
+
result
|
132
|
+
in [false, _]
|
133
|
+
eval(str, object.instance_eval { binding })
|
134
|
+
end
|
135
|
+
else
|
136
|
+
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Evaluates a guard method with support for event arguments passed to transitions.
|
141
|
+
# This method uses arity detection to determine whether to pass event arguments
|
142
|
+
# to the guard, ensuring backward compatibility.
|
143
|
+
#
|
144
|
+
# == Parameters
|
145
|
+
# * object - The object context to evaluate within
|
146
|
+
# * method - The guard method/proc to evaluate
|
147
|
+
# * event_args - Array of arguments passed to the event (optional)
|
148
|
+
#
|
149
|
+
# == Arity-based behavior
|
150
|
+
# * Arity 1: Only passes the object (backward compatible)
|
151
|
+
# * Arity -1 or > 1: Passes object + event arguments
|
152
|
+
#
|
153
|
+
# == Examples
|
154
|
+
#
|
155
|
+
# # Backward compatible single-parameter guard
|
156
|
+
# guard = lambda {|obj| obj.valid? }
|
157
|
+
# evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object)
|
158
|
+
#
|
159
|
+
# # New multi-parameter guard receiving event args
|
160
|
+
# guard = lambda {|obj, *args| obj.valid? && args[0] != :skip }
|
161
|
+
# evaluate_method_with_event_args(object, guard, [:skip]) # => calls guard.call(object, :skip)
|
162
|
+
def evaluate_method_with_event_args(object, method, event_args = [])
|
163
|
+
case method
|
164
|
+
in Symbol
|
165
|
+
# Symbol methods currently don't support event arguments
|
166
|
+
# This maintains backward compatibility
|
167
|
+
evaluate_method(object, method)
|
168
|
+
in Proc => proc
|
169
|
+
arity = proc.arity
|
170
|
+
|
171
|
+
# Arity-based decision for backward compatibility using pattern matching
|
172
|
+
case arity
|
173
|
+
in 0
|
174
|
+
proc.call
|
175
|
+
in 1
|
176
|
+
proc.call(object)
|
177
|
+
in -1
|
178
|
+
# Splat parameters: object + all event args
|
179
|
+
proc.call(object, *event_args)
|
180
|
+
in arity if arity > 1
|
181
|
+
# Explicit parameters: object + limited event args
|
182
|
+
args_needed = arity - 1 # Subtract 1 for the object parameter
|
183
|
+
proc.call(object, *event_args[0, args_needed])
|
184
|
+
else
|
185
|
+
# Negative arity other than -1 (unlikely but handle gracefully)
|
186
|
+
proc.call(object, *event_args)
|
187
|
+
end
|
188
|
+
in Method => meth
|
189
|
+
arity = meth.arity
|
190
|
+
|
191
|
+
case arity
|
192
|
+
in 0
|
193
|
+
meth.call
|
194
|
+
in 1
|
195
|
+
meth.call(object)
|
196
|
+
in -1
|
197
|
+
meth.call(object, *event_args)
|
198
|
+
in arity if arity > 1
|
199
|
+
args_needed = arity - 1
|
200
|
+
meth.call(object, *event_args[0, args_needed])
|
111
201
|
else
|
112
|
-
|
202
|
+
meth.call(object, *event_args)
|
203
|
+
end
|
204
|
+
in String
|
205
|
+
# String evaluation doesn't support event arguments for security
|
206
|
+
evaluate_method(object, method)
|
207
|
+
else
|
208
|
+
# Fall back to standard evaluation
|
209
|
+
evaluate_method(object, method)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
# Validates string input before eval to prevent code injection
|
216
|
+
# This is a basic safety check - not foolproof security
|
217
|
+
def validate_eval_string(method_string)
|
218
|
+
# Check for obviously dangerous patterns
|
219
|
+
dangerous_patterns = [
|
220
|
+
/`.*`/, # Backticks (shell execution)
|
221
|
+
/system\s*\(/, # System calls
|
222
|
+
/exec\s*\(/, # Exec calls
|
223
|
+
/eval\s*\(/, # Nested eval
|
224
|
+
/require\s+['"]/, # Require statements
|
225
|
+
/load\s+['"]/, # Load statements
|
226
|
+
/File\./, # File operations
|
227
|
+
/IO\./, # IO operations
|
228
|
+
/Dir\./, # Directory operations
|
229
|
+
/Kernel\./ # Kernel operations
|
230
|
+
]
|
231
|
+
|
232
|
+
dangerous_patterns.each do |pattern|
|
233
|
+
raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Basic syntax validation - but allow yield since it's valid in block context
|
237
|
+
begin
|
238
|
+
test_code = method_string.include?('yield') ? "def dummy_method; #{method_string}; end" : method_string
|
239
|
+
SyntaxValidator.validate!(test_code, '(eval)')
|
240
|
+
rescue SyntaxError => e
|
241
|
+
raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}"
|
113
242
|
end
|
114
243
|
end
|
115
244
|
end
|
data/lib/state_machines/event.rb
CHANGED
@@ -7,7 +7,6 @@ module StateMachines
|
|
7
7
|
# another. The state that an attribute is transitioned to depends on the
|
8
8
|
# branches configured for the event.
|
9
9
|
class Event
|
10
|
-
|
11
10
|
include MatcherHelpers
|
12
11
|
|
13
12
|
# The state machine for which this event is defined
|
@@ -34,7 +33,7 @@ module StateMachines
|
|
34
33
|
#
|
35
34
|
# Configuration options:
|
36
35
|
# * <tt>:human_name</tt> - The human-readable version of this event's name
|
37
|
-
def initialize(machine, name, options = nil, human_name: nil, **extra_options)
|
36
|
+
def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
|
38
37
|
# Handle both old hash style and new kwargs style for backward compatibility
|
39
38
|
if options.is_a?(Hash)
|
40
39
|
# Old style: initialize(machine, name, {human_name: 'Custom Name'})
|
@@ -43,6 +42,7 @@ module StateMachines
|
|
43
42
|
else
|
44
43
|
# New style: initialize(machine, name, human_name: 'Custom Name')
|
45
44
|
raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
|
45
|
+
|
46
46
|
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
|
47
47
|
end
|
48
48
|
|
@@ -63,7 +63,7 @@ module StateMachines
|
|
63
63
|
|
64
64
|
# Creates a copy of this event in addition to the list of associated
|
65
65
|
# branches to prevent conflicts across events within a class hierarchy.
|
66
|
-
def initialize_copy(orig)
|
66
|
+
def initialize_copy(orig) # :nodoc:
|
67
67
|
super
|
68
68
|
@branches = @branches.dup
|
69
69
|
@known_states = @known_states.dup
|
@@ -77,8 +77,8 @@ module StateMachines
|
|
77
77
|
|
78
78
|
# Evaluates the given block within the context of this event. This simply
|
79
79
|
# provides a DSL-like syntax for defining transitions.
|
80
|
-
def context(&
|
81
|
-
instance_eval(&
|
80
|
+
def context(&)
|
81
|
+
instance_eval(&)
|
82
82
|
end
|
83
83
|
|
84
84
|
# Creates a new transition that determines what to change the current state
|
@@ -102,9 +102,7 @@ module StateMachines
|
|
102
102
|
|
103
103
|
# Only a certain subset of explicit options are allowed for transition
|
104
104
|
# requirements
|
105
|
-
|
106
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless)
|
107
|
-
end
|
105
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - %i[from to on except_from except_to except_on if unless]).empty?
|
108
106
|
|
109
107
|
branches << branch = Branch.new(options.merge(on: name))
|
110
108
|
@known_states |= branch.known_states
|
@@ -133,24 +131,26 @@ module StateMachines
|
|
133
131
|
# specified, then this will match any to state.
|
134
132
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
135
133
|
# conditionals defined for each one. Default is true.
|
136
|
-
|
134
|
+
#
|
135
|
+
# Event arguments are passed to guard conditions if they accept multiple parameters.
|
136
|
+
def transition_for(object, requirements = {}, *event_args)
|
137
137
|
StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
|
138
138
|
requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
|
139
139
|
|
140
140
|
branches.each do |branch|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
141
|
+
next unless (match = branch.match(object, requirements, event_args))
|
142
|
+
|
143
|
+
# Branch allows for the transition to occur
|
144
|
+
from = requirements[:from]
|
145
|
+
to = if match[:to].is_a?(LoopbackMatcher)
|
146
|
+
from
|
147
|
+
else
|
148
|
+
values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }
|
149
|
+
|
150
|
+
match[:to].filter(values).first
|
151
|
+
end
|
152
|
+
|
153
|
+
return Transition.new(object, machine, name, from, to, !custom_from_state)
|
154
154
|
end
|
155
155
|
|
156
156
|
# No transition matched
|
@@ -163,13 +163,13 @@ module StateMachines
|
|
163
163
|
#
|
164
164
|
# Any additional arguments are passed to the StateMachines::Transition#perform
|
165
165
|
# instance method.
|
166
|
-
def fire(object, *
|
166
|
+
def fire(object, *event_args)
|
167
167
|
machine.reset(object)
|
168
168
|
|
169
|
-
if (transition = transition_for(object))
|
170
|
-
transition.perform(*
|
169
|
+
if (transition = transition_for(object, {}, *event_args))
|
170
|
+
transition.perform(*event_args)
|
171
171
|
else
|
172
|
-
on_failure(object, *
|
172
|
+
on_failure(object, *event_args)
|
173
173
|
false
|
174
174
|
end
|
175
175
|
end
|
@@ -194,7 +194,6 @@ module StateMachines
|
|
194
194
|
@known_states = []
|
195
195
|
end
|
196
196
|
|
197
|
-
|
198
197
|
def draw(graph, options = {}, io = $stdout)
|
199
198
|
machine.renderer.draw_event(self, graph, options, io)
|
200
199
|
end
|
@@ -207,16 +206,16 @@ module StateMachines
|
|
207
206
|
# event.transition all - :idling => :parked, :idling => same
|
208
207
|
# event # => #<StateMachines::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
|
209
208
|
def inspect
|
210
|
-
transitions = branches.
|
209
|
+
transitions = branches.flat_map do |branch|
|
211
210
|
branch.state_requirements.map do |state_requirement|
|
212
211
|
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
|
213
|
-
end
|
214
|
-
end
|
212
|
+
end
|
213
|
+
end.join(', ')
|
215
214
|
|
216
|
-
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions
|
215
|
+
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions}]>"
|
217
216
|
end
|
218
217
|
|
219
|
-
|
218
|
+
protected
|
220
219
|
|
221
220
|
# Add the various instance methods that can transition the object using
|
222
221
|
# the current event
|
@@ -3,8 +3,8 @@
|
|
3
3
|
module StateMachines
|
4
4
|
# Represents a collection of events in a state machine
|
5
5
|
class EventCollection < NodeCollection
|
6
|
-
def initialize(machine)
|
7
|
-
super(machine, index: [
|
6
|
+
def initialize(machine) # :nodoc:
|
7
|
+
super(machine, index: %i[name qualified_name])
|
8
8
|
end
|
9
9
|
|
10
10
|
# Gets the list of events that can be fired on the given object.
|
@@ -130,12 +130,11 @@ module StateMachines
|
|
130
130
|
false
|
131
131
|
end
|
132
132
|
end
|
133
|
-
|
134
133
|
end
|
135
134
|
|
136
|
-
|
135
|
+
private
|
137
136
|
|
138
|
-
def match(requirements)
|
137
|
+
def match(requirements) # :nodoc:
|
139
138
|
requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
|
140
139
|
end
|
141
140
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module StateMachines
|
4
4
|
module ClassMethods
|
5
|
-
def self.extended(base)
|
5
|
+
def self.extended(base) # :nodoc:
|
6
6
|
base.class_eval do
|
7
7
|
@state_machines = MachineCollection.new
|
8
8
|
end
|
@@ -138,13 +138,13 @@ module StateMachines
|
|
138
138
|
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm
|
139
139
|
def fire_events!(*events)
|
140
140
|
run_action = [true, false].include?(events.last) ? events.pop : true
|
141
|
-
fire_events(*(events + [run_action])) ||
|
141
|
+
fire_events(*(events + [run_action])) || raise(StateMachines::InvalidParallelTransition.new(self, events))
|
142
142
|
end
|
143
143
|
|
144
|
-
|
144
|
+
protected
|
145
145
|
|
146
|
-
def initialize_state_machines(options = {}, &
|
147
|
-
self.class.state_machines.initialize_states(self, options, &
|
146
|
+
def initialize_state_machines(options = {}, &) # :nodoc:
|
147
|
+
self.class.state_machines.initialize_states(self, options, &)
|
148
148
|
end
|
149
149
|
end
|
150
150
|
end
|
@@ -26,15 +26,15 @@ module StateMachines
|
|
26
26
|
# Register integration
|
27
27
|
def register(name_or_module)
|
28
28
|
case name_or_module.class.to_s
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
when 'Module'
|
30
|
+
add(name_or_module)
|
31
|
+
else
|
32
|
+
raise IntegrationError
|
33
33
|
end
|
34
34
|
true
|
35
35
|
end
|
36
36
|
|
37
|
-
def reset
|
37
|
+
def reset # :nodoc:#
|
38
38
|
@integrations = []
|
39
39
|
end
|
40
40
|
|
@@ -47,12 +47,9 @@ module StateMachines
|
|
47
47
|
# StateMachines::Integrations.register(StateMachines::Integrations::ActiveModel)
|
48
48
|
# StateMachines::Integrations.integrations
|
49
49
|
# # => [StateMachines::Integrations::ActiveModel]
|
50
|
-
|
51
|
-
# Register all namespaced integrations
|
52
|
-
@integrations
|
53
|
-
end
|
50
|
+
attr_reader :integrations
|
54
51
|
|
55
|
-
|
52
|
+
alias list integrations
|
56
53
|
|
57
54
|
# Attempts to find an integration that matches the given class. This will
|
58
55
|
# look through all of the built-in integrations under the StateMachines::Integrations
|
@@ -102,12 +99,12 @@ module StateMachines
|
|
102
99
|
integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
|
103
100
|
end
|
104
101
|
|
105
|
-
|
102
|
+
private
|
106
103
|
|
107
104
|
def add(integration)
|
108
|
-
|
109
|
-
|
110
|
-
|
105
|
+
return unless integration.respond_to?(:integration_name)
|
106
|
+
|
107
|
+
@integrations.insert(0, integration) unless @integrations.include?(integration)
|
111
108
|
end
|
112
109
|
end
|
113
110
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
class Machine
|
5
|
+
module ActionHooks
|
6
|
+
protected
|
7
|
+
|
8
|
+
# Determines whether action helpers should be defined for this machine.
|
9
|
+
# This is only true if there is an action configured and no other machines
|
10
|
+
# have process this same configuration already.
|
11
|
+
def define_action_helpers?
|
12
|
+
action && owner_class.state_machines.none? { |_name, machine| machine.action == action && machine != self }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds helper methods for automatically firing events when an action
|
16
|
+
# is invoked
|
17
|
+
def define_action_helpers
|
18
|
+
return unless action_hook
|
19
|
+
|
20
|
+
@action_hook_defined = true
|
21
|
+
define_action_hook
|
22
|
+
end
|
23
|
+
|
24
|
+
# Hooks directly into actions by defining the same method in an included
|
25
|
+
# module. As a result, when the action gets invoked, any state events
|
26
|
+
# defined for the object will get run. Method visibility is preserved.
|
27
|
+
def define_action_hook
|
28
|
+
action_hook = self.action_hook
|
29
|
+
action = self.action
|
30
|
+
private_action_hook = owner_class.private_method_defined?(action_hook)
|
31
|
+
|
32
|
+
# Only define helper if it hasn't
|
33
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
34
|
+
def #{action_hook}(*)
|
35
|
+
self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
|
36
|
+
end
|
37
|
+
|
38
|
+
private #{action_hook.inspect} if #{private_action_hook}
|
39
|
+
END_EVAL
|
40
|
+
end
|
41
|
+
|
42
|
+
# The method to hook into for triggering transitions when invoked. By
|
43
|
+
# default, this is the action configured for the machine.
|
44
|
+
#
|
45
|
+
# Since the default hook technique relies on module inheritance, the
|
46
|
+
# action must be defined in an ancestor of the owner classs in order for
|
47
|
+
# it to be the action hook.
|
48
|
+
def action_hook
|
49
|
+
action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
class Machine
|
5
|
+
module Callbacks
|
6
|
+
# Creates a callback that will be invoked *before* a transition is
|
7
|
+
# performed so long as the given requirements match the transition.
|
8
|
+
def before_transition(*args, **options, &)
|
9
|
+
# Extract legacy positional arguments and merge with keyword options
|
10
|
+
parsed_options = parse_callback_arguments(args, options)
|
11
|
+
|
12
|
+
# Only validate callback-specific options, not state transition requirements
|
13
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
14
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
15
|
+
|
16
|
+
add_callback(:before, parsed_options, &)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates a callback that will be invoked *after* a transition is
|
20
|
+
# performed so long as the given requirements match the transition.
|
21
|
+
def after_transition(*args, **options, &)
|
22
|
+
# Extract legacy positional arguments and merge with keyword options
|
23
|
+
parsed_options = parse_callback_arguments(args, options)
|
24
|
+
|
25
|
+
# Only validate callback-specific options, not state transition requirements
|
26
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
27
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
28
|
+
|
29
|
+
add_callback(:after, parsed_options, &)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a callback that will be invoked *around* a transition so long
|
33
|
+
# as the given requirements match the transition.
|
34
|
+
def around_transition(*args, **options, &)
|
35
|
+
# Extract legacy positional arguments and merge with keyword options
|
36
|
+
parsed_options = parse_callback_arguments(args, options)
|
37
|
+
|
38
|
+
# Only validate callback-specific options, not state transition requirements
|
39
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
40
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
41
|
+
|
42
|
+
add_callback(:around, parsed_options, &)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creates a callback that will be invoked after a transition has failed
|
46
|
+
# to be performed.
|
47
|
+
def after_failure(*args, **options, &)
|
48
|
+
# Extract legacy positional arguments and merge with keyword options
|
49
|
+
parsed_options = parse_callback_arguments(args, options)
|
50
|
+
|
51
|
+
# Only validate callback-specific options, not state transition requirements
|
52
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
53
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
54
|
+
|
55
|
+
add_callback(:failure, parsed_options, &)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|