nxt_state_machine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|