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,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
|