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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Parsing
6
+ private
7
+
8
+ # Parses callback arguments for backward compatibility with both positional
9
+ # and keyword argument styles. Supports Ruby 3.2+ keyword arguments while
10
+ # maintaining full backward compatibility with the legacy API.
11
+ def parse_callback_arguments(args, options)
12
+ # Handle legacy positional args: before_transition(:method1, :method2, from: :state)
13
+ if args.any?
14
+ # Extract hash options from the end of args if present
15
+ parsed_options = args.last.is_a?(Hash) ? args.pop.dup : {}
16
+
17
+ # Merge any additional keyword options
18
+ parsed_options.merge!(options) if options.any?
19
+
20
+ # Remaining args become the :do option (method names to call)
21
+ parsed_options[:do] = args if args.any?
22
+
23
+ parsed_options
24
+ else
25
+ # Pure keyword argument style: before_transition(from: :state, to: :other, do: :method)
26
+ options.dup
27
+ end
28
+ end
29
+
30
+ # Adds a new transition callback of the given type.
31
+ def add_callback(type, options, &)
32
+ callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &)
33
+ add_states(callback.known_states)
34
+ callback
35
+ end
36
+
37
+ # Tracks the given set of states in the list of all known states for
38
+ # this machine
39
+ def add_states(new_states)
40
+ new_states.map do |new_state|
41
+ # Check for other states that use a different class type for their name.
42
+ # This typically prevents string / symbol misuse.
43
+ if new_state && (conflict = states.detect { |state| state.name && state.name.class != new_state.class })
44
+ raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
45
+ end
46
+
47
+ unless (state = states[new_state])
48
+ states << state = State.new(self, new_state)
49
+
50
+ # Copy states over to sibling machines
51
+ sibling_machines.each { |machine| machine.states << state }
52
+ end
53
+
54
+ state
55
+ end
56
+ end
57
+
58
+ # Tracks the given set of events in the list of all known events for
59
+ # this machine
60
+ def add_events(new_events)
61
+ new_events.map do |new_event|
62
+ # Check for other states that use a different class type for their name.
63
+ # This typically prevents string / symbol misuse.
64
+ if (conflict = events.detect { |event| event.name.class != new_event.class })
65
+ raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
66
+ end
67
+
68
+ unless (event = events[new_event])
69
+ events << event = Event.new(self, new_event)
70
+ end
71
+
72
+ event
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Rendering
6
+ # Gets the renderer for this machine.
7
+ def renderer
8
+ @renderer ||= StdioRenderer.new
9
+ end
10
+
11
+ # Generates a visual representation of this machine for a given format.
12
+ def draw(**)
13
+ renderer.draw(self, **)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Scoping
6
+ protected
7
+
8
+ # Defines the with/without scope helpers for this attribute. Both the
9
+ # singular and plural versions of the attribute are defined for each
10
+ # scope helper. A custom plural can be specified if it cannot be
11
+ # automatically determined by either calling +pluralize+ on the attribute
12
+ # name or adding an "s" to the end of the name.
13
+ def define_scopes(custom_plural = nil)
14
+ plural = custom_plural || pluralize(name)
15
+
16
+ %i[with without].each do |kind|
17
+ [name, plural].map(&:to_s).uniq.each do |suffix|
18
+ method = "#{kind}_#{suffix}"
19
+
20
+ next unless (scope = send("create_#{kind}_scope", method))
21
+
22
+ # Converts state names to their corresponding values so that they
23
+ # can be looked up properly
24
+ define_helper(:class, method) do |machine, klass, *states|
25
+ run_scope(scope, machine, klass, states)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ # Creates a scope for finding objects *with* a particular value or values
32
+ # for the attribute.
33
+ #
34
+ # By default, this is a no-op.
35
+ def create_with_scope(name); end
36
+
37
+ # Creates a scope for finding objects *without* a particular value or
38
+ # values for the attribute.
39
+ #
40
+ # By default, this is a no-op.
41
+ def create_without_scope(name); end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module StateMethods
6
+ # Gets the initial state of the machine for the given object. If a dynamic
7
+ # initial state was configured for this machine, then the object will be
8
+ # passed into the lambda block to help determine the actual state.
9
+ #
10
+ # == Examples
11
+ #
12
+ # With a static initial state:
13
+ #
14
+ # class Vehicle
15
+ # state_machine :initial => :parked do
16
+ # ...
17
+ # end
18
+ # end
19
+ #
20
+ # vehicle = Vehicle.new
21
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
22
+ #
23
+ # With a dynamic initial state:
24
+ #
25
+ # class Vehicle
26
+ # attr_accessor :force_idle
27
+ #
28
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
29
+ # ...
30
+ # end
31
+ # end
32
+ #
33
+ # vehicle = Vehicle.new
34
+ #
35
+ # vehicle.force_idle = true
36
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
37
+ #
38
+ # vehicle.force_idle = false
39
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
40
+ def initial_state(object)
41
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
42
+ end
43
+
44
+ # Whether a dynamic initial state is being used in the machine
45
+ def dynamic_initial_state?
46
+ instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
47
+ end
48
+
49
+ # Initializes the state on the given object. Initial values are only set if
50
+ # the machine's attribute hasn't been previously initialized.
51
+ #
52
+ # Configuration options:
53
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
54
+ # current value
55
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
56
+ # directly to the object
57
+ def initialize_state(object, options = {})
58
+ state = initial_state(object)
59
+ return unless state && (options[:force] || initialize_state?(object))
60
+
61
+ value = state.value
62
+
63
+ if (hash = options[:to])
64
+ hash[attribute.to_s] = value
65
+ else
66
+ write(object, :state, value)
67
+ end
68
+ end
69
+
70
+ # Customizes the definition of one or more states in the machine.
71
+ #
72
+ # Configuration options:
73
+ # * <tt>:value</tt> - The actual value to store when an object transitions
74
+ # to the state. Default is the name (stringified).
75
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
76
+ # then setting this to true will cache the evaluated result
77
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
78
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
79
+ # By default, the configured value is matched.
80
+ # * <tt>:human_name</tt> - The human-readable version of this state's name.
81
+ # By default, this is either defined by the integration or stringifies the
82
+ # name and converts underscores to spaces.
83
+ #
84
+ # == Customizing the stored value
85
+ #
86
+ # Whenever a state is automatically discovered in the state machine, its
87
+ # default value is assumed to be the stringified version of the name. For
88
+ # example,
89
+ #
90
+ # class Vehicle
91
+ # state_machine :initial => :parked do
92
+ # event :ignite do
93
+ # transition :parked => :idling
94
+ # end
95
+ # end
96
+ # end
97
+ #
98
+ # In the above state machine, there are two states automatically discovered:
99
+ # :parked and :idling. These states, by default, will store their stringified
100
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
101
+ #
102
+ # For legacy systems or when tying state machines into existing frameworks,
103
+ # it's oftentimes necessary to need to store a different value for a state
104
+ # than the default. In order to continue taking advantage of an expressive
105
+ # state machine and helper methods, every defined state can be re-configured
106
+ # with a custom stored value. For example,
107
+ #
108
+ # class Vehicle
109
+ # state_machine :initial => :parked do
110
+ # event :ignite do
111
+ # transition :parked => :idling
112
+ # end
113
+ #
114
+ # state :idling, :value => 'IDLING'
115
+ # state :parked, :value => 'PARKED
116
+ # end
117
+ # end
118
+ #
119
+ # This is also useful if being used in association with a database and,
120
+ # instead of storing the state name in a column, you want to store the
121
+ # state's foreign key:
122
+ #
123
+ # class VehicleState < ActiveRecord::Base
124
+ # end
125
+ #
126
+ # class Vehicle < ActiveRecord::Base
127
+ # state_machine :attribute => :state_id, :initial => :parked do
128
+ # event :ignite do
129
+ # transition :parked => :idling
130
+ # end
131
+ #
132
+ # states.each do |state|
133
+ # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
134
+ # end
135
+ # end
136
+ # end
137
+ #
138
+ # In the above example, each known state is configured to store it's
139
+ # associated database id in the +state_id+ attribute. Also, notice that a
140
+ # lambda block is used to define the state's value. This is required in
141
+ # situations (like testing) where the model is loaded without any existing
142
+ # data (i.e. no VehicleState records available).
143
+ #
144
+ # One caveat to the above example is to keep performance in mind. To avoid
145
+ # constant db hits for looking up the VehicleState ids, the value is cached
146
+ # by specifying the <tt>:cache</tt> option. Alternatively, a custom
147
+ # caching strategy can be used like so:
148
+ #
149
+ # class VehicleState < ActiveRecord::Base
150
+ # cattr_accessor :cache_store
151
+ # self.cache_store = ActiveSupport::Cache::MemoryStore.new
152
+ #
153
+ # def self.find_by_name(name)
154
+ # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
155
+ # end
156
+ # end
157
+ #
158
+ # === Dynamic values
159
+ #
160
+ # In addition to customizing states with other value types, lambda blocks
161
+ # can also be specified to allow for a state's value to be determined
162
+ # dynamically at runtime. For example,
163
+ #
164
+ # class Vehicle
165
+ # state_machine :purchased_at, :initial => :available do
166
+ # event :purchase do
167
+ # transition all => :purchased
168
+ # end
169
+ #
170
+ # event :restock do
171
+ # transition all => :available
172
+ # end
173
+ #
174
+ # state :available, :value => nil
175
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
176
+ # end
177
+ # end
178
+ #
179
+ # In the above definition, the <tt>:purchased</tt> state is customized with
180
+ # both a dynamic value *and* a value matcher.
181
+ #
182
+ # When an object transitions to the purchased state, the value's lambda
183
+ # block will be called. This will get the current time and store it in the
184
+ # object's +purchased_at+ attribute.
185
+ #
186
+ # *Note* that the custom matcher is very important here. Since there's no
187
+ # way for the state machine to figure out an object's state when it's set to
188
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
189
+ # were not configured for the state, then an ArgumentError exception would
190
+ # be raised at runtime, indicating that the state machine could not figure
191
+ # out what the current state of the object was.
192
+ #
193
+ # == Behaviors
194
+ #
195
+ # Behaviors define a series of methods to mixin with objects when the current
196
+ # state matches the given one(s). This allows instance methods to behave
197
+ # a specific way depending on what the value of the object's state is.
198
+ #
199
+ # For example,
200
+ #
201
+ # class Vehicle
202
+ # attr_accessor :driver
203
+ # attr_accessor :passenger
204
+ #
205
+ # state_machine :initial => :parked do
206
+ # event :ignite do
207
+ # transition :parked => :idling
208
+ # end
209
+ #
210
+ # state :parked do
211
+ # def speed
212
+ # 0
213
+ # end
214
+ #
215
+ # def rotate_driver
216
+ # driver = self.driver
217
+ # self.driver = passenger
218
+ # self.passenger = driver
219
+ # true
220
+ # end
221
+ # end
222
+ #
223
+ # state :idling, :first_gear do
224
+ # def speed
225
+ # 20
226
+ # end
227
+ #
228
+ # def rotate_driver
229
+ # self.state = 'parked'
230
+ # rotate_driver
231
+ # end
232
+ # end
233
+ #
234
+ # other_states :backing_up
235
+ # end
236
+ # end
237
+ #
238
+ # In the above example, there are two dynamic behaviors defined for the
239
+ # class:
240
+ # * +speed+
241
+ # * +rotate_driver+
242
+ #
243
+ # Each of these behaviors are instance methods on the Vehicle class. However,
244
+ # which method actually gets invoked is based on the current state of the
245
+ # object. Using the above class as the example:
246
+ #
247
+ # vehicle = Vehicle.new
248
+ # vehicle.driver = 'John'
249
+ # vehicle.passenger = 'Jane'
250
+ #
251
+ # # Behaviors in the "parked" state
252
+ # vehicle.state # => "parked"
253
+ # vehicle.speed # => 0
254
+ # vehicle.rotate_driver # => true
255
+ # vehicle.driver # => "Jane"
256
+ # vehicle.passenger # => "John"
257
+ #
258
+ # vehicle.ignite # => true
259
+ #
260
+ # # Behaviors in the "idling" state
261
+ # vehicle.state # => "idling"
262
+ # vehicle.speed # => 20
263
+ # vehicle.rotate_driver # => true
264
+ # vehicle.driver # => "John"
265
+ # vehicle.passenger # => "Jane"
266
+ #
267
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
268
+ # implementations changed how they behave based on what the current state
269
+ # of the vehicle was.
270
+ #
271
+ # === Invalid behaviors
272
+ #
273
+ # If a specific behavior has not been defined for a state, then a
274
+ # NoMethodError exception will be raised, indicating that that method would
275
+ # not normally exist for an object with that state.
276
+ #
277
+ # Using the example from before:
278
+ #
279
+ # vehicle = Vehicle.new
280
+ # vehicle.state = 'backing_up'
281
+ # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
282
+ #
283
+ # === Using matchers
284
+ #
285
+ # The +all+ / +any+ matchers can be used to easily define behaviors for a
286
+ # group of states. Note, however, that you cannot use these matchers to
287
+ # set configurations for states. Behaviors using these matchers can be
288
+ # defined at any point in the state machine and will always get applied to
289
+ # the proper states.
290
+ #
291
+ # For example:
292
+ #
293
+ # state_machine :initial => :parked do
294
+ # ...
295
+ #
296
+ # state all - [:parked, :idling, :stalled] do
297
+ # validates_presence_of :speed
298
+ #
299
+ # def speed
300
+ # gear * 10
301
+ # end
302
+ # end
303
+ # end
304
+ #
305
+ # == State-aware class methods
306
+ #
307
+ # In addition to defining scopes for instance methods that are state-aware,
308
+ # the same can be done for certain types of class methods.
309
+ #
310
+ # Some libraries have support for class-level methods that only run certain
311
+ # behaviors based on a conditions hash passed in. For example:
312
+ #
313
+ # class Vehicle < ActiveRecord::Base
314
+ # state_machine do
315
+ # ...
316
+ # state :first_gear, :second_gear, :third_gear do
317
+ # validates_presence_of :speed
318
+ # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
319
+ # end
320
+ # end
321
+ # end
322
+ #
323
+ # In the above ActiveRecord model, two validations have been defined which
324
+ # will *only* run when the Vehicle object is in one of the three states:
325
+ # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
326
+ # conditions can continue to be used.
327
+ #
328
+ # This functionality is not library-specific and can work for any class-level
329
+ # method that is defined like so:
330
+ #
331
+ # def validates_presence_of(attribute, options = {})
332
+ # ...
333
+ # end
334
+ #
335
+ # The minimum requirement is that the last argument in the method be an
336
+ # options hash which contains at least <tt>:if</tt> condition support.
337
+ def state(*names, &)
338
+ options = names.last.is_a?(Hash) ? names.pop : {}
339
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
340
+
341
+ # Store the context so that it can be used for / matched against any state
342
+ # that gets added
343
+ @states.context(names, &) if block_given?
344
+
345
+ if names.first.is_a?(Matcher)
346
+ # Add any states referenced in the matcher. When matchers are used,
347
+ # states are not allowed to be configured.
348
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
349
+
350
+ states = add_states(names.first.values)
351
+ else
352
+ states = add_states(names)
353
+
354
+ # Update the configuration for the state(s)
355
+ states.each do |state|
356
+ if options.include?(:value)
357
+ state.value = options[:value]
358
+ self.states.update(state)
359
+ end
360
+
361
+ state.human_name = options[:human_name] if options.include?(:human_name)
362
+ state.cache = options[:cache] if options.include?(:cache)
363
+ state.matcher = options[:if] if options.include?(:if)
364
+ end
365
+ end
366
+
367
+ states.length == 1 ? states.first : states
368
+ end
369
+
370
+ alias other_states state
371
+
372
+ # Gets the current value stored in the given object's attribute.
373
+ def read(object, attribute, ivar = false)
374
+ attribute = self.attribute(attribute)
375
+ if ivar
376
+ object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
377
+ else
378
+ object.send(attribute)
379
+ end
380
+ end
381
+
382
+ # Sets a new value in the given object's attribute.
383
+ def write(object, attribute, value, ivar = false)
384
+ attribute = self.attribute(attribute)
385
+ ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
386
+ end
387
+
388
+ protected
389
+
390
+ # Determines if the machine's attribute needs to be initialized. This
391
+ # will only be true if the machine's attribute is blank.
392
+ def initialize_state?(object)
393
+ value = read(object, :state)
394
+ (value.nil? || (value.respond_to?(:empty?) && value.empty?)) && !states[value, :value]
395
+ end
396
+ end
397
+ end
398
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Utilities
6
+ protected
7
+
8
+ # Looks up other machines that have been defined in the owner class and
9
+ # are targeting the same attribute as this machine. When accessing
10
+ # sibling machines, they will be automatically copied for the current
11
+ # class if they haven't been already. This ensures that any configuration
12
+ # changes made to the sibling machines only affect this class and not any
13
+ # base class that may have originally defined the machine.
14
+ def sibling_machines
15
+ owner_class.state_machines.each_with_object([]) do |(name, machine), machines|
16
+ machines << (owner_class.state_machine(name) {}) if machine.attribute == attribute && machine != self
17
+ end
18
+ end
19
+
20
+ # Determines whether there's already a helper method defined within the
21
+ # given scope. This is true only if one of the owner's ancestors defines
22
+ # the method and is further along in the ancestor chain than this
23
+ # machine's helper module.
24
+ def owner_class_ancestor_has_method?(scope, method)
25
+ return false unless owner_class_has_method?(scope, method)
26
+
27
+ superclasses = owner_class.ancestors.select { |ancestor| ancestor.is_a?(Class) }[1..]
28
+
29
+ if scope == :class
30
+ current = owner_class.singleton_class
31
+ superclass = superclasses.first
32
+ else
33
+ current = owner_class
34
+ superclass = owner_class.superclass
35
+ end
36
+
37
+ # Generate the list of modules that *only* occur in the owner class, but
38
+ # were included *prior* to the helper modules, in addition to the
39
+ # superclasses
40
+ ancestors = current.ancestors - superclass.ancestors + superclasses
41
+ helper_module_index = ancestors.index(@helper_modules[scope])
42
+ ancestors = helper_module_index ? ancestors[helper_module_index..].reverse : ancestors.reverse
43
+
44
+ # Search for for the first ancestor that defined this method
45
+ ancestors.detect do |ancestor|
46
+ ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
47
+ ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
48
+ end
49
+ end
50
+
51
+ # Determines whether the given method is defined in the owner class or
52
+ # in a superclass.
53
+ def owner_class_has_method?(scope, method)
54
+ target = scope == :class ? owner_class.singleton_class : owner_class
55
+ target.method_defined?(method) || target.private_method_defined?(method)
56
+ end
57
+
58
+ # Pluralizes the given word using #pluralize (if available) or simply
59
+ # adding an "s" to the end of the word
60
+ def pluralize(word)
61
+ word = word.to_s
62
+ if word.respond_to?(:pluralize)
63
+ word.pluralize
64
+ else
65
+ "#{word}s"
66
+ end
67
+ end
68
+
69
+ # Generates the results for the given scope based on one or more states to
70
+ # filter by
71
+ def run_scope(scope, machine, klass, states)
72
+ values = states.flatten.compact.map { |state| machine.states.fetch(state).value }
73
+ scope.call(klass, values)
74
+ end
75
+
76
+ # Adds sibling machine configurations to the current machine. This
77
+ # will add states from other machines that have the same attribute.
78
+ def add_sibling_machine_configs
79
+ # Add existing states
80
+ sibling_machines.each do |machine|
81
+ machine.states.each { |state| states << state unless states[state.name] }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Validation
6
+ # Frozen constant to avoid repeated array allocations
7
+ DANGEROUS_PATTERNS = [
8
+ /`.*`/, # Backticks (shell execution)
9
+ /system\s*\(/, # System calls
10
+ /exec\s*\(/, # Exec calls
11
+ /eval\s*\(/, # Nested eval
12
+ /require\s+['"]/, # Require statements
13
+ /load\s+['"]/, # Load statements
14
+ /File\./, # File operations
15
+ /IO\./, # IO operations
16
+ /Dir\./, # Directory operations
17
+ /Kernel\./ # Kernel operations
18
+ ].freeze
19
+
20
+ private
21
+
22
+ # Validates string input before eval to prevent code injection
23
+ # This is a basic safety check - not foolproof security
24
+ def validate_eval_string(method_string)
25
+ # Check for obviously dangerous patterns
26
+ DANGEROUS_PATTERNS.each do |pattern|
27
+ raise SecurityError, "Potentially dangerous code detected in eval string: #{method_string.inspect}" if method_string.match?(pattern)
28
+ end
29
+
30
+ # Basic syntax validation (cross-platform)
31
+ begin
32
+ SyntaxValidator.validate!(method_string, '(eval)')
33
+ rescue SyntaxError => e
34
+ raise ArgumentError, "Invalid Ruby syntax in eval string: #{e.message}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end