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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/state_machines-0.201.0/LICENSE.txt +23 -0
  3. data/state_machines-0.201.0/README.md +1024 -0
  4. data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
  5. data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
  6. data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
  7. data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
  8. data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
  9. data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
  10. data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
  11. data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
  12. data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
  13. data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  14. data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
  15. data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
  16. data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
  17. data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
  18. data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
  19. data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
  20. data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
  21. data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
  22. data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
  23. data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
  24. data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
  25. data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
  26. data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
  27. data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
  28. data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
  29. data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
  30. data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
  31. data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
  32. data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
  33. data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
  34. data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
  35. data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
  36. data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
  37. data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
  38. data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
  39. data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
  40. data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
  41. data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
  42. data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
  43. data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
  44. data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
  45. data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
  46. data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
  47. data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
  48. data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
  49. data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
  50. data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
  51. data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
  52. data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
  53. data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
  54. data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
  55. data/state_machines-0.201.0/lib/state_machines.rb +6 -0
  56. data/ultra-smart-sys.gemspec +12 -0
  57. metadata +96 -0
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
5
+ module StateMachines
6
+ # A state defines a value that an attribute can be in after being transitioned
7
+ # 0 or more times. States can represent a value of any type in Ruby, though
8
+ # the most common (and default) type is String.
9
+ #
10
+ # In addition to defining the machine's value, a state can also define a
11
+ # behavioral context for an object when that object is in the state. See
12
+ # StateMachines::Machine#state for more information about how state-driven
13
+ # behavior can be utilized.
14
+ class State
15
+ # The state machine for which this state is defined
16
+ attr_reader :machine
17
+
18
+ # The unique identifier for the state used in event and callback definitions
19
+ attr_reader :name
20
+
21
+ # The fully-qualified identifier for the state, scoped by the machine's
22
+ # namespace
23
+ attr_reader :qualified_name
24
+
25
+ # The human-readable name for the state
26
+ attr_writer :human_name
27
+
28
+ # The value that is written to a machine's attribute when an object
29
+ # transitions into this state
30
+ attr_writer :value
31
+
32
+ # Whether this state's value should be cached after being evaluated
33
+ attr_accessor :cache
34
+
35
+ # Whether or not this state is the initial state to use for new objects
36
+ attr_accessor :initial
37
+ alias initial? initial
38
+
39
+ # A custom lambda block for determining whether a given value matches this
40
+ # state
41
+ attr_accessor :matcher
42
+
43
+ # Creates a new state within the context of the given machine.
44
+ #
45
+ # Configuration options:
46
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
47
+ # machine. Default is false.
48
+ # * <tt>:value</tt> - The value to store when an object transitions to this
49
+ # state. Default is the name (stringified).
50
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
51
+ # then setting this to true will cache the evaluated result
52
+ # * <tt>:if</tt> - Determines whether a value matches this state
53
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
54
+ # By default, the configured value is matched.
55
+ # * <tt>:human_name</tt> - The human-readable version of this state's name
56
+ def initialize(machine, name, options = nil, initial: false, value: :__not_provided__, cache: nil, if: nil, human_name: nil, **extra_options) # :nodoc:
57
+ # Handle both old hash style and new kwargs style for backward compatibility
58
+ case options
59
+ in Hash
60
+ # Old style: initialize(machine, name, {initial: true, value: 'foo'})
61
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :initial, :value, :cache, :if, :human_name)
62
+ initial = options.fetch(:initial, false)
63
+ value = options.include?(:value) ? options[:value] : :__not_provided__
64
+ cache = options[:cache]
65
+ if_condition = options[:if]
66
+ human_name = options[:human_name]
67
+ in nil
68
+ # New style: initialize(machine, name, initial: true, value: 'foo')
69
+ StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty?
70
+ if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
71
+ else
72
+ # Handle unexpected options
73
+ raise ArgumentError, "Unexpected positional argument in State initialize: #{options.inspect}"
74
+ end
75
+
76
+ @machine = machine
77
+ @name = name
78
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
79
+ @human_name = human_name || (@name ? @name.to_s.tr('_', ' ') : 'nil')
80
+ @value = value == :__not_provided__ ? name&.to_s : value
81
+ @cache = cache
82
+ @matcher = if_condition
83
+ @initial = initial == true
84
+ @context = StateContext.new(self)
85
+
86
+ return unless name
87
+
88
+ conflicting_machines = machine.owner_class.state_machines.select do |_other_name, other_machine|
89
+ other_machine != machine && other_machine.states[qualified_name, :qualified_name]
90
+ end
91
+
92
+ # Output a warning if another machine has a conflicting qualified name
93
+ # for a different attribute
94
+ if (conflict = conflicting_machines.detect do |_other_name, other_machine|
95
+ other_machine.attribute != machine.attribute
96
+ end)
97
+ _name, other_machine = conflict
98
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
99
+ elsif conflicting_machines.empty?
100
+ # Only bother adding predicates when another machine for the same
101
+ # attribute hasn't already done so
102
+ add_predicate
103
+ end
104
+ end
105
+
106
+ # Creates a copy of this state, excluding the context to prevent conflicts
107
+ # across different machines.
108
+ def initialize_copy(orig) # :nodoc:
109
+ super
110
+ @context = StateContext.new(self)
111
+ end
112
+
113
+ def machine=(machine)
114
+ @machine = machine
115
+ @context = StateContext.new(self)
116
+ end
117
+
118
+ # Determines whether there are any states that can be transitioned to from
119
+ # this state. If there are none, then this state is considered *final*.
120
+ # Any objects in a final state will remain so forever given the current
121
+ # machine's definition.
122
+ def final?
123
+ machine.events.none? do |event|
124
+ event.branches.any? do |branch|
125
+ branch.state_requirements.any? do |requirement|
126
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, from: name)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Transforms the state name into a more human-readable format, such as
133
+ # "first gear" instead of "first_gear"
134
+ def human_name(klass = @machine.owner_class)
135
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
136
+ end
137
+
138
+ # Generates a human-readable description of this state's name / value:
139
+ #
140
+ # For example,
141
+ #
142
+ # State.new(machine, :parked).description # => "parked"
143
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
144
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
145
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
146
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
147
+ #
148
+ # Configuration options:
149
+ # * <tt>:human_name</tt> - Whether to use this state's human name in the
150
+ # description or just the internal name
151
+ def description(options = {})
152
+ label = options[:human_name] ? human_name : name
153
+ description = +(label ? label.to_s : label.inspect)
154
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
155
+ description
156
+ end
157
+
158
+ # The value that represents this state. This will optionally evaluate the
159
+ # original block if it's a lambda block. Otherwise, the static value is
160
+ # returned.
161
+ #
162
+ # For example,
163
+ #
164
+ # State.new(machine, :parked, :value => 1).value # => 1
165
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
166
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
167
+ def value(eval = true)
168
+ if @value.is_a?(Proc) && eval
169
+ if cache_value?
170
+ @value = @value.call
171
+ machine.states.update(self)
172
+ @value
173
+ else
174
+ @value.call
175
+ end
176
+ else
177
+ @value
178
+ end
179
+ end
180
+
181
+ # Determines whether this state matches the given value. If no matcher is
182
+ # configured, then this will check whether the values are equivalent.
183
+ # Otherwise, the matcher will determine the result.
184
+ #
185
+ # For example,
186
+ #
187
+ # # Without a matcher
188
+ # state = State.new(machine, :parked, :value => 1)
189
+ # state.matches?(1) # => true
190
+ # state.matches?(2) # => false
191
+ #
192
+ # # With a matcher
193
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
194
+ # state.matches?(nil) # => false
195
+ # state.matches?(Time.now) # => true
196
+ def matches?(other_value)
197
+ matcher ? matcher.call(other_value) : other_value == value
198
+ end
199
+
200
+ # Defines a context for the state which will be enabled on instances of
201
+ # the owner class when the machine is in this state.
202
+ #
203
+ # This can be called multiple times. Each time a new context is created,
204
+ # a new module will be included in the owner class.
205
+ def context(&)
206
+ # Include the context
207
+ context = @context
208
+ machine.owner_class.class_eval { include context }
209
+
210
+ # Evaluate the method definitions and track which ones were added
211
+ old_methods = context_methods
212
+ context.class_eval(&)
213
+ new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method }
214
+
215
+ # Alias new methods so that the only execute when the object is in this state
216
+ new_methods.each do |(method_name, _method)|
217
+ context_name = context_name_for(method_name)
218
+ context.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
219
+ alias_method :"#{context_name}", :#{method_name}
220
+ def #{method_name}(*args, &block)
221
+ state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
222
+ options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
223
+ state.call(self, :"#{context_name}", *(args + [options]), &block)
224
+ end
225
+ END_EVAL
226
+ end
227
+
228
+ true
229
+ end
230
+
231
+ # The list of methods that have been defined in this state's context
232
+ def context_methods
233
+ @context.instance_methods.inject({}) do |methods, name|
234
+ methods.merge(name.to_sym => @context.instance_method(name))
235
+ end
236
+ end
237
+
238
+ # Calls a method defined in this state's context on the given object. All
239
+ # arguments and any block will be passed into the method defined.
240
+ #
241
+ # If the method has never been defined for this state, then a NoMethodError
242
+ # will be raised.
243
+ def call(object, method, *args, &)
244
+ options = args.last.is_a?(Hash) ? args.pop : {}
245
+ options = { method_name: method }.merge(options)
246
+ state = machine.states.match!(object)
247
+
248
+ if state == self && object.respond_to?(method)
249
+ object.send(method, *args, &)
250
+ elsif (method_missing = options[:method_missing])
251
+ # Dispatch to the superclass since the object either isn't in this state
252
+ # or this state doesn't handle the method
253
+ begin
254
+ method_missing.call
255
+ rescue NoMethodError => e
256
+ raise unless e.name.to_s == options[:method_name].to_s && e.args == args
257
+
258
+ # No valid context for this method
259
+ raise InvalidContext.new(object,
260
+ "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
261
+ end
262
+ end
263
+ end
264
+
265
+ def draw(graph, options = {}, io = $stdout)
266
+ machine.renderer.draw_state(self, graph, options, io)
267
+ end
268
+
269
+ # Generates a nicely formatted description of this state's contents.
270
+ #
271
+ # For example,
272
+ #
273
+ # state = StateMachines::State.new(machine, :parked, :value => 1, :initial => true)
274
+ # state # => #<StateMachines::State name=:parked value=1 initial=true context=[]>
275
+ def inspect
276
+ attributes = [[:name, name], [:value, @value], [:initial, initial?]]
277
+ "#<#{self.class} #{attributes.map { |attr, value| "#{attr}=#{value.inspect}" } * ' '}>"
278
+ end
279
+
280
+ private
281
+
282
+ # Should the value be cached after it's evaluated for the first time?
283
+ def cache_value?
284
+ @cache
285
+ end
286
+
287
+ # Adds a predicate method to the owner class so long as a name has
288
+ # actually been configured for the state
289
+ def add_predicate
290
+ predicate_method = "#{qualified_name}?"
291
+
292
+ if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method)
293
+ warn_about_method_conflict(predicate_method, machine.owner_class.ancestors.first)
294
+ elsif machine.send(:owner_class_has_method?, :instance, predicate_method)
295
+ warn_about_method_conflict(predicate_method, machine.owner_class)
296
+ else
297
+ machine.define_helper(:instance, predicate_method) do |machine, object|
298
+ machine.states.matches?(object, name)
299
+ end
300
+ end
301
+ end
302
+
303
+ # Generates the name of the method containing the actual implementation
304
+ def context_name_for(method)
305
+ :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
306
+ end
307
+
308
+ def warn_about_method_conflict(method, defined_in)
309
+ return if StateMachines::Machine.ignore_method_conflicts
310
+
311
+ warn "Instance method #{method.inspect} is already defined in #{defined_in.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Represents a collection of states in a state machine
5
+ class StateCollection < NodeCollection
6
+ def initialize(machine) # :nodoc:
7
+ super(machine, index: %i[name qualified_name value])
8
+ end
9
+
10
+ # Determines whether the given object is in a specific state. If the
11
+ # object's current value doesn't match the state, then this will return
12
+ # false, otherwise true. If the given state is unknown, then an IndexError
13
+ # will be raised.
14
+ #
15
+ # == Examples
16
+ #
17
+ # class Vehicle
18
+ # state_machine :initial => :parked do
19
+ # other_states :idling
20
+ # end
21
+ # end
22
+ #
23
+ # states = Vehicle.state_machine.states
24
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
25
+ #
26
+ # states.matches?(vehicle, :parked) # => true
27
+ # states.matches?(vehicle, :idling) # => false
28
+ # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
29
+ def matches?(object, name)
30
+ fetch(name).matches?(machine.read(object, :state))
31
+ end
32
+
33
+ # Determines the current state of the given object as configured by this
34
+ # state machine. This will attempt to find a known state that matches
35
+ # the value of the attribute on the object.
36
+ #
37
+ # == Examples
38
+ #
39
+ # class Vehicle
40
+ # state_machine :initial => :parked do
41
+ # other_states :idling
42
+ # end
43
+ # end
44
+ #
45
+ # states = Vehicle.state_machine.states
46
+ #
47
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
48
+ # states.match(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
49
+ #
50
+ # vehicle.state = 'idling'
51
+ # states.match(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=true>
52
+ #
53
+ # vehicle.state = 'invalid'
54
+ # states.match(vehicle) # => nil
55
+ def match(object)
56
+ value = machine.read(object, :state)
57
+ self[value, :value] || detect { |state| state.matches?(value) }
58
+ end
59
+
60
+ # Determines the current state of the given object as configured by this
61
+ # state machine. If no state is found, then an ArgumentError will be
62
+ # raised.
63
+ #
64
+ # == Examples
65
+ #
66
+ # class Vehicle
67
+ # state_machine :initial => :parked do
68
+ # other_states :idling
69
+ # end
70
+ # end
71
+ #
72
+ # states = Vehicle.state_machine.states
73
+ #
74
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
75
+ # states.match!(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
76
+ #
77
+ # vehicle.state = 'invalid'
78
+ # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
79
+ def match!(object)
80
+ match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
81
+ end
82
+
83
+ # Gets the order in which states should be displayed based on where they
84
+ # were first referenced. This will order states in the following priority:
85
+ #
86
+ # 1. Initial state
87
+ # 2. Event transitions (:from, :except_from, :to, :except_to options)
88
+ # 3. States with behaviors
89
+ # 4. States referenced via +state+ or +other_states+
90
+ # 5. States referenced in callbacks
91
+ #
92
+ # This order will determine how the GraphViz visualizations are rendered.
93
+ def by_priority
94
+ order = select { |state| state.initial }.map { |state| state.name }
95
+
96
+ machine.events.each { |event| order += event.known_states }
97
+ order += select { |state| state.context_methods.any? }.map { |state| state.name }
98
+ order += keys(:name) - machine.callbacks.values.flatten.flat_map(&:known_states)
99
+ order += keys(:name)
100
+
101
+ order.uniq!
102
+ order.map! { |name| self[name] }
103
+ order
104
+ end
105
+
106
+ private
107
+
108
+ # Gets the value for the given attribute on the node
109
+ def value(node, attribute)
110
+ attribute == :value ? node.value(false) : super
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
5
+ module StateMachines
6
+ # Represents a module which will get evaluated within the context of a state.
7
+ #
8
+ # Class-level methods are proxied to the owner class, injecting a custom
9
+ # <tt>:if</tt> condition along with method. This assumes that the method has
10
+ # support for a set of configuration options, including <tt>:if</tt>. This
11
+ # condition will check that the object's state matches this context's state.
12
+ #
13
+ # Instance-level methods are used to define state-driven behavior on the
14
+ # state's owner class.
15
+ #
16
+ # == Examples
17
+ #
18
+ # class Vehicle
19
+ # class << self
20
+ # attr_accessor :validations
21
+ #
22
+ # def validate(options, &block)
23
+ # validations << options
24
+ # end
25
+ # end
26
+ #
27
+ # self.validations = []
28
+ # attr_accessor :state, :simulate
29
+ #
30
+ # def moving?
31
+ # self.class.validations.all? {|validation| validation[:if].call(self)}
32
+ # end
33
+ # end
34
+ #
35
+ # In the above class, a simple set of validation behaviors have been defined.
36
+ # Each validation consists of a configuration like so:
37
+ #
38
+ # Vehicle.validate :unless => :simulate
39
+ # Vehicle.validate :if => lambda {|vehicle| ...}
40
+ #
41
+ # In order to scope validations to a particular state context, the class-level
42
+ # +validate+ method can be invoked like so:
43
+ #
44
+ # machine = StateMachines::Machine.new(Vehicle)
45
+ # context = StateMachines::StateContext.new(machine.state(:first_gear))
46
+ # context.validate(:unless => :simulate)
47
+ #
48
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
49
+ # vehicle.moving? # => false
50
+ #
51
+ # vehicle.state = 'first_gear'
52
+ # vehicle.moving? # => true
53
+ #
54
+ # vehicle.simulate = true
55
+ # vehicle.moving? # => false
56
+ class StateContext < Module
57
+ include EvalHelpers
58
+
59
+ # The state machine for which this context's state is defined
60
+ attr_reader :machine
61
+
62
+ # The state that must be present in an object for this context to be active
63
+ attr_reader :state
64
+
65
+ # Creates a new context for the given state
66
+ def initialize(state)
67
+ @state = state
68
+ @machine = state.machine
69
+
70
+ state_name = state.name
71
+ machine_name = machine.name
72
+ @condition = ->(object) { object.class.state_machine(machine_name).states.matches?(object, state_name) }
73
+ end
74
+
75
+ # Creates a new transition that determines what to change the current state
76
+ # to when an event fires from this state.
77
+ #
78
+ # Since this transition is being defined within a state context, you do
79
+ # *not* need to specify the <tt>:from</tt> option for the transition. For
80
+ # example:
81
+ #
82
+ # state_machine do
83
+ # state :parked do
84
+ # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
85
+ # transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
86
+ # end
87
+ # end
88
+ #
89
+ # See StateMachines::Machine#transition for a description of the possible
90
+ # configurations for defining transitions.
91
+ def transition(options)
92
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :on, :if, :unless)
93
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
94
+ raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
95
+
96
+ machine.transition(options.merge(options[:to] ? { from: state.name } : { to: state.name }))
97
+ end
98
+
99
+ # Hooks in condition-merging to methods that don't exist in this module
100
+ def method_missing(*args, &)
101
+ # Get the configuration
102
+ if args.last.is_a?(Hash)
103
+ options = args.last
104
+ else
105
+ args << options = {}
106
+ end
107
+
108
+ # Get any existing condition that may need to be merged
109
+ if_condition = options.delete(:if)
110
+ unless_condition = options.delete(:unless)
111
+
112
+ # Provide scope access to configuration in case the block is evaluated
113
+ # within the object instance
114
+ proxy = self
115
+ proxy_condition = @condition
116
+
117
+ # Replace the configuration condition with the one configured for this
118
+ # proxy, merging together any existing conditions
119
+ options[:if] = lambda do |*condition_args|
120
+ # Block may be executed within the context of the actual object, so
121
+ # it'll either be the first argument or the executing context
122
+ object = condition_args.first || self
123
+
124
+ proxy.evaluate_method(object, proxy_condition) &&
125
+ Array(if_condition).all? { |condition| proxy.evaluate_method(object, condition) } &&
126
+ !Array(unless_condition).any? { |condition| proxy.evaluate_method(object, condition) }
127
+ end
128
+
129
+ # Evaluate the method on the owner class with the condition proxied
130
+ # through
131
+ machine.owner_class.send(*args, &)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module STDIORenderer
5
+ module_function def draw_machine(machine, io: $stdout)
6
+ draw_class(machine: machine, io: io)
7
+ draw_states(machine: machine, io: io)
8
+ draw_events(machine: machine, io: io)
9
+ end
10
+
11
+ module_function def draw_class(machine:, io: $stdout)
12
+ io.puts "Class: #{machine.owner_class.name}"
13
+ end
14
+
15
+ module_function def draw_states(machine:, io: $stdout)
16
+ io.puts ' States:'
17
+ if machine.states.to_a.empty?
18
+ io.puts ' - None'
19
+ else
20
+ machine.states.each do |state|
21
+ io.puts " - #{state.name}"
22
+ end
23
+ end
24
+ end
25
+
26
+ module_function def draw_event(event, _graph, options: {}, io: $stdout)
27
+ io = io || options[:io] || $stdout
28
+ io.puts " Event: #{event.name}"
29
+ end
30
+
31
+ module_function def draw_branch(branch, _graph, _event, options: {}, io: $stdout)
32
+ io = io || options[:io] || $stdout
33
+ io.puts " Branch: #{branch.inspect}"
34
+ end
35
+
36
+ module_function def draw_state(state, _graph, options: {}, io: $stdout)
37
+ io = io || options[:io] || $stdout
38
+ io.puts " State: #{state.name}"
39
+ end
40
+
41
+ module_function def draw_events(machine:, io: $stdout)
42
+ io.puts ' Events:'
43
+ if machine.events.to_a.empty?
44
+ io.puts ' - None'
45
+ else
46
+ machine.events.each do |event|
47
+ io.puts " - #{event.name}"
48
+ event.branches.each do |branch|
49
+ branch.state_requirements.each do |requirement|
50
+ out = +' - '
51
+ out << "#{draw_requirement(requirement[:from])} => #{draw_requirement(requirement[:to])}"
52
+ out << " IF #{branch.if_condition}" if branch.if_condition
53
+ out << " UNLESS #{branch.unless_condition}" if branch.unless_condition
54
+ io.puts out
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ module_function def draw_requirement(requirement)
62
+ case requirement
63
+ when StateMachines::BlacklistMatcher
64
+ "ALL EXCEPT #{requirement.values.join(', ')}"
65
+ when StateMachines::AllMatcher
66
+ 'ALL'
67
+ when StateMachines::LoopbackMatcher
68
+ 'SAME'
69
+ else
70
+ requirement.values.join(', ')
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Cross-platform syntax validation for eval strings
5
+ # Supports CRuby, JRuby, TruffleRuby via pluggable backends
6
+ module SyntaxValidator
7
+ # Public API: raises SyntaxError if code is invalid
8
+ def validate!(code, filename = '(eval)')
9
+ backend.validate!(code, filename)
10
+ end
11
+ module_function :validate!
12
+
13
+ private
14
+
15
+ # Lazily pick the best backend for this platform
16
+ # Prefer RubyVM for performance on CRuby, fallback to eval for compatibility
17
+ def backend
18
+ @backend ||= if RubyVmBackend.available?
19
+ RubyVmBackend
20
+ else
21
+ UniversalBackend
22
+ end
23
+ end
24
+ module_function :backend
25
+
26
+ # MRI backend using RubyVM::InstructionSequence
27
+ module RubyVmBackend
28
+ def available?
29
+ RUBY_ENGINE == 'ruby'
30
+ end
31
+ module_function :available?
32
+
33
+ def validate!(code, filename)
34
+ # compile will raise a SyntaxError on bad syntax
35
+ RubyVM::InstructionSequence.compile(code, filename)
36
+ true
37
+ end
38
+ module_function :validate!
39
+ end
40
+
41
+ # Universal Ruby backend
42
+ module UniversalBackend
43
+ def validate!(code, filename)
44
+ code = code.b
45
+ code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) {
46
+ "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n"
47
+ }
48
+ code = code.force_encoding(Encoding::UTF_8)
49
+ catch { |tag| eval(code, binding, filename, __LINE__ - 1) } == :ok
50
+ end
51
+ module_function :validate!
52
+ end
53
+ end
54
+ end