state_machines 0.20.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +124 -13
- data/lib/state_machines/branch.rb +12 -13
- data/lib/state_machines/callback.rb +11 -12
- data/lib/state_machines/core.rb +0 -1
- data/lib/state_machines/error.rb +5 -4
- data/lib/state_machines/eval_helpers.rb +83 -45
- data/lib/state_machines/event.rb +23 -26
- data/lib/state_machines/event_collection.rb +4 -5
- data/lib/state_machines/extensions.rb +5 -5
- data/lib/state_machines/helper_module.rb +1 -1
- data/lib/state_machines/integrations/base.rb +1 -1
- data/lib/state_machines/integrations.rb +11 -14
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +25 -11
- data/lib/state_machines/machine/configuration.rb +124 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +73 -617
- data/lib/state_machines/machine_collection.rb +18 -14
- data/lib/state_machines/macro_methods.rb +2 -2
- data/lib/state_machines/matcher.rb +6 -6
- data/lib/state_machines/matcher_helpers.rb +1 -1
- data/lib/state_machines/node_collection.rb +18 -17
- data/lib/state_machines/path.rb +2 -4
- data/lib/state_machines/path_collection.rb +2 -3
- data/lib/state_machines/state.rb +6 -5
- data/lib/state_machines/state_collection.rb +3 -3
- data/lib/state_machines/state_context.rb +6 -7
- data/lib/state_machines/stdio_renderer.rb +16 -16
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +290 -27
- data/lib/state_machines/transition.rb +43 -41
- data/lib/state_machines/transition_collection.rb +22 -25
- data/lib/state_machines/version.rb +1 -1
- metadata +23 -9
data/lib/state_machines/event.rb
CHANGED
@@ -7,7 +7,6 @@ module StateMachines
|
|
7
7
|
# another. The state that an attribute is transitioned to depends on the
|
8
8
|
# branches configured for the event.
|
9
9
|
class Event
|
10
|
-
|
11
10
|
include MatcherHelpers
|
12
11
|
|
13
12
|
# The state machine for which this event is defined
|
@@ -34,7 +33,7 @@ module StateMachines
|
|
34
33
|
#
|
35
34
|
# Configuration options:
|
36
35
|
# * <tt>:human_name</tt> - The human-readable version of this event's name
|
37
|
-
def initialize(machine, name, options = nil, human_name: nil, **extra_options)
|
36
|
+
def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
|
38
37
|
# Handle both old hash style and new kwargs style for backward compatibility
|
39
38
|
if options.is_a?(Hash)
|
40
39
|
# Old style: initialize(machine, name, {human_name: 'Custom Name'})
|
@@ -43,6 +42,7 @@ module StateMachines
|
|
43
42
|
else
|
44
43
|
# New style: initialize(machine, name, human_name: 'Custom Name')
|
45
44
|
raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
|
45
|
+
|
46
46
|
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
|
47
47
|
end
|
48
48
|
|
@@ -63,7 +63,7 @@ module StateMachines
|
|
63
63
|
|
64
64
|
# Creates a copy of this event in addition to the list of associated
|
65
65
|
# branches to prevent conflicts across events within a class hierarchy.
|
66
|
-
def initialize_copy(orig)
|
66
|
+
def initialize_copy(orig) # :nodoc:
|
67
67
|
super
|
68
68
|
@branches = @branches.dup
|
69
69
|
@known_states = @known_states.dup
|
@@ -77,8 +77,8 @@ module StateMachines
|
|
77
77
|
|
78
78
|
# Evaluates the given block within the context of this event. This simply
|
79
79
|
# provides a DSL-like syntax for defining transitions.
|
80
|
-
def context(&
|
81
|
-
instance_eval(&
|
80
|
+
def context(&)
|
81
|
+
instance_eval(&)
|
82
82
|
end
|
83
83
|
|
84
84
|
# Creates a new transition that determines what to change the current state
|
@@ -102,9 +102,7 @@ module StateMachines
|
|
102
102
|
|
103
103
|
# Only a certain subset of explicit options are allowed for transition
|
104
104
|
# requirements
|
105
|
-
|
106
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless)
|
107
|
-
end
|
105
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - %i[from to on except_from except_to except_on if unless]).empty?
|
108
106
|
|
109
107
|
branches << branch = Branch.new(options.merge(on: name))
|
110
108
|
@known_states |= branch.known_states
|
@@ -138,19 +136,19 @@ module StateMachines
|
|
138
136
|
requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
|
139
137
|
|
140
138
|
branches.each do |branch|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
139
|
+
next unless (match = branch.match(object, requirements))
|
140
|
+
|
141
|
+
# Branch allows for the transition to occur
|
142
|
+
from = requirements[:from]
|
143
|
+
to = if match[:to].is_a?(LoopbackMatcher)
|
144
|
+
from
|
145
|
+
else
|
146
|
+
values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }
|
147
|
+
|
148
|
+
match[:to].filter(values).first
|
149
|
+
end
|
150
|
+
|
151
|
+
return Transition.new(object, machine, name, from, to, !custom_from_state)
|
154
152
|
end
|
155
153
|
|
156
154
|
# No transition matched
|
@@ -163,13 +161,13 @@ module StateMachines
|
|
163
161
|
#
|
164
162
|
# Any additional arguments are passed to the StateMachines::Transition#perform
|
165
163
|
# instance method.
|
166
|
-
def fire(object, *
|
164
|
+
def fire(object, *)
|
167
165
|
machine.reset(object)
|
168
166
|
|
169
167
|
if (transition = transition_for(object))
|
170
|
-
transition.perform(*
|
168
|
+
transition.perform(*)
|
171
169
|
else
|
172
|
-
on_failure(object, *
|
170
|
+
on_failure(object, *)
|
173
171
|
false
|
174
172
|
end
|
175
173
|
end
|
@@ -194,7 +192,6 @@ module StateMachines
|
|
194
192
|
@known_states = []
|
195
193
|
end
|
196
194
|
|
197
|
-
|
198
195
|
def draw(graph, options = {}, io = $stdout)
|
199
196
|
machine.renderer.draw_event(self, graph, options, io)
|
200
197
|
end
|
@@ -216,7 +213,7 @@ module StateMachines
|
|
216
213
|
"#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
|
217
214
|
end
|
218
215
|
|
219
|
-
|
216
|
+
protected
|
220
217
|
|
221
218
|
# Add the various instance methods that can transition the object using
|
222
219
|
# the current event
|
@@ -3,8 +3,8 @@
|
|
3
3
|
module StateMachines
|
4
4
|
# Represents a collection of events in a state machine
|
5
5
|
class EventCollection < NodeCollection
|
6
|
-
def initialize(machine)
|
7
|
-
super(machine, index: [
|
6
|
+
def initialize(machine) # :nodoc:
|
7
|
+
super(machine, index: %i[name qualified_name])
|
8
8
|
end
|
9
9
|
|
10
10
|
# Gets the list of events that can be fired on the given object.
|
@@ -130,12 +130,11 @@ module StateMachines
|
|
130
130
|
false
|
131
131
|
end
|
132
132
|
end
|
133
|
-
|
134
133
|
end
|
135
134
|
|
136
|
-
|
135
|
+
private
|
137
136
|
|
138
|
-
def match(requirements)
|
137
|
+
def match(requirements) # :nodoc:
|
139
138
|
requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
|
140
139
|
end
|
141
140
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module StateMachines
|
4
4
|
module ClassMethods
|
5
|
-
def self.extended(base)
|
5
|
+
def self.extended(base) # :nodoc:
|
6
6
|
base.class_eval do
|
7
7
|
@state_machines = MachineCollection.new
|
8
8
|
end
|
@@ -138,13 +138,13 @@ module StateMachines
|
|
138
138
|
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm
|
139
139
|
def fire_events!(*events)
|
140
140
|
run_action = [true, false].include?(events.last) ? events.pop : true
|
141
|
-
fire_events(*(events + [run_action])) ||
|
141
|
+
fire_events(*(events + [run_action])) || raise(StateMachines::InvalidParallelTransition.new(self, events))
|
142
142
|
end
|
143
143
|
|
144
|
-
|
144
|
+
protected
|
145
145
|
|
146
|
-
def initialize_state_machines(options = {}, &
|
147
|
-
self.class.state_machines.initialize_states(self, options, &
|
146
|
+
def initialize_state_machines(options = {}, &) # :nodoc:
|
147
|
+
self.class.state_machines.initialize_states(self, options, &)
|
148
148
|
end
|
149
149
|
end
|
150
150
|
end
|
@@ -26,15 +26,15 @@ module StateMachines
|
|
26
26
|
# Register integration
|
27
27
|
def register(name_or_module)
|
28
28
|
case name_or_module.class.to_s
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
when 'Module'
|
30
|
+
add(name_or_module)
|
31
|
+
else
|
32
|
+
raise IntegrationError
|
33
33
|
end
|
34
34
|
true
|
35
35
|
end
|
36
36
|
|
37
|
-
def reset
|
37
|
+
def reset # :nodoc:#
|
38
38
|
@integrations = []
|
39
39
|
end
|
40
40
|
|
@@ -47,12 +47,9 @@ module StateMachines
|
|
47
47
|
# StateMachines::Integrations.register(StateMachines::Integrations::ActiveModel)
|
48
48
|
# StateMachines::Integrations.integrations
|
49
49
|
# # => [StateMachines::Integrations::ActiveModel]
|
50
|
-
|
51
|
-
# Register all namespaced integrations
|
52
|
-
@integrations
|
53
|
-
end
|
50
|
+
attr_reader :integrations
|
54
51
|
|
55
|
-
|
52
|
+
alias list integrations
|
56
53
|
|
57
54
|
# Attempts to find an integration that matches the given class. This will
|
58
55
|
# look through all of the built-in integrations under the StateMachines::Integrations
|
@@ -102,12 +99,12 @@ module StateMachines
|
|
102
99
|
integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
|
103
100
|
end
|
104
101
|
|
105
|
-
|
102
|
+
private
|
106
103
|
|
107
104
|
def add(integration)
|
108
|
-
|
109
|
-
|
110
|
-
|
105
|
+
return unless integration.respond_to?(:integration_name)
|
106
|
+
|
107
|
+
@integrations.insert(0, integration) unless @integrations.include?(integration)
|
111
108
|
end
|
112
109
|
end
|
113
110
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
class Machine
|
5
|
+
module ActionHooks
|
6
|
+
protected
|
7
|
+
|
8
|
+
# Determines whether action helpers should be defined for this machine.
|
9
|
+
# This is only true if there is an action configured and no other machines
|
10
|
+
# have process this same configuration already.
|
11
|
+
def define_action_helpers?
|
12
|
+
action && owner_class.state_machines.none? { |_name, machine| machine.action == action && machine != self }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds helper methods for automatically firing events when an action
|
16
|
+
# is invoked
|
17
|
+
def define_action_helpers
|
18
|
+
return unless action_hook
|
19
|
+
|
20
|
+
@action_hook_defined = true
|
21
|
+
define_action_hook
|
22
|
+
end
|
23
|
+
|
24
|
+
# Hooks directly into actions by defining the same method in an included
|
25
|
+
# module. As a result, when the action gets invoked, any state events
|
26
|
+
# defined for the object will get run. Method visibility is preserved.
|
27
|
+
def define_action_hook
|
28
|
+
action_hook = self.action_hook
|
29
|
+
action = self.action
|
30
|
+
private_action_hook = owner_class.private_method_defined?(action_hook)
|
31
|
+
|
32
|
+
# Only define helper if it hasn't
|
33
|
+
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
34
|
+
def #{action_hook}(*)
|
35
|
+
self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
|
36
|
+
end
|
37
|
+
|
38
|
+
private #{action_hook.inspect} if #{private_action_hook}
|
39
|
+
END_EVAL
|
40
|
+
end
|
41
|
+
|
42
|
+
# The method to hook into for triggering transitions when invoked. By
|
43
|
+
# default, this is the action configured for the machine.
|
44
|
+
#
|
45
|
+
# Since the default hook technique relies on module inheritance, the
|
46
|
+
# action must be defined in an ancestor of the owner classs in order for
|
47
|
+
# it to be the action hook.
|
48
|
+
def action_hook
|
49
|
+
action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
class Machine
|
5
|
+
module Callbacks
|
6
|
+
# Creates a callback that will be invoked *before* a transition is
|
7
|
+
# performed so long as the given requirements match the transition.
|
8
|
+
def before_transition(*args, **options, &)
|
9
|
+
# Extract legacy positional arguments and merge with keyword options
|
10
|
+
parsed_options = parse_callback_arguments(args, options)
|
11
|
+
|
12
|
+
# Only validate callback-specific options, not state transition requirements
|
13
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
14
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
15
|
+
|
16
|
+
add_callback(:before, parsed_options, &)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates a callback that will be invoked *after* a transition is
|
20
|
+
# performed so long as the given requirements match the transition.
|
21
|
+
def after_transition(*args, **options, &)
|
22
|
+
# Extract legacy positional arguments and merge with keyword options
|
23
|
+
parsed_options = parse_callback_arguments(args, options)
|
24
|
+
|
25
|
+
# Only validate callback-specific options, not state transition requirements
|
26
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
27
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
28
|
+
|
29
|
+
add_callback(:after, parsed_options, &)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a callback that will be invoked *around* a transition so long
|
33
|
+
# as the given requirements match the transition.
|
34
|
+
def around_transition(*args, **options, &)
|
35
|
+
# Extract legacy positional arguments and merge with keyword options
|
36
|
+
parsed_options = parse_callback_arguments(args, options)
|
37
|
+
|
38
|
+
# Only validate callback-specific options, not state transition requirements
|
39
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
40
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
41
|
+
|
42
|
+
add_callback(:around, parsed_options, &)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creates a callback that will be invoked after a transition has failed
|
46
|
+
# to be performed.
|
47
|
+
def after_failure(*args, **options, &)
|
48
|
+
# Extract legacy positional arguments and merge with keyword options
|
49
|
+
parsed_options = parse_callback_arguments(args, options)
|
50
|
+
|
51
|
+
# Only validate callback-specific options, not state transition requirements
|
52
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
53
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
54
|
+
|
55
|
+
add_callback(:failure, parsed_options, &)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -14,14 +14,14 @@ module StateMachines
|
|
14
14
|
# If a machine of the given name already exists in one of the class's
|
15
15
|
# superclasses, then a copy of that machine will be created and stored
|
16
16
|
# in the new owner class (the original will remain unchanged).
|
17
|
-
def find_or_create(owner_class, *args, &
|
17
|
+
def find_or_create(owner_class, *args, &)
|
18
18
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
19
19
|
name = args.first || :state
|
20
20
|
|
21
21
|
# Find an existing machine
|
22
|
-
machine = owner_class.respond_to?(:state_machines) &&
|
23
|
-
(args.first && owner_class.state_machines[name] || !args.first &&
|
24
|
-
owner_class.state_machines.values.first) || nil
|
22
|
+
machine = (owner_class.respond_to?(:state_machines) &&
|
23
|
+
((args.first && owner_class.state_machines[name]) || (!args.first &&
|
24
|
+
owner_class.state_machines.values.first))) || nil
|
25
25
|
|
26
26
|
if machine
|
27
27
|
# Only create a new copy if changes are being made to the machine in
|
@@ -33,10 +33,10 @@ module StateMachines
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# Evaluate DSL
|
36
|
-
machine.instance_eval(&
|
36
|
+
machine.instance_eval(&) if block_given?
|
37
37
|
else
|
38
38
|
# No existing machine: create a new one
|
39
|
-
machine = new(owner_class, name, options, &
|
39
|
+
machine = new(owner_class, name, options, &)
|
40
40
|
end
|
41
41
|
|
42
42
|
machine
|
@@ -47,6 +47,7 @@ module StateMachines
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# Default messages to use for validation errors in ORM integrations
|
50
|
+
# Thread-safe access via atomic operations on simple values
|
50
51
|
attr_accessor :ignore_method_conflicts
|
51
52
|
|
52
53
|
def default_messages
|
@@ -54,17 +55,19 @@ module StateMachines
|
|
54
55
|
invalid: 'is invalid',
|
55
56
|
invalid_event: 'cannot transition when %s',
|
56
57
|
invalid_transition: 'cannot transition via "%1$s"'
|
57
|
-
}
|
58
|
+
}.freeze
|
58
59
|
end
|
59
60
|
|
60
61
|
def default_messages=(messages)
|
61
|
-
|
62
|
+
# Atomic replacement with frozen object
|
63
|
+
@default_messages = deep_freeze_hash(messages)
|
62
64
|
end
|
63
65
|
|
64
66
|
def replace_messages(message_hash)
|
65
|
-
|
66
|
-
|
67
|
-
|
67
|
+
# Atomic replacement: read current messages, merge with new ones, replace atomically
|
68
|
+
current_messages = @default_messages || {}
|
69
|
+
merged_messages = current_messages.merge(message_hash)
|
70
|
+
@default_messages = deep_freeze_hash(merged_messages)
|
68
71
|
end
|
69
72
|
|
70
73
|
attr_writer :renderer
|
@@ -74,6 +77,17 @@ module StateMachines
|
|
74
77
|
|
75
78
|
STDIORenderer
|
76
79
|
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Deep freezes a hash and all its string values for thread safety
|
84
|
+
def deep_freeze_hash(hash)
|
85
|
+
hash.each_with_object({}) do |(key, value), frozen_hash|
|
86
|
+
frozen_key = key.respond_to?(:freeze) ? key.freeze : key
|
87
|
+
frozen_value = value.respond_to?(:freeze) ? value.freeze : value
|
88
|
+
frozen_hash[frozen_key] = frozen_value
|
89
|
+
end.freeze
|
90
|
+
end
|
77
91
|
end
|
78
92
|
end
|
79
93
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
class Machine
|
5
|
+
module Configuration
|
6
|
+
# Initializes a new state machine with the given configuration.
|
7
|
+
def initialize(owner_class, *args, &)
|
8
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
9
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
10
|
+
|
11
|
+
# Find an integration that matches this machine's owner class
|
12
|
+
@integration = if options.include?(:integration)
|
13
|
+
options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
|
14
|
+
else
|
15
|
+
StateMachines::Integrations.match(owner_class)
|
16
|
+
end
|
17
|
+
|
18
|
+
if @integration
|
19
|
+
extend @integration
|
20
|
+
options = (@integration.defaults || {}).merge(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add machine-wide defaults
|
24
|
+
options = { use_transactions: true, initialize: true }.merge(options)
|
25
|
+
|
26
|
+
# Set machine configuration
|
27
|
+
@name = args.first || :state
|
28
|
+
@attribute = options[:attribute] || @name
|
29
|
+
@events = EventCollection.new(self)
|
30
|
+
@states = StateCollection.new(self)
|
31
|
+
@callbacks = { before: [], after: [], failure: [] }
|
32
|
+
@namespace = options[:namespace]
|
33
|
+
@messages = options[:messages] || {}
|
34
|
+
@action = options[:action]
|
35
|
+
@use_transactions = options[:use_transactions]
|
36
|
+
@initialize_state = options[:initialize]
|
37
|
+
@action_hook_defined = false
|
38
|
+
self.owner_class = owner_class
|
39
|
+
|
40
|
+
# Merge with sibling machine configurations
|
41
|
+
add_sibling_machine_configs
|
42
|
+
|
43
|
+
# Define class integration
|
44
|
+
define_helpers
|
45
|
+
define_scopes(options[:plural])
|
46
|
+
after_initialize
|
47
|
+
|
48
|
+
# Evaluate DSL
|
49
|
+
instance_eval(&) if block_given?
|
50
|
+
self.initial_state = options[:initial] unless sibling_machines.any?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates a copy of this machine in addition to copies of each associated
|
54
|
+
# event/states/callback, so that the modifications to those collections do
|
55
|
+
# not affect the original machine.
|
56
|
+
def initialize_copy(orig) # :nodoc:
|
57
|
+
super
|
58
|
+
|
59
|
+
@events = @events.dup
|
60
|
+
@events.machine = self
|
61
|
+
@states = @states.dup
|
62
|
+
@states.machine = self
|
63
|
+
@callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sets the class which is the owner of this state machine. Any methods
|
67
|
+
# generated by states, events, or other parts of the machine will be defined
|
68
|
+
# on the given owner class.
|
69
|
+
def owner_class=(klass)
|
70
|
+
@owner_class = klass
|
71
|
+
|
72
|
+
# Create modules for extending the class with state/event-specific methods
|
73
|
+
@helper_modules = helper_modules = { instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class) }
|
74
|
+
owner_class.class_eval do
|
75
|
+
extend helper_modules[:class]
|
76
|
+
include helper_modules[:instance]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add class-/instance-level methods to the owner class for state initialization
|
80
|
+
unless owner_class < StateMachines::InstanceMethods
|
81
|
+
owner_class.class_eval do
|
82
|
+
extend StateMachines::ClassMethods
|
83
|
+
include StateMachines::InstanceMethods
|
84
|
+
end
|
85
|
+
|
86
|
+
define_state_initializer if @initialize_state
|
87
|
+
end
|
88
|
+
|
89
|
+
# Record this machine as matched to the name in the current owner class.
|
90
|
+
# This will override any machines mapped to the same name in any superclasses.
|
91
|
+
owner_class.state_machines[name] = self
|
92
|
+
end
|
93
|
+
|
94
|
+
# Sets the initial state of the machine. This can be either the static name
|
95
|
+
# of a state or a lambda block which determines the initial state at
|
96
|
+
# creation time.
|
97
|
+
def initial_state=(new_initial_state)
|
98
|
+
@initial_state = new_initial_state
|
99
|
+
add_states([@initial_state]) unless dynamic_initial_state?
|
100
|
+
|
101
|
+
# Update all states to reflect the new initial state
|
102
|
+
states.each { |state| state.initial = (state.name == @initial_state) }
|
103
|
+
|
104
|
+
# Output a warning if there are conflicting initial states for the machine's
|
105
|
+
# attribute
|
106
|
+
initial_state = states.detect(&:initial)
|
107
|
+
has_owner_default = !owner_class_attribute_default.nil?
|
108
|
+
has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
|
109
|
+
return unless has_owner_default && has_conflicting_default
|
110
|
+
|
111
|
+
warn(
|
112
|
+
"Both #{owner_class.name} and its #{name.inspect} machine have defined " \
|
113
|
+
"a different default for \"#{attribute}\". Use only one or the other for " \
|
114
|
+
'defining defaults to avoid unexpected behaviors.'
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Gets the attribute name for the given machine scope.
|
119
|
+
def attribute(name = :state)
|
120
|
+
name == :state ? @attribute : :"#{self.name}_#{name}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
class Machine
|
5
|
+
module EventMethods
|
6
|
+
# Defines one or more events for the machine and the transitions that can
|
7
|
+
# be performed when those events are run.
|
8
|
+
def event(*names, &)
|
9
|
+
options = names.last.is_a?(Hash) ? names.pop : {}
|
10
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
11
|
+
|
12
|
+
# Store the context so that it can be used for / matched against any event
|
13
|
+
# that gets added
|
14
|
+
@events.context(names, &) if block_given?
|
15
|
+
|
16
|
+
if names.first.is_a?(Matcher)
|
17
|
+
# Add any events referenced in the matcher. When matchers are used,
|
18
|
+
# events are not allowed to be configured.
|
19
|
+
raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
|
20
|
+
|
21
|
+
events = add_events(names.first.values)
|
22
|
+
else
|
23
|
+
events = add_events(names)
|
24
|
+
|
25
|
+
# Update the configuration for the event(s)
|
26
|
+
events.each do |event|
|
27
|
+
event.human_name = options[:human_name] if options.include?(:human_name)
|
28
|
+
|
29
|
+
# Add any states that may have been referenced within the event
|
30
|
+
add_states(event.known_states)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
events.length == 1 ? events.first : events
|
35
|
+
end
|
36
|
+
|
37
|
+
alias on event
|
38
|
+
|
39
|
+
# Creates a new transition that determines what to change the current state
|
40
|
+
# to when an event fires.
|
41
|
+
def transition(options)
|
42
|
+
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
43
|
+
|
44
|
+
branches = []
|
45
|
+
options = options.dup
|
46
|
+
event(*Array(options.delete(:on))) { branches << transition(options) }
|
47
|
+
|
48
|
+
branches.length == 1 ? branches.first : branches
|
49
|
+
end
|
50
|
+
|
51
|
+
# Gets the list of all possible transition paths from the current state to
|
52
|
+
# the given target state. If multiple target states are provided, then
|
53
|
+
# this will return all possible paths to those states.
|
54
|
+
def paths_for(object, requirements = {})
|
55
|
+
PathCollection.new(object, self, requirements)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|