nxt_state_machine 0.1.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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +348 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/nxt_state_machine/callable.rb +63 -0
- data/lib/nxt_state_machine/callback_registry.rb +35 -0
- data/lib/nxt_state_machine/error_callback_registry.rb +38 -0
- data/lib/nxt_state_machine/errors/error.rb +1 -0
- data/lib/nxt_state_machine/errors/event_already_registered.rb +5 -0
- data/lib/nxt_state_machine/errors/event_without_transitions.rb +5 -0
- data/lib/nxt_state_machine/errors/initial_state_already_defined.rb +7 -0
- data/lib/nxt_state_machine/errors/invalid_callback_option.rb +5 -0
- data/lib/nxt_state_machine/errors/missing_configuration.rb +5 -0
- data/lib/nxt_state_machine/errors/state_already_registered.rb +5 -0
- data/lib/nxt_state_machine/errors/transition_already_registered.rb +5 -0
- data/lib/nxt_state_machine/errors/transition_halted.rb +12 -0
- data/lib/nxt_state_machine/errors/transition_not_defined.rb +5 -0
- data/lib/nxt_state_machine/errors/unknown_state_error.rb +5 -0
- data/lib/nxt_state_machine/event.rb +49 -0
- data/lib/nxt_state_machine/event_registry.rb +11 -0
- data/lib/nxt_state_machine/integrations/active_record.rb +77 -0
- data/lib/nxt_state_machine/integrations/attr_accessor.rb +69 -0
- data/lib/nxt_state_machine/integrations/hash.rb +67 -0
- data/lib/nxt_state_machine/state.rb +17 -0
- data/lib/nxt_state_machine/state_machine.rb +179 -0
- data/lib/nxt_state_machine/state_registry.rb +12 -0
- data/lib/nxt_state_machine/transition/around_callback_chain.rb +26 -0
- data/lib/nxt_state_machine/transition/proxy.rb +31 -0
- data/lib/nxt_state_machine/transition/store.rb +19 -0
- data/lib/nxt_state_machine/transition.rb +87 -0
- data/lib/nxt_state_machine/version.rb +3 -0
- data/lib/nxt_state_machine.rb +96 -0
- data/nxt_state_machine.gemspec +46 -0
- metadata +202 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Event
|
3
|
+
include NxtRegistry
|
4
|
+
|
5
|
+
def initialize(name, state_machine:, &block)
|
6
|
+
@state_machine = state_machine
|
7
|
+
@name = name
|
8
|
+
@event_transitions = registry("#{name} event transitions")
|
9
|
+
|
10
|
+
configure(&block)
|
11
|
+
|
12
|
+
ensure_event_has_transitions
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :name, :state_machine, :event_transitions
|
16
|
+
|
17
|
+
delegate :before_transition,
|
18
|
+
:after_transition,
|
19
|
+
:around_transition,
|
20
|
+
:on_error,
|
21
|
+
:on_error!,
|
22
|
+
:any_state,
|
23
|
+
:all_states,
|
24
|
+
:all_states_except,
|
25
|
+
to: :state_machine
|
26
|
+
|
27
|
+
def transitions(from:, to:, &block)
|
28
|
+
Array(from).each do |from_state|
|
29
|
+
transition = Transition.new(name, from: from_state, to: to, state_machine: state_machine, &block)
|
30
|
+
state_machine.transitions << transition
|
31
|
+
event_transitions.register(from_state, transition)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :transition, :transitions
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def configure(&block)
|
40
|
+
instance_exec(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def ensure_event_has_transitions
|
44
|
+
return if event_transitions.size > 0
|
45
|
+
|
46
|
+
raise NxtStateMachine::Errors::EventWithoutTransitions, "No transitions for event :#{name} defined"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class EventRegistry < NxtRegistry::Registry
|
3
|
+
def initialize
|
4
|
+
super :events do
|
5
|
+
on_key_already_registered do |key|
|
6
|
+
raise NxtStateMachine::Errors::EventAlreadyRegistered, "An event with the name '#{key}' was already registered!"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
module ActiveRecord
|
3
|
+
module ClassMethods
|
4
|
+
def state_machine(name = :default, state_attr: :state, target: nil, &config)
|
5
|
+
machine = super(
|
6
|
+
name,
|
7
|
+
state_attr: state_attr,
|
8
|
+
target: target,
|
9
|
+
&config
|
10
|
+
)
|
11
|
+
|
12
|
+
machine.get_state_with do |target|
|
13
|
+
if target
|
14
|
+
if target.send(state_attr).nil? && target.new_record?
|
15
|
+
target.assign_attributes(state_attr => machine.initial_state.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
current_state = target.send(state_attr)
|
19
|
+
current_state&.to_sym
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
machine.set_state_with do |target, transition|
|
24
|
+
target.transaction do
|
25
|
+
transition.run_before_callbacks
|
26
|
+
result = set_state(target, transition, state_attr, :save)
|
27
|
+
transition.run_after_callbacks
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
rescue StandardError => error
|
32
|
+
target.assign_attributes(state_attr => transition.from.to_s)
|
33
|
+
|
34
|
+
if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
|
35
|
+
false
|
36
|
+
else
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
machine.set_state_with! do |target, transition|
|
42
|
+
target.transaction do
|
43
|
+
transition.run_before_callbacks
|
44
|
+
result = set_state(target, transition, state_attr, :save!)
|
45
|
+
transition.run_after_callbacks
|
46
|
+
|
47
|
+
result
|
48
|
+
end
|
49
|
+
rescue StandardError
|
50
|
+
target.assign_attributes(state_attr => transition.from.to_s)
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
|
54
|
+
machine
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
private
|
60
|
+
|
61
|
+
def set_state(target, transition, state_attr, method)
|
62
|
+
transition.execute do |block|
|
63
|
+
result = block ? block.call : nil
|
64
|
+
target.assign_attributes(state_attr => transition.to.to_s)
|
65
|
+
set_state_result = target.send(method) || halt_transition
|
66
|
+
block ? result : set_state_result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.included(base)
|
72
|
+
base.include(NxtStateMachine)
|
73
|
+
base.include(InstanceMethods)
|
74
|
+
base.extend(ClassMethods)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
module AttrAccessor
|
3
|
+
module ClassMethods
|
4
|
+
def state_machine(name = :default, state_attr: :state, target: nil, &config)
|
5
|
+
machine = super(
|
6
|
+
name,
|
7
|
+
state_attr: state_attr,
|
8
|
+
target: target,
|
9
|
+
&config
|
10
|
+
)
|
11
|
+
|
12
|
+
machine.get_state_with do |target|
|
13
|
+
if target.send(state_attr).nil?
|
14
|
+
target.send("#{state_attr}=", initial_state.enum)
|
15
|
+
end
|
16
|
+
|
17
|
+
target.send(state_attr)
|
18
|
+
end
|
19
|
+
|
20
|
+
machine.set_state_with do |target, transition|
|
21
|
+
transition.run_before_callbacks
|
22
|
+
result = set_state(target, transition, state_attr)
|
23
|
+
transition.run_after_callbacks
|
24
|
+
|
25
|
+
result
|
26
|
+
rescue StandardError => error
|
27
|
+
target.send("#{state_attr}=", transition.from.enum)
|
28
|
+
|
29
|
+
if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
|
30
|
+
false
|
31
|
+
else
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
machine.set_state_with! do |target, transition|
|
37
|
+
transition.run_before_callbacks
|
38
|
+
result = set_state(target, transition, state_attr)
|
39
|
+
transition.run_after_callbacks
|
40
|
+
|
41
|
+
result
|
42
|
+
rescue StandardError
|
43
|
+
target.send("#{state_attr}=", transition.from.enum)
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
|
47
|
+
machine
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module InstanceMethods
|
52
|
+
private
|
53
|
+
|
54
|
+
def set_state(target, transition, state_attr)
|
55
|
+
transition.execute do |block|
|
56
|
+
result = block ? block.call : nil
|
57
|
+
set_state_result = target.send("#{state_attr}=", transition.to.enum)
|
58
|
+
block ? result : set_state_result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.included(base)
|
64
|
+
base.include(NxtStateMachine)
|
65
|
+
base.include(InstanceMethods)
|
66
|
+
base.extend(ClassMethods)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
module Hash
|
3
|
+
module ClassMethods
|
4
|
+
def state_machine(name = :default, state_attr: :state, target: nil, &config)
|
5
|
+
machine = super(
|
6
|
+
name,
|
7
|
+
state_attr: state_attr,
|
8
|
+
target: target,
|
9
|
+
&config
|
10
|
+
)
|
11
|
+
|
12
|
+
machine.get_state_with do |target|
|
13
|
+
if target[state_attr].nil?
|
14
|
+
target[state_attr] = initial_state.enum
|
15
|
+
end
|
16
|
+
|
17
|
+
target[state_attr]
|
18
|
+
end
|
19
|
+
|
20
|
+
machine.set_state_with do |target, transition|
|
21
|
+
transition.run_before_callbacks
|
22
|
+
result = set_state(target, transition, state_attr)
|
23
|
+
transition.run_after_callbacks
|
24
|
+
result
|
25
|
+
rescue StandardError => error
|
26
|
+
target[state_attr] = transition.from.enum
|
27
|
+
|
28
|
+
if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
|
29
|
+
false
|
30
|
+
else
|
31
|
+
raise
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
machine.set_state_with! do |target, transition|
|
36
|
+
transition.run_before_callbacks
|
37
|
+
result = set_state(target, transition, state_attr)
|
38
|
+
transition.run_after_callbacks
|
39
|
+
|
40
|
+
result
|
41
|
+
rescue StandardError
|
42
|
+
target[state_attr] = transition.from.enum
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
|
46
|
+
machine
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module InstanceMethods
|
51
|
+
private
|
52
|
+
|
53
|
+
def set_state(target, transition, state_attr)
|
54
|
+
transition.execute do |block|
|
55
|
+
result = block ? block.call : nil
|
56
|
+
set_state_result = target[state_attr] = transition.to.enum || halt_transition
|
57
|
+
block ? result : set_state_result
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.included(base)
|
63
|
+
base.include(NxtStateMachine)
|
64
|
+
base.extend(ClassMethods)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class State
|
3
|
+
def initialize(enum, machine, **opts)
|
4
|
+
@enum = enum
|
5
|
+
@machine = machine
|
6
|
+
@initial = opts.delete(:initial)
|
7
|
+
@transitions = []
|
8
|
+
@options = opts.with_indifferent_access
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :enum, :initial, :transitions, :machine, :options
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
enum.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class StateMachine
|
3
|
+
def initialize(name, class_context, event_registry, **opts)
|
4
|
+
@name = name
|
5
|
+
@class_context = class_context
|
6
|
+
@options = opts
|
7
|
+
|
8
|
+
@states = NxtStateMachine::StateRegistry.new
|
9
|
+
@transitions = Transition::Store.new
|
10
|
+
@events = event_registry
|
11
|
+
@callbacks = CallbackRegistry.new
|
12
|
+
@error_callback_registry = ErrorCallbackRegistry.new
|
13
|
+
|
14
|
+
@initial_state = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :class_context, :states, :transitions, :events, :options, :callbacks, :name, :error_callback_registry
|
18
|
+
attr_accessor :initial_state
|
19
|
+
|
20
|
+
def get_state_with(method = nil, &block)
|
21
|
+
method_or_block = (method || block)
|
22
|
+
@get_state_with ||= method_or_block && Callable.new(method_or_block) ||
|
23
|
+
raise_missing_configuration_error(:get_state_with)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_state_with(method = nil, &block)
|
27
|
+
method_or_block = (method || block)
|
28
|
+
@set_state_with ||= method_or_block && Callable.new(method_or_block) ||
|
29
|
+
raise_missing_configuration_error(:set_state_with)
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_state_with!(method = nil, &block)
|
33
|
+
method_or_block = (method || block)
|
34
|
+
@set_state_with_bang ||= method_or_block && Callable.new(method_or_block) ||
|
35
|
+
raise_missing_configuration_error(:set_state_with!)
|
36
|
+
end
|
37
|
+
|
38
|
+
def state(*names, **opts, &block)
|
39
|
+
defaults = { initial: false }
|
40
|
+
opts.reverse_merge!(defaults)
|
41
|
+
machine = self
|
42
|
+
|
43
|
+
Array(names).map do |name|
|
44
|
+
if opts.fetch(:initial) && initial_state.present?
|
45
|
+
raise NxtStateMachine::Errors::InitialStateAlreadyDefined, ":#{initial_state.enum} was already defined as the initial state"
|
46
|
+
else
|
47
|
+
state = new_state_class(&block).new(name, self, opts)
|
48
|
+
states.register(name, state)
|
49
|
+
self.initial_state = state if opts.fetch(:initial)
|
50
|
+
|
51
|
+
class_context.define_method "#{name}?" do
|
52
|
+
# States internally are always strings
|
53
|
+
machine.current_state_name(self) == name
|
54
|
+
end
|
55
|
+
|
56
|
+
state
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def transitions
|
62
|
+
@transitions ||= events.values.flat_map(&:event_transitions)
|
63
|
+
end
|
64
|
+
|
65
|
+
def all_transitions_from_to(from: all_states, to: all_states)
|
66
|
+
transitions.select { |transition| transition.transitions_from_to?(from, to) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def any_state
|
70
|
+
states.values.map(&:enum)
|
71
|
+
end
|
72
|
+
|
73
|
+
alias_method :all_states, :any_state
|
74
|
+
|
75
|
+
def all_states_except(*excluded)
|
76
|
+
all_states - excluded
|
77
|
+
end
|
78
|
+
|
79
|
+
def event(name, &block)
|
80
|
+
event = Event.new(name, state_machine: self, &block)
|
81
|
+
events.register(name, event)
|
82
|
+
|
83
|
+
class_context.define_method name do |*args, **opts|
|
84
|
+
event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
|
85
|
+
transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
|
86
|
+
transition.prepare(name, self, :set_state_with, *args, **opts)
|
87
|
+
end
|
88
|
+
|
89
|
+
class_context.define_method "#{name}!" do |*args, **opts|
|
90
|
+
event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
|
91
|
+
transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
|
92
|
+
transition.prepare("#{name}!", self, :set_state_with!, *args, **opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
class_context.define_method "can_#{name}?" do
|
96
|
+
event.state_machine.can_transition?(name, event.state_machine.current_state_name(self))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def can_transition?(event_name, from)
|
101
|
+
normalized_event_name = event_name
|
102
|
+
event = events.resolve(normalized_event_name)
|
103
|
+
event && event.event_transitions.key?(from)
|
104
|
+
end
|
105
|
+
|
106
|
+
def can_transition!(event, from)
|
107
|
+
return true if can_transition?(event, from)
|
108
|
+
raise NxtStateMachine::Errors::TransitionNotDefined, "No transition :#{event} for state :#{from} defined"
|
109
|
+
end
|
110
|
+
|
111
|
+
def before_transition(from:, to:, run: nil, &block)
|
112
|
+
callbacks.register(from, to, :before, run, block)
|
113
|
+
end
|
114
|
+
|
115
|
+
def after_transition(from:, to:, run: nil, &block)
|
116
|
+
callbacks.register(from, to, :after, run, block)
|
117
|
+
end
|
118
|
+
|
119
|
+
def on_error(error = StandardError, from:, to:, run: nil, &block)
|
120
|
+
error_callback_registry.register(from, to, error, run, block)
|
121
|
+
end
|
122
|
+
|
123
|
+
def on_error!(error = StandardError, from:, to:, run: nil, &block)
|
124
|
+
error_callback_registry.register!(from, to, error, run, block)
|
125
|
+
end
|
126
|
+
|
127
|
+
def around_transition(from:, to:, run: nil, &block)
|
128
|
+
callbacks.register(from, to, :around, run, block)
|
129
|
+
end
|
130
|
+
|
131
|
+
def configure(&block)
|
132
|
+
instance_exec(&block)
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
def run_before_callbacks(transition, context)
|
137
|
+
run_callbacks(transition, :before, context)
|
138
|
+
end
|
139
|
+
|
140
|
+
def run_after_callbacks(transition, context)
|
141
|
+
run_callbacks(transition, :after, context)
|
142
|
+
end
|
143
|
+
|
144
|
+
def find_error_callback(error, transition)
|
145
|
+
error_callback_registry.resolve(error, transition)
|
146
|
+
end
|
147
|
+
|
148
|
+
def run_callbacks(transition, kind, context)
|
149
|
+
current_callbacks = callbacks.resolve(transition, kind)
|
150
|
+
|
151
|
+
current_callbacks.each do |callback|
|
152
|
+
Callable.new(callback).with_context(context).call(transition)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def current_state_name(context)
|
157
|
+
get_state_with.with_context(context).call(target(context))
|
158
|
+
end
|
159
|
+
|
160
|
+
def target(context)
|
161
|
+
@target_method ||= options[:target] || :itself
|
162
|
+
context.send(@target_method)
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def raise_missing_configuration_error(method)
|
168
|
+
raise NxtStateMachine::Errors::MissingConfiguration, "Configuration method :#{method} was not defined"
|
169
|
+
end
|
170
|
+
|
171
|
+
def new_state_class(&block)
|
172
|
+
if block
|
173
|
+
Class.new(NxtStateMachine::State, &block)
|
174
|
+
else
|
175
|
+
NxtStateMachine::State
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class StateRegistry < NxtRegistry::Registry
|
3
|
+
def initialize
|
4
|
+
super :states do
|
5
|
+
on_key_already_registered do |key|
|
6
|
+
raise NxtStateMachine::Errors::StateAlreadyRegistered,
|
7
|
+
"A state with the name '#{key}' was already registered!"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Transition::AroundCallbackChain
|
3
|
+
|
4
|
+
def initialize(transition, context, state_machine)
|
5
|
+
@transition = transition
|
6
|
+
@context = context
|
7
|
+
@state_machine = state_machine
|
8
|
+
end
|
9
|
+
|
10
|
+
def build(proxy)
|
11
|
+
return proxy unless callbacks.any?
|
12
|
+
|
13
|
+
callbacks.map { |c| Callable.new(c).with_context(context) }.reverse.inject(proxy) do |previous, callback|
|
14
|
+
-> { callback.call(previous, transition) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def callbacks
|
21
|
+
@callbacks ||= state_machine.callbacks.resolve(transition).kind(:around)
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :transition, :context, :state_machine
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Transition::Proxy
|
3
|
+
def initialize(event, state_machine, transition, context)
|
4
|
+
@event = event
|
5
|
+
@transition = transition
|
6
|
+
@state_machine = state_machine
|
7
|
+
@context = context
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(&block)
|
11
|
+
proxy = if block.arity == 1
|
12
|
+
Proc.new do
|
13
|
+
block.call(transition.block_proxy)
|
14
|
+
end
|
15
|
+
else
|
16
|
+
block
|
17
|
+
end
|
18
|
+
|
19
|
+
around_callback_chain(proxy).call
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def around_callback_chain(proxy)
|
25
|
+
@around_callback_chain ||= Transition::AroundCallbackChain.new(transition, context, state_machine).build(proxy)
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :proxy, :transition, :state_machine, :context, :event
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Transition::Store < Array
|
3
|
+
def <<(transition)
|
4
|
+
ensure_transition_unique(transition)
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
alias_method :add, :<<
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def ensure_transition_unique(transition)
|
13
|
+
return unless find { |other| other.from.enum == transition.from.enum && other.to.enum == transition.to.enum }
|
14
|
+
|
15
|
+
raise NxtStateMachine::Errors::TransitionAlreadyRegistered,
|
16
|
+
"A transition from :#{transition.from.enum} to :#{transition.to.enum} was already registered"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Transition
|
3
|
+
def initialize(name, from:, to:, state_machine:, &block)
|
4
|
+
@name = name
|
5
|
+
@from = state_machine.states.resolve(from)
|
6
|
+
@to = state_machine.states.resolve(to)
|
7
|
+
@state_machine = state_machine
|
8
|
+
@block = block
|
9
|
+
|
10
|
+
# TODO: Write a spec that verifies that transitions are unique
|
11
|
+
ensure_states_exist
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name, :from, :to
|
15
|
+
|
16
|
+
# TODO: Probably would make sense if we could also define the event name to be passed in
|
17
|
+
# => This way we could differentiate what event triggered the callback!!!
|
18
|
+
|
19
|
+
def prepare(event, context, set_state_with_method, *args, **opts)
|
20
|
+
# This exposes the transition block on the transition_to_execute so it can be executed later in :set_state_with
|
21
|
+
current_transition = clone
|
22
|
+
current_transition.send(:context=, context)
|
23
|
+
current_transition.send(:event=, event)
|
24
|
+
current_transition.send(:block_proxy=, nil)
|
25
|
+
|
26
|
+
# block_proxy only is set when the transition accepts a block!
|
27
|
+
if block
|
28
|
+
proxy = Proc.new do
|
29
|
+
# if the block takes arguments we always pass the transition as the first one
|
30
|
+
args.prepend(current_transition) if block.arity > 0
|
31
|
+
context.instance_exec(*args, **opts, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
current_transition.send(:block_proxy=, proxy)
|
35
|
+
end
|
36
|
+
|
37
|
+
state_machine.send(
|
38
|
+
set_state_with_method
|
39
|
+
).with_context(
|
40
|
+
context
|
41
|
+
).call(state_machine.target(context), current_transition)
|
42
|
+
end
|
43
|
+
|
44
|
+
def execute(&block)
|
45
|
+
# This is called on the cloned transition from above!
|
46
|
+
Transition::Proxy.new(event, state_machine,self, context).call(&block)
|
47
|
+
rescue StandardError => error
|
48
|
+
callback = state_machine.find_error_callback(error, self)
|
49
|
+
raise unless callback
|
50
|
+
|
51
|
+
Callable.new(callback).with_context(context).call(error, self)
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_method :with_around_callbacks, :execute
|
55
|
+
|
56
|
+
def run_before_callbacks
|
57
|
+
state_machine.run_before_callbacks(self, context)
|
58
|
+
end
|
59
|
+
|
60
|
+
def run_after_callbacks
|
61
|
+
state_machine.run_after_callbacks(self, context)
|
62
|
+
end
|
63
|
+
|
64
|
+
def transitions_from_to?(from_state, to_state)
|
65
|
+
from.enum.in?(Array(from_state)) && to.enum.in?(Array(to_state))
|
66
|
+
end
|
67
|
+
|
68
|
+
def id
|
69
|
+
@id ||= "#{from.to_s}_#{to.to_s}"
|
70
|
+
end
|
71
|
+
|
72
|
+
attr_reader :block_proxy, :event
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
delegate :all_states, to: :state_machine
|
77
|
+
|
78
|
+
attr_reader :block, :state_machine
|
79
|
+
attr_accessor :context
|
80
|
+
attr_writer :block_proxy, :event
|
81
|
+
|
82
|
+
def ensure_states_exist
|
83
|
+
raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{from} registered" unless state_machine.states.key?(from.enum)
|
84
|
+
raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{to} registered" unless state_machine.states.key?(to.enum)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|