ultra-smart-sys 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/state_machines-0.201.0/LICENSE.txt +23 -0
- data/state_machines-0.201.0/README.md +1024 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
- data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
- data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
- data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
- data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
- data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
- data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
- data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
- data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
- data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
- data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
- data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
- data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
- data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
- data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
- data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
- data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
- data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
- data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
- data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
- data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
- data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
- data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
- data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
- data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
- data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
- data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
- data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
- data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
- data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
- data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
- data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
- data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
- data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
- data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
- data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
- data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
- data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
- data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
- data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
- data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
- data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
- data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines.rb +6 -0
- data/ultra-smart-sys.gemspec +12 -0
- metadata +96 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'syntax_validator'
|
|
4
|
+
|
|
5
|
+
module StateMachines
|
|
6
|
+
# Provides a set of helper methods for evaluating methods within the context
|
|
7
|
+
# of an object.
|
|
8
|
+
module EvalHelpers
|
|
9
|
+
# Evaluates one of several different types of methods within the context
|
|
10
|
+
# of the given object. Methods can be one of the following types:
|
|
11
|
+
# * Symbol
|
|
12
|
+
# * Method / Proc
|
|
13
|
+
# * String
|
|
14
|
+
#
|
|
15
|
+
# == Examples
|
|
16
|
+
#
|
|
17
|
+
# Below are examples of the various ways that a method can be evaluated
|
|
18
|
+
# on an object:
|
|
19
|
+
#
|
|
20
|
+
# class Person
|
|
21
|
+
# def initialize(name)
|
|
22
|
+
# @name = name
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# def name
|
|
26
|
+
# @name
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# class PersonCallback
|
|
31
|
+
# def self.run(person)
|
|
32
|
+
# person.name
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# person = Person.new('John Smith')
|
|
37
|
+
#
|
|
38
|
+
# evaluate_method(person, :name) # => "John Smith"
|
|
39
|
+
# evaluate_method(person, PersonCallback.method(:run)) # => "John Smith"
|
|
40
|
+
# evaluate_method(person, Proc.new {|person| person.name}) # => "John Smith"
|
|
41
|
+
# evaluate_method(person, lambda {|person| person.name}) # => "John Smith"
|
|
42
|
+
# evaluate_method(person, '@name') # => "John Smith"
|
|
43
|
+
#
|
|
44
|
+
# == Additional arguments
|
|
45
|
+
#
|
|
46
|
+
# Additional arguments can be passed to the methods being evaluated. If
|
|
47
|
+
# the method defines additional arguments other than the object context,
|
|
48
|
+
# then all arguments are required.
|
|
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
|
+
#
|
|
55
|
+
# For example,
|
|
56
|
+
#
|
|
57
|
+
# person = Person.new('John Smith')
|
|
58
|
+
#
|
|
59
|
+
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
|
60
|
+
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
|
61
|
+
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
|
|
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)
|
|
73
|
+
case method
|
|
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]
|
|
89
|
+
else
|
|
90
|
+
# insert the block to the end of the args
|
|
91
|
+
args << block
|
|
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
|
|
99
|
+
|
|
100
|
+
# Call the Proc with the arguments
|
|
101
|
+
proc.call(*args, **)
|
|
102
|
+
|
|
103
|
+
in Method => meth
|
|
104
|
+
args.unshift(object)
|
|
105
|
+
arity = meth.arity
|
|
106
|
+
|
|
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)
|
|
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
|
+
# Evaluate the string in the object's context
|
|
118
|
+
if block_given?
|
|
119
|
+
# TruffleRuby and some other implementations need special handling for blocks
|
|
120
|
+
# Create a temporary method to evaluate the string with block support
|
|
121
|
+
eigen = class << object; self; end
|
|
122
|
+
eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
123
|
+
def __temp_eval_method__(*args, &b)
|
|
124
|
+
#{str}
|
|
125
|
+
end
|
|
126
|
+
RUBY
|
|
127
|
+
result = object.__temp_eval_method__(*args, &block)
|
|
128
|
+
eigen.send(:remove_method, :__temp_eval_method__)
|
|
129
|
+
result
|
|
130
|
+
else
|
|
131
|
+
object.instance_eval(str, __FILE__, __LINE__)
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Evaluates a guard method with support for event arguments passed to transitions.
|
|
139
|
+
# This method uses arity detection to determine whether to pass event arguments
|
|
140
|
+
# to the guard, ensuring backward compatibility.
|
|
141
|
+
#
|
|
142
|
+
# == Parameters
|
|
143
|
+
# * object - The object context to evaluate within
|
|
144
|
+
# * method - The guard method/proc to evaluate
|
|
145
|
+
# * event_args - Array of arguments passed to the event (optional)
|
|
146
|
+
#
|
|
147
|
+
# == Arity-based behavior
|
|
148
|
+
# * Arity 1: Only passes the object (backward compatible)
|
|
149
|
+
# * Arity -1 or > 1: Passes object + event arguments
|
|
150
|
+
#
|
|
151
|
+
# == Examples
|
|
152
|
+
#
|
|
153
|
+
# # Backward compatible single-parameter guard
|
|
154
|
+
# guard = lambda {|obj| obj.valid? }
|
|
155
|
+
# evaluate_method_with_event_args(object, guard, [:force]) # => calls guard.call(object)
|
|
156
|
+
#
|
|
157
|
+
# # New multi-parameter guard receiving event args
|
|
158
|
+
# guard = lambda {|obj, *args| obj.valid? && args[0] != :skip }
|
|
159
|
+
# evaluate_method_with_event_args(object, guard, [:skip]) # => calls guard.call(object, :skip)
|
|
160
|
+
def evaluate_method_with_event_args(object, method, event_args = [])
|
|
161
|
+
case method
|
|
162
|
+
in Symbol
|
|
163
|
+
# Symbol methods currently don't support event arguments
|
|
164
|
+
# This maintains backward compatibility
|
|
165
|
+
evaluate_method(object, method)
|
|
166
|
+
in Proc | Method => callable
|
|
167
|
+
arity = callable.arity
|
|
168
|
+
|
|
169
|
+
# Arity-based decision for backward compatibility using pattern matching
|
|
170
|
+
case arity
|
|
171
|
+
in 0
|
|
172
|
+
callable.call
|
|
173
|
+
in 1
|
|
174
|
+
callable.call(object)
|
|
175
|
+
in -1
|
|
176
|
+
# Splat parameters: object + all event args
|
|
177
|
+
callable.call(object, *event_args)
|
|
178
|
+
in arity if arity > 1
|
|
179
|
+
# Explicit parameters: object + limited event args
|
|
180
|
+
args_needed = arity - 1 # Subtract 1 for the object parameter
|
|
181
|
+
callable.call(object, *event_args[0, args_needed])
|
|
182
|
+
else
|
|
183
|
+
# Negative arity other than -1 (unlikely but handle gracefully)
|
|
184
|
+
callable.call(object, *event_args)
|
|
185
|
+
end
|
|
186
|
+
in String
|
|
187
|
+
# String evaluation doesn't support event arguments for security
|
|
188
|
+
evaluate_method(object, method)
|
|
189
|
+
else
|
|
190
|
+
# Fall back to standard evaluation
|
|
191
|
+
evaluate_method(object, method)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# Validates string input before eval to prevent code injection
|
|
198
|
+
# This is a basic safety check - not foolproof security
|
|
199
|
+
def validate_eval_string(method_string)
|
|
200
|
+
# Check for obviously dangerous patterns
|
|
201
|
+
dangerous_patterns = [
|
|
202
|
+
/`.*`/, # Backticks (shell execution)
|
|
203
|
+
/system\s*\(/, # System calls
|
|
204
|
+
/exec\s*\(/, # Exec calls
|
|
205
|
+
/eval\s*\(/, # Nested eval
|
|
206
|
+
/require\s+['"]/, # Require statements
|
|
207
|
+
/load\s+['"]/, # Load statements
|
|
208
|
+
/File\./, # File operations
|
|
209
|
+
/IO\./, # IO operations
|
|
210
|
+
/Dir\./, # Directory operations
|
|
211
|
+
/Kernel\./ # Kernel operations
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
dangerous_patterns.each do |pattern|
|
|
215
|
+
raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Basic syntax validation - but allow yield since it's valid in block context
|
|
219
|
+
begin
|
|
220
|
+
test_code = method_string.include?('yield') ? "def dummy_method; #{method_string}; end" : method_string
|
|
221
|
+
SyntaxValidator.validate!(test_code, '(eval)')
|
|
222
|
+
rescue SyntaxError => e
|
|
223
|
+
raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'options_validator'
|
|
4
|
+
|
|
5
|
+
module StateMachines
|
|
6
|
+
# An event defines an action that transitions an attribute from one state to
|
|
7
|
+
# another. The state that an attribute is transitioned to depends on the
|
|
8
|
+
# branches configured for the event.
|
|
9
|
+
class Event
|
|
10
|
+
include MatcherHelpers
|
|
11
|
+
|
|
12
|
+
# The state machine for which this event is defined
|
|
13
|
+
attr_accessor :machine
|
|
14
|
+
|
|
15
|
+
# The name of the event
|
|
16
|
+
attr_reader :name
|
|
17
|
+
|
|
18
|
+
# The fully-qualified name of the event, scoped by the machine's namespace
|
|
19
|
+
attr_reader :qualified_name
|
|
20
|
+
|
|
21
|
+
# The human-readable name for the event
|
|
22
|
+
attr_writer :human_name
|
|
23
|
+
|
|
24
|
+
# The list of branches that determine what state this event transitions
|
|
25
|
+
# objects to when fired
|
|
26
|
+
attr_reader :branches
|
|
27
|
+
|
|
28
|
+
# A list of all of the states known to this event using the configured
|
|
29
|
+
# branches/transitions as the source
|
|
30
|
+
attr_reader :known_states
|
|
31
|
+
|
|
32
|
+
# Creates a new event within the context of the given machine
|
|
33
|
+
#
|
|
34
|
+
# Configuration options:
|
|
35
|
+
# * <tt>:human_name</tt> - The human-readable version of this event's name
|
|
36
|
+
def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
|
|
37
|
+
# Handle both old hash style and new kwargs style for backward compatibility
|
|
38
|
+
case options
|
|
39
|
+
in Hash
|
|
40
|
+
# Old style: initialize(machine, name, {human_name: 'Custom Name'})
|
|
41
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
|
42
|
+
human_name = options[:human_name]
|
|
43
|
+
in nil
|
|
44
|
+
# New style: initialize(machine, name, human_name: 'Custom Name')
|
|
45
|
+
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
|
|
46
|
+
else
|
|
47
|
+
# Handle unexpected options
|
|
48
|
+
raise ArgumentError, "Unexpected positional argument in Event initialize: #{options.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@machine = machine
|
|
52
|
+
@name = name
|
|
53
|
+
@qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
|
|
54
|
+
@human_name = human_name || @name.to_s.tr('_', ' ')
|
|
55
|
+
reset
|
|
56
|
+
|
|
57
|
+
# Output a warning if another event has a conflicting qualified name
|
|
58
|
+
if (conflict = machine.owner_class.state_machines.detect { |_other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] })
|
|
59
|
+
_name, other_machine = conflict
|
|
60
|
+
warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
|
|
61
|
+
else
|
|
62
|
+
add_actions
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Creates a copy of this event in addition to the list of associated
|
|
67
|
+
# branches to prevent conflicts across events within a class hierarchy.
|
|
68
|
+
def initialize_copy(orig) # :nodoc:
|
|
69
|
+
super
|
|
70
|
+
@branches = @branches.dup
|
|
71
|
+
@known_states = @known_states.dup
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Transforms the event name into a more human-readable format, such as
|
|
75
|
+
# "turn on" instead of "turn_on"
|
|
76
|
+
def human_name(klass = @machine.owner_class)
|
|
77
|
+
@human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Evaluates the given block within the context of this event. This simply
|
|
81
|
+
# provides a DSL-like syntax for defining transitions.
|
|
82
|
+
def context(&)
|
|
83
|
+
instance_eval(&)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Creates a new transition that determines what to change the current state
|
|
87
|
+
# to when this event fires.
|
|
88
|
+
#
|
|
89
|
+
# Since this transition is being defined within an event context, you do
|
|
90
|
+
# *not* need to specify the <tt>:on</tt> option for the transition. For
|
|
91
|
+
# example:
|
|
92
|
+
#
|
|
93
|
+
# state_machine do
|
|
94
|
+
# event :ignite do
|
|
95
|
+
# transition :parked => :idling, :idling => same, :if => :seatbelt_on? # Transitions to :idling if seatbelt is on
|
|
96
|
+
# transition all => :parked, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
|
|
97
|
+
# end
|
|
98
|
+
# end
|
|
99
|
+
#
|
|
100
|
+
# See StateMachines::Machine#transition for a description of the possible
|
|
101
|
+
# configurations for defining transitions.
|
|
102
|
+
def transition(options)
|
|
103
|
+
raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
|
|
104
|
+
|
|
105
|
+
# Only a certain subset of explicit options are allowed for transition
|
|
106
|
+
# requirements
|
|
107
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state) if (options.keys - %i[from to on except_from except_to except_on if unless if_state unless_state if_all_states unless_all_states if_any_state unless_any_state]).empty?
|
|
108
|
+
|
|
109
|
+
branches << branch = Branch.new(options.merge(on: name))
|
|
110
|
+
@known_states |= branch.known_states
|
|
111
|
+
branch
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Determines whether any transitions can be performed for this event based
|
|
115
|
+
# on the current state of the given object.
|
|
116
|
+
#
|
|
117
|
+
# If the event can't be fired, then this will return false, otherwise true.
|
|
118
|
+
#
|
|
119
|
+
# *Note* that this will not take the object context into account. Although
|
|
120
|
+
# a transition may be possible based on the state machine definition,
|
|
121
|
+
# object-specific behaviors (like validations) may prevent it from firing.
|
|
122
|
+
def can_fire?(object, requirements = {})
|
|
123
|
+
!transition_for(object, requirements).nil?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Finds and builds the next transition that can be performed on the given
|
|
127
|
+
# object. If no transitions can be made, then this will return nil.
|
|
128
|
+
#
|
|
129
|
+
# Valid requirement options:
|
|
130
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
|
131
|
+
# are specified, then this will be the object's current state.
|
|
132
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
|
133
|
+
# specified, then this will match any to state.
|
|
134
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
|
135
|
+
# conditionals defined for each one. Default is true.
|
|
136
|
+
#
|
|
137
|
+
# Event arguments are passed to guard conditions if they accept multiple parameters.
|
|
138
|
+
def transition_for(object, requirements = {}, *event_args)
|
|
139
|
+
StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
|
|
140
|
+
requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
|
|
141
|
+
|
|
142
|
+
branches.each do |branch|
|
|
143
|
+
next unless (match = branch.match(object, requirements, event_args))
|
|
144
|
+
|
|
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
|
+
|
|
158
|
+
# No transition matched
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Attempts to perform the next available transition on the given object.
|
|
163
|
+
# If no transitions can be made, then this will return false, otherwise
|
|
164
|
+
# true.
|
|
165
|
+
#
|
|
166
|
+
# Any additional arguments are passed to the StateMachines::Transition#perform
|
|
167
|
+
# instance method.
|
|
168
|
+
def fire(object, *event_args)
|
|
169
|
+
machine.reset(object)
|
|
170
|
+
|
|
171
|
+
if (transition = transition_for(object, {}, *event_args))
|
|
172
|
+
transition.perform(*event_args)
|
|
173
|
+
else
|
|
174
|
+
on_failure(object, *event_args)
|
|
175
|
+
false
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Marks the object as invalid and runs any failure callbacks associated with
|
|
180
|
+
# this event. This should get called anytime this event fails to transition.
|
|
181
|
+
def on_failure(object, *args)
|
|
182
|
+
state = machine.states.match!(object)
|
|
183
|
+
machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)], [:state, state.human_name(object.class)]])
|
|
184
|
+
|
|
185
|
+
transition = Transition.new(object, machine, name, state.name, state.name)
|
|
186
|
+
transition.args = args if args.any?
|
|
187
|
+
transition.run_callbacks(before: false)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Resets back to the initial state of the event, with no branches / known
|
|
191
|
+
# states associated. This allows you to redefine an event in situations
|
|
192
|
+
# where you either are re-using an existing state machine implementation
|
|
193
|
+
# or are subclassing machines.
|
|
194
|
+
def reset
|
|
195
|
+
@branches = []
|
|
196
|
+
@known_states = []
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def draw(graph, options = {}, io = $stdout)
|
|
200
|
+
machine.renderer.draw_event(self, graph, options, io)
|
|
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.flat_map do |branch|
|
|
212
|
+
branch.state_requirements.map do |state_requirement|
|
|
213
|
+
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
|
|
214
|
+
end
|
|
215
|
+
end.join(', ')
|
|
216
|
+
|
|
217
|
+
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions}]>"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
protected
|
|
221
|
+
|
|
222
|
+
# Add the various instance methods that can transition the object using
|
|
223
|
+
# the current event
|
|
224
|
+
def add_actions
|
|
225
|
+
# Checks whether the event can be fired on the current object
|
|
226
|
+
machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, *args, **kwargs|
|
|
227
|
+
machine.event(name).can_fire?(object, *args, **kwargs)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Gets the next transition that would be performed if the event were
|
|
231
|
+
# fired now
|
|
232
|
+
machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, *args, **kwargs|
|
|
233
|
+
machine.event(name).transition_for(object, *args, **kwargs)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Fires the event
|
|
237
|
+
machine.define_helper(:instance, qualified_name) do |machine, object, *args, **kwargs|
|
|
238
|
+
machine.event(name).fire(object, *args, **kwargs)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Fires the event, raising an exception if it fails
|
|
242
|
+
machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, *args, **kwargs|
|
|
243
|
+
object.send(qualified_name, *args, **kwargs) || raise(StateMachines::InvalidTransition.new(object, machine, name))
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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: %i[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
|
+
# TODO, simplify
|
|
120
|
+
# First try the regular event_transition
|
|
121
|
+
transition = machine.read(object, :event_transition)
|
|
122
|
+
|
|
123
|
+
# If not found and we have stored transitions by machine (issue #91)
|
|
124
|
+
if !transition && (transitions_by_machine = object.instance_variable_get(:@_state_machine_event_transitions))
|
|
125
|
+
transition = transitions_by_machine[machine.name]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
transition || if event_name = machine.read(object, :event)
|
|
129
|
+
if event = self[event_name.to_sym, :name]
|
|
130
|
+
event.transition_for(object) || begin
|
|
131
|
+
# No valid transition: invalidate
|
|
132
|
+
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
# Event is unknown: invalidate
|
|
137
|
+
machine.invalidate(object, :event, :invalid) if invalidate
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def match(requirements) # :nodoc:
|
|
146
|
+
requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|