finite_machine 0.11.2 → 0.14.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 +5 -5
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +1 -1
- data/README.md +679 -624
- data/lib/finite_machine.rb +35 -45
- data/lib/finite_machine/async_call.rb +5 -21
- data/lib/finite_machine/callable.rb +4 -4
- data/lib/finite_machine/catchable.rb +24 -14
- data/lib/finite_machine/choice_merger.rb +20 -20
- data/lib/finite_machine/const.rb +16 -0
- data/lib/finite_machine/definition.rb +3 -3
- data/lib/finite_machine/dsl.rb +98 -151
- data/lib/finite_machine/env.rb +4 -2
- data/lib/finite_machine/event_definition.rb +7 -15
- data/lib/finite_machine/{events_chain.rb → events_map.rb} +40 -53
- data/lib/finite_machine/hook_event.rb +60 -61
- data/lib/finite_machine/hooks.rb +44 -36
- data/lib/finite_machine/listener.rb +2 -2
- data/lib/finite_machine/logger.rb +5 -4
- data/lib/finite_machine/{event_queue.rb → message_queue.rb} +76 -32
- data/lib/finite_machine/observer.rb +71 -34
- data/lib/finite_machine/safety.rb +16 -14
- data/lib/finite_machine/state_definition.rb +3 -5
- data/lib/finite_machine/state_machine.rb +93 -76
- data/lib/finite_machine/state_parser.rb +55 -83
- data/lib/finite_machine/subscribers.rb +2 -2
- data/lib/finite_machine/threadable.rb +3 -1
- data/lib/finite_machine/transition.rb +34 -34
- data/lib/finite_machine/transition_builder.rb +23 -32
- data/lib/finite_machine/transition_event.rb +12 -11
- data/lib/finite_machine/two_phase_lock.rb +8 -6
- data/lib/finite_machine/undefined_transition.rb +5 -6
- data/lib/finite_machine/version.rb +2 -2
- metadata +58 -142
- data/.gitignore +0 -18
- data/.rspec +0 -5
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -26
- data/Gemfile +0 -15
- data/Rakefile +0 -8
- data/assets/finite_machine_logo.png +0 -0
- data/examples/atm.rb +0 -45
- data/examples/bug_system.rb +0 -145
- data/finite_machine.gemspec +0 -23
- data/lib/finite_machine/async_proxy.rb +0 -30
- data/spec/integration/system_spec.rb +0 -95
- data/spec/spec_helper.rb +0 -33
- data/spec/unit/alias_target_spec.rb +0 -108
- data/spec/unit/async_events_spec.rb +0 -138
- data/spec/unit/callable/call_spec.rb +0 -113
- data/spec/unit/callbacks_spec.rb +0 -942
- data/spec/unit/can_spec.rb +0 -98
- data/spec/unit/choice_spec.rb +0 -331
- data/spec/unit/define_spec.rb +0 -55
- data/spec/unit/definition_spec.rb +0 -115
- data/spec/unit/event_names_spec.rb +0 -19
- data/spec/unit/event_queue_spec.rb +0 -52
- data/spec/unit/events_chain/add_spec.rb +0 -25
- data/spec/unit/events_chain/cancel_transitions_spec.rb +0 -22
- data/spec/unit/events_chain/choice_transition_spec.rb +0 -28
- data/spec/unit/events_chain/clear_spec.rb +0 -15
- data/spec/unit/events_chain/events_spec.rb +0 -18
- data/spec/unit/events_chain/inspect_spec.rb +0 -24
- data/spec/unit/events_chain/match_transition_spec.rb +0 -37
- data/spec/unit/events_chain/move_to_spec.rb +0 -48
- data/spec/unit/events_chain/states_for_spec.rb +0 -17
- data/spec/unit/events_spec.rb +0 -459
- data/spec/unit/handlers_spec.rb +0 -152
- data/spec/unit/hook_event/build_spec.rb +0 -15
- data/spec/unit/hook_event/eql_spec.rb +0 -36
- data/spec/unit/hook_event/infer_default_name_spec.rb +0 -13
- data/spec/unit/hook_event/initialize_spec.rb +0 -25
- data/spec/unit/hook_event/notify_spec.rb +0 -14
- data/spec/unit/hooks/call_spec.rb +0 -24
- data/spec/unit/hooks/clear_spec.rb +0 -16
- data/spec/unit/hooks/inspect_spec.rb +0 -17
- data/spec/unit/hooks/register_spec.rb +0 -22
- data/spec/unit/if_unless_spec.rb +0 -353
- data/spec/unit/initial_spec.rb +0 -222
- data/spec/unit/inspect_spec.rb +0 -17
- data/spec/unit/is_spec.rb +0 -55
- data/spec/unit/log_transitions_spec.rb +0 -30
- data/spec/unit/logger_spec.rb +0 -38
- data/spec/unit/respond_to_spec.rb +0 -38
- data/spec/unit/state_parser/inspect_spec.rb +0 -25
- data/spec/unit/state_parser/parse_spec.rb +0 -59
- data/spec/unit/states_spec.rb +0 -34
- data/spec/unit/subscribers_spec.rb +0 -42
- data/spec/unit/target_spec.rb +0 -225
- data/spec/unit/terminated_spec.rb +0 -95
- data/spec/unit/transition/check_conditions_spec.rb +0 -54
- data/spec/unit/transition/inspect_spec.rb +0 -25
- data/spec/unit/transition/matches_spec.rb +0 -23
- data/spec/unit/transition/states_spec.rb +0 -31
- data/spec/unit/transition/to_state_spec.rb +0 -27
- data/spec/unit/trigger_spec.rb +0 -22
- data/spec/unit/undefined_transition/eql_spec.rb +0 -17
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
@@ -1,7 +1,9 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "hook_event"
|
2
4
|
|
3
5
|
module FiniteMachine
|
4
|
-
# Module
|
6
|
+
# Module responsible for safety checks against known methods
|
5
7
|
module Safety
|
6
8
|
EVENT_CONFLICT_MESSAGE = \
|
7
9
|
"You tried to define an event named \"%{name}\", however this would " \
|
@@ -33,11 +35,11 @@ module FiniteMachine
|
|
33
35
|
# @api public
|
34
36
|
def detect_event_conflict!(event_name, method_name = event_name)
|
35
37
|
if method_already_implemented?(method_name)
|
36
|
-
|
38
|
+
raise FiniteMachine::AlreadyDefinedError, EVENT_CONFLICT_MESSAGE % {
|
37
39
|
name: event_name,
|
38
40
|
type: :instance,
|
39
41
|
method: method_name,
|
40
|
-
source:
|
42
|
+
source: "FiniteMachine"
|
41
43
|
}
|
42
44
|
end
|
43
45
|
end
|
@@ -55,12 +57,12 @@ module FiniteMachine
|
|
55
57
|
def ensure_valid_callback_name!(event_type, name)
|
56
58
|
message = if wrong_event_name?(name, event_type)
|
57
59
|
EVENT_CALLBACK_CONFLICT_MESSAGE % {
|
58
|
-
type: "on_#{event_type
|
60
|
+
type: "on_#{event_type}",
|
59
61
|
name: name
|
60
62
|
}
|
61
63
|
elsif wrong_state_name?(name, event_type)
|
62
64
|
STATE_CALLBACK_CONFLICT_MESSAGE % {
|
63
|
-
type: "on_#{event_type
|
65
|
+
type: "on_#{event_type}",
|
64
66
|
name: name
|
65
67
|
}
|
66
68
|
elsif !callback_names.include?(name)
|
@@ -71,7 +73,7 @@ module FiniteMachine
|
|
71
73
|
else
|
72
74
|
nil
|
73
75
|
end
|
74
|
-
message &&
|
76
|
+
message && raise_invalid_callback_error(message)
|
75
77
|
end
|
76
78
|
|
77
79
|
private
|
@@ -87,8 +89,8 @@ module FiniteMachine
|
|
87
89
|
# @api private
|
88
90
|
def wrong_event_name?(name, event_type)
|
89
91
|
machine.states.include?(name) &&
|
90
|
-
|
91
|
-
|
92
|
+
!machine.events.include?(name) &&
|
93
|
+
event_type < HookEvent::Anyaction
|
92
94
|
end
|
93
95
|
|
94
96
|
# Check if state name exists
|
@@ -101,14 +103,14 @@ module FiniteMachine
|
|
101
103
|
#
|
102
104
|
# @api private
|
103
105
|
def wrong_state_name?(name, event_type)
|
104
|
-
machine.
|
105
|
-
|
106
|
-
|
106
|
+
machine.events.include?(name) &&
|
107
|
+
!machine.states.include?(name) &&
|
108
|
+
event_type < HookEvent::Anystate
|
107
109
|
end
|
108
110
|
|
109
|
-
def
|
111
|
+
def raise_invalid_callback_error(message)
|
110
112
|
exception = InvalidCallbackNameError
|
111
|
-
machine.catch_error(exception) ||
|
113
|
+
machine.catch_error(exception) || raise(exception, message)
|
112
114
|
end
|
113
115
|
|
114
116
|
# Check if method is already implemented inside StateMachine
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
4
|
# A class responsible for defining state query methods on state machine
|
@@ -8,15 +8,13 @@ module FiniteMachine
|
|
8
8
|
#
|
9
9
|
# @api private
|
10
10
|
class StateDefinition
|
11
|
-
include Threadable
|
12
|
-
|
13
11
|
# Initialize a StateDefinition
|
14
12
|
#
|
15
13
|
# @param [StateMachine] machine
|
16
14
|
#
|
17
15
|
# @api public
|
18
16
|
def initialize(machine)
|
19
|
-
|
17
|
+
@machine = machine
|
20
18
|
end
|
21
19
|
|
22
20
|
# Define query methods for states
|
@@ -34,7 +32,7 @@ module FiniteMachine
|
|
34
32
|
private
|
35
33
|
|
36
34
|
# The current state machine
|
37
|
-
|
35
|
+
attr_reader :machine
|
38
36
|
|
39
37
|
# Define helper state mehods for the transition states
|
40
38
|
#
|
@@ -1,4 +1,15 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
require_relative "catchable"
|
6
|
+
require_relative "dsl"
|
7
|
+
require_relative "env"
|
8
|
+
require_relative "events_map"
|
9
|
+
require_relative "hook_event"
|
10
|
+
require_relative "observer"
|
11
|
+
require_relative "threadable"
|
12
|
+
require_relative "subscribers"
|
2
13
|
|
3
14
|
module FiniteMachine
|
4
15
|
# Base class for state machine
|
@@ -7,11 +18,18 @@ module FiniteMachine
|
|
7
18
|
include Catchable
|
8
19
|
extend Forwardable
|
9
20
|
|
21
|
+
# Current state
|
22
|
+
#
|
23
|
+
# @return [Symbol]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
attr_threadsafe :state
|
27
|
+
|
10
28
|
# Initial state, defaults to :none
|
11
29
|
attr_threadsafe :initial_state
|
12
30
|
|
13
|
-
# Final state, defaults to
|
14
|
-
attr_threadsafe :
|
31
|
+
# Final state, defaults to nil
|
32
|
+
attr_threadsafe :terminal_states
|
15
33
|
|
16
34
|
# The prefix used to name events.
|
17
35
|
attr_threadsafe :namespace
|
@@ -20,21 +38,14 @@ module FiniteMachine
|
|
20
38
|
attr_threadsafe :env
|
21
39
|
|
22
40
|
# The state machine event definitions
|
23
|
-
attr_threadsafe :
|
24
|
-
|
25
|
-
# Errors DSL
|
26
|
-
#
|
27
|
-
# @return [ErrorsDSL]
|
28
|
-
#
|
29
|
-
# @api private
|
30
|
-
attr_threadsafe :errors_dsl
|
41
|
+
attr_threadsafe :events_map
|
31
42
|
|
32
|
-
#
|
43
|
+
# Machine dsl
|
33
44
|
#
|
34
|
-
# @return [
|
45
|
+
# @return [DSL]
|
35
46
|
#
|
36
47
|
# @api private
|
37
|
-
attr_threadsafe :
|
48
|
+
attr_threadsafe :dsl
|
38
49
|
|
39
50
|
# The state machine observer
|
40
51
|
#
|
@@ -43,13 +54,6 @@ module FiniteMachine
|
|
43
54
|
# @api private
|
44
55
|
attr_threadsafe :observer
|
45
56
|
|
46
|
-
# Current state
|
47
|
-
#
|
48
|
-
# @return [Symbol]
|
49
|
-
#
|
50
|
-
# @api private
|
51
|
-
attr_threadsafe :state
|
52
|
-
|
53
57
|
# The state machine subscribers
|
54
58
|
#
|
55
59
|
# @return [Subscribers]
|
@@ -57,73 +61,81 @@ module FiniteMachine
|
|
57
61
|
# @api private
|
58
62
|
attr_threadsafe :subscribers
|
59
63
|
|
60
|
-
# The queue for asynchronoous events
|
61
|
-
#
|
62
|
-
# @return [EventQueue]
|
63
|
-
#
|
64
|
-
# @api private
|
65
|
-
attr_threadsafe :event_queue
|
66
|
-
|
67
64
|
# Allow or not logging of transitions
|
68
65
|
attr_threadsafe :log_transitions
|
69
66
|
|
70
|
-
def_delegators
|
67
|
+
def_delegators :dsl, :initial, :terminal, :event, :trigger_init,
|
71
68
|
:alias_target
|
72
69
|
|
73
|
-
def_delegator :events_dsl, :event
|
74
|
-
|
75
70
|
# Initialize state machine
|
76
71
|
#
|
72
|
+
# @example
|
73
|
+
# fsm = FiniteMachine::StateMachine.new(target_alias: :car) do
|
74
|
+
# initial :red
|
75
|
+
#
|
76
|
+
# event :go, :red => :green
|
77
|
+
#
|
78
|
+
# on_transition do |event|
|
79
|
+
# car.state = event.to
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# @param [Hash] options
|
84
|
+
# the options to create state machine with
|
85
|
+
# @option options [String] :alias_target
|
86
|
+
# the alias for target object
|
87
|
+
#
|
77
88
|
# @api private
|
78
89
|
def initialize(*args, &block)
|
79
|
-
|
80
|
-
|
81
|
-
@event_queue = EventQueue.new
|
90
|
+
options = args.last.is_a?(::Hash) ? args.pop : {}
|
82
91
|
@initial_state = DEFAULT_STATE
|
83
|
-
@
|
92
|
+
@auto_methods = options.fetch(:auto_methods, true)
|
84
93
|
@subscribers = Subscribers.new
|
85
94
|
@observer = Observer.new(self)
|
86
|
-
@
|
95
|
+
@events_map = EventsMap.new
|
87
96
|
@env = Env.new(self, [])
|
88
|
-
@
|
89
|
-
@errors_dsl = ErrorsDSL.new(self)
|
90
|
-
@dsl = DSL.new(self, attributes)
|
97
|
+
@dsl = DSL.new(self, options)
|
91
98
|
|
92
|
-
|
99
|
+
env.target = args.pop unless args.empty?
|
100
|
+
env.aliases << options[:alias_target] if options[:alias_target]
|
101
|
+
dsl.call(&block) if block_given?
|
93
102
|
trigger_init
|
94
103
|
end
|
95
104
|
|
96
|
-
#
|
105
|
+
# Check if event methods should be auto generated
|
97
106
|
#
|
98
|
-
# @
|
99
|
-
# machine.subscribe(Observer.new(machine))
|
107
|
+
# @return [Boolean]
|
100
108
|
#
|
101
109
|
# @api public
|
102
|
-
def
|
103
|
-
|
110
|
+
def auto_methods?
|
111
|
+
@auto_methods
|
104
112
|
end
|
105
113
|
|
106
|
-
#
|
114
|
+
# Attach state machine to an object
|
115
|
+
#
|
116
|
+
# This allows state machine to initiate events in the context
|
117
|
+
# of a particular object
|
107
118
|
#
|
108
119
|
# @example
|
109
|
-
#
|
120
|
+
# FiniteMachine.define(target: object) do
|
121
|
+
# ...
|
122
|
+
# end
|
110
123
|
#
|
111
|
-
# @return [
|
124
|
+
# @return [Object|FiniteMachine::StateMachine]
|
112
125
|
#
|
113
126
|
# @api public
|
114
|
-
|
127
|
+
def target
|
128
|
+
env.target
|
129
|
+
end
|
115
130
|
|
116
|
-
#
|
131
|
+
# Subscribe observer for event notifications
|
117
132
|
#
|
118
|
-
# @
|
133
|
+
# @example
|
134
|
+
# machine.subscribe(Observer.new(machine))
|
119
135
|
#
|
120
136
|
# @api public
|
121
|
-
def
|
122
|
-
|
123
|
-
@async_proxy.method_missing method_name, *args, &block
|
124
|
-
else
|
125
|
-
@async_proxy
|
126
|
-
end
|
137
|
+
def subscribe(*observers)
|
138
|
+
sync_exclusive { subscribers.subscribe(*observers) }
|
127
139
|
end
|
128
140
|
|
129
141
|
# Get current state
|
@@ -132,7 +144,7 @@ module FiniteMachine
|
|
132
144
|
#
|
133
145
|
# @api public
|
134
146
|
def current
|
135
|
-
state
|
147
|
+
sync_shared { state }
|
136
148
|
end
|
137
149
|
|
138
150
|
# Check if current state matches provided state
|
@@ -162,19 +174,19 @@ module FiniteMachine
|
|
162
174
|
#
|
163
175
|
# @api public
|
164
176
|
def states
|
165
|
-
sync_shared {
|
177
|
+
sync_shared { events_map.states }
|
166
178
|
end
|
167
179
|
|
168
180
|
# Retireve all event names
|
169
181
|
#
|
170
182
|
# @example
|
171
|
-
# fsm.
|
183
|
+
# fsm.events # => [:init, :start, :stop]
|
172
184
|
#
|
173
185
|
# @return [Array[Symbol]]
|
174
186
|
#
|
175
187
|
# @api public
|
176
|
-
def
|
177
|
-
|
188
|
+
def events
|
189
|
+
events_map.events
|
178
190
|
end
|
179
191
|
|
180
192
|
# Checks if event can be triggered
|
@@ -183,7 +195,7 @@ module FiniteMachine
|
|
183
195
|
# fsm.can?(:go) # => true
|
184
196
|
#
|
185
197
|
# @example
|
186
|
-
# fsm.can?(:go,
|
198
|
+
# fsm.can?(:go, "Piotr") # checks condition with parameter "Piotr"
|
187
199
|
#
|
188
200
|
# @param [String] event
|
189
201
|
#
|
@@ -191,8 +203,8 @@ module FiniteMachine
|
|
191
203
|
#
|
192
204
|
# @api public
|
193
205
|
def can?(*args)
|
194
|
-
event_name
|
195
|
-
|
206
|
+
event_name = args.shift
|
207
|
+
events_map.can_perform?(event_name, current, *args)
|
196
208
|
end
|
197
209
|
|
198
210
|
# Checks if event cannot be triggered
|
@@ -215,7 +227,7 @@ module FiniteMachine
|
|
215
227
|
#
|
216
228
|
# @api public
|
217
229
|
def terminated?
|
218
|
-
is?(
|
230
|
+
is?(terminal_states)
|
219
231
|
end
|
220
232
|
|
221
233
|
# Restore this machine to a known state
|
@@ -238,7 +250,7 @@ module FiniteMachine
|
|
238
250
|
#
|
239
251
|
# @api private
|
240
252
|
def valid_state?(event_name)
|
241
|
-
current_states =
|
253
|
+
current_states = events_map.states_for(event_name)
|
242
254
|
current_states.any? { |state| state == current || state == ANY_STATE }
|
243
255
|
end
|
244
256
|
|
@@ -273,7 +285,7 @@ module FiniteMachine
|
|
273
285
|
else
|
274
286
|
exception = InvalidStateError
|
275
287
|
catch_error(exception) ||
|
276
|
-
|
288
|
+
raise(exception, "inappropriate current state '#{current}'")
|
277
289
|
|
278
290
|
false
|
279
291
|
end
|
@@ -290,9 +302,9 @@ module FiniteMachine
|
|
290
302
|
#
|
291
303
|
# @api public
|
292
304
|
def trigger!(event_name, *data, &block)
|
293
|
-
|
294
|
-
from = current # Save away current state
|
305
|
+
from = current # Save away current state
|
295
306
|
|
307
|
+
sync_exclusive do
|
296
308
|
notify HookEvent::Before, event_name, from, *data
|
297
309
|
|
298
310
|
status = try_trigger(event_name) do
|
@@ -313,6 +325,9 @@ module FiniteMachine
|
|
313
325
|
|
314
326
|
status
|
315
327
|
end
|
328
|
+
rescue Exception => err
|
329
|
+
self.state = from # rollback transition
|
330
|
+
raise err
|
316
331
|
end
|
317
332
|
|
318
333
|
# Trigger transition event without raising any errors
|
@@ -325,7 +340,7 @@ module FiniteMachine
|
|
325
340
|
# @api public
|
326
341
|
def trigger(event_name, *data, &block)
|
327
342
|
trigger!(event_name, *data, &block)
|
328
|
-
rescue InvalidStateError, TransitionError
|
343
|
+
rescue InvalidStateError, TransitionError, CallbackError
|
329
344
|
false
|
330
345
|
end
|
331
346
|
|
@@ -336,7 +351,7 @@ module FiniteMachine
|
|
336
351
|
# @api private
|
337
352
|
def transition!(event_name, *data, &block)
|
338
353
|
from_state = current
|
339
|
-
to_state =
|
354
|
+
to_state = events_map.move_to(event_name, from_state, *data)
|
340
355
|
|
341
356
|
block.call(from_state, to_state) if block
|
342
357
|
|
@@ -376,9 +391,11 @@ module FiniteMachine
|
|
376
391
|
# @api public
|
377
392
|
def inspect
|
378
393
|
sync_shared do
|
379
|
-
"<##{self.class}:0x#{object_id.to_s(16)}
|
380
|
-
"@
|
381
|
-
"@
|
394
|
+
"<##{self.class}:0x#{object_id.to_s(16)} " \
|
395
|
+
"@current=#{current.inspect} " \
|
396
|
+
"@states=#{states} " \
|
397
|
+
"@events=#{events} " \
|
398
|
+
"@transitions=#{events_map.state_transitions}>"
|
382
399
|
end
|
383
400
|
end
|
384
401
|
|
@@ -393,7 +410,7 @@ module FiniteMachine
|
|
393
410
|
#
|
394
411
|
# @api private
|
395
412
|
def raise_transition_error(error)
|
396
|
-
|
413
|
+
raise TransitionError, Logger.format_error(error)
|
397
414
|
end
|
398
415
|
|
399
416
|
# Forward the message to observer or self
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module FiniteMachine
|
4
4
|
# A class responsible for converting transition arguments to states
|
@@ -7,148 +7,120 @@ module FiniteMachine
|
|
7
7
|
#
|
8
8
|
# @api private
|
9
9
|
class StateParser
|
10
|
-
|
10
|
+
NON_STATE_KEYS = %i[name if unless silent].freeze
|
11
11
|
|
12
|
-
|
12
|
+
STATE_KEYS = %i[from to].freeze
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
# Initialize a StateParser
|
14
|
+
# Extract states from user defined attributes
|
17
15
|
#
|
18
16
|
# @example
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# @param [Hash] attrs
|
22
|
-
#
|
23
|
-
# @api public
|
24
|
-
def initialize(attrs)
|
25
|
-
@attrs = ensure_only_states!(attrs)
|
26
|
-
freeze
|
27
|
-
end
|
28
|
-
|
29
|
-
# Extract states from attributes
|
17
|
+
# StateParser.parse({from: [:green, :blue], to: :red})
|
18
|
+
# # => {green: :red, green: :blue}
|
30
19
|
#
|
31
20
|
# @param [Proc] block
|
32
21
|
#
|
33
|
-
# @example
|
34
|
-
# StateParpser.new(attr).parase_states
|
35
|
-
#
|
36
22
|
# @yield [Hash[Symbol]] the resolved states
|
37
23
|
#
|
38
24
|
# @return [Hash[Symbol]] the resolved states
|
39
25
|
#
|
40
26
|
# @api public
|
41
|
-
def parse(&block)
|
42
|
-
|
27
|
+
def self.parse(attributes, &block)
|
28
|
+
attrs = ensure_only_states!(attributes)
|
29
|
+
states = extract_states(attrs)
|
43
30
|
block ? states.each(&block) : states
|
44
31
|
end
|
45
32
|
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# @example
|
49
|
-
# parser = StateParser.new({from: :green, to: :red})
|
50
|
-
# parser.contains_from_to_keys? # => true
|
51
|
-
#
|
52
|
-
# @example
|
53
|
-
# parser = StateParser.new({:green => :red})
|
54
|
-
# parser.contains_from_to_keys? # => false
|
33
|
+
# Extract only states from attributes
|
55
34
|
#
|
56
|
-
# @return [
|
35
|
+
# @return [Hash[Symbol]]
|
57
36
|
#
|
58
|
-
# @api
|
59
|
-
def
|
60
|
-
|
37
|
+
# @api private
|
38
|
+
def self.ensure_only_states!(attrs)
|
39
|
+
attributes = attrs.dup
|
40
|
+
NON_STATE_KEYS.each { |key| attributes.delete(key) }
|
41
|
+
raise_not_enough_transitions unless attributes.any?
|
42
|
+
attributes
|
61
43
|
end
|
44
|
+
private_class_method :ensure_only_states!
|
62
45
|
|
63
|
-
#
|
46
|
+
# Perform extraction of states from user supplied definitions
|
64
47
|
#
|
65
|
-
# @return [
|
48
|
+
# @return [Hash[Symbol]] the resolved states
|
66
49
|
#
|
67
|
-
# @api
|
68
|
-
def
|
69
|
-
attrs
|
50
|
+
# @api private
|
51
|
+
def self.extract_states(attrs)
|
52
|
+
if contains_from_to_keys?(attrs)
|
53
|
+
convert_from_to_attributes_to_states_hash(attrs)
|
54
|
+
else
|
55
|
+
convert_attributes_to_states_hash(attrs)
|
56
|
+
end
|
70
57
|
end
|
58
|
+
private_class_method :extract_states
|
71
59
|
|
72
|
-
#
|
60
|
+
# Check if attributes contain :from or :to key
|
73
61
|
#
|
74
|
-
# @
|
62
|
+
# @example
|
63
|
+
# StateParser.contains_from_to_keys?({from: :green, to: :red})
|
64
|
+
# # => true
|
75
65
|
#
|
76
|
-
# @
|
77
|
-
|
78
|
-
|
79
|
-
"<##{self.class} @attrs=#{attributes}>"
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
# Extract only states from attributes
|
66
|
+
# @example
|
67
|
+
# StateParser.contains_from_to_keys?({:green => :red})
|
68
|
+
# # => false
|
85
69
|
#
|
86
|
-
# @return [
|
70
|
+
# @return [Boolean]
|
87
71
|
#
|
88
|
-
# @api
|
89
|
-
def
|
90
|
-
|
91
|
-
BLACKLIST.each { |key| attributes.delete(key) }
|
92
|
-
raise_not_enough_transitions unless attributes.any?
|
93
|
-
attributes
|
72
|
+
# @api public
|
73
|
+
def self.contains_from_to_keys?(attrs)
|
74
|
+
STATE_KEYS.any? { |key| attrs.keys.include?(key) }
|
94
75
|
end
|
76
|
+
private_class_method :contains_from_to_keys?
|
95
77
|
|
96
78
|
# Convert attrbiutes with :from, :to keys to states hash
|
97
79
|
#
|
98
80
|
# @return [Hash[Symbol]]
|
99
81
|
#
|
100
82
|
# @api private
|
101
|
-
def convert_from_to_attributes_to_states_hash
|
102
|
-
Array(attrs[:from] || ANY_STATE).
|
83
|
+
def self.convert_from_to_attributes_to_states_hash(attrs)
|
84
|
+
Array(attrs[:from] || ANY_STATE).each_with_object({}) do |state, hash|
|
103
85
|
hash[state] = attrs[:to] || state
|
104
|
-
hash
|
105
86
|
end
|
106
87
|
end
|
88
|
+
private_class_method :convert_from_to_attributes_to_states_hash
|
107
89
|
|
108
90
|
# Convert collapsed attributes to states hash
|
109
91
|
#
|
110
92
|
# @example
|
111
|
-
#
|
112
|
-
#
|
93
|
+
# StateParser.convert_attributes_to_states_hash([:green, :red] => :yellow)
|
94
|
+
# # => {green: :yellow, red: :yellow}
|
95
|
+
#
|
96
|
+
# @param [Hash] attrs
|
97
|
+
# the attributes to convert to a simple hash
|
113
98
|
#
|
114
99
|
# @return [Hash[Symbol]]
|
115
100
|
#
|
116
101
|
# @api private
|
117
|
-
def convert_attributes_to_states_hash
|
118
|
-
attrs.
|
102
|
+
def self.convert_attributes_to_states_hash(attrs)
|
103
|
+
attrs.each_with_object({}) do |(k, v), hash|
|
119
104
|
if k.respond_to?(:to_ary)
|
120
105
|
k.each { |el| hash[el] = v }
|
121
106
|
else
|
122
107
|
hash[k] = v
|
123
108
|
end
|
124
|
-
hash
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
# Perform extraction of states from user supplied definitions
|
129
|
-
#
|
130
|
-
# @return [Hash[Symbol]] the resolved states
|
131
|
-
#
|
132
|
-
# @api private
|
133
|
-
def extract_states
|
134
|
-
if contains_from_to_keys?
|
135
|
-
convert_from_to_attributes_to_states_hash
|
136
|
-
else
|
137
|
-
convert_attributes_to_states_hash
|
138
109
|
end
|
139
110
|
end
|
111
|
+
private_class_method :convert_attributes_to_states_hash
|
140
112
|
|
141
113
|
# Raise error when not enough transitions are provided
|
142
114
|
#
|
143
115
|
# @raise [NotEnoughTransitionsError]
|
144
|
-
# if the event has
|
116
|
+
# if the event has no transitions
|
145
117
|
#
|
146
118
|
# @return [nil]
|
147
119
|
#
|
148
120
|
# @api private
|
149
|
-
def raise_not_enough_transitions
|
150
|
-
|
151
|
-
" '#{attrs.inspect}'"
|
121
|
+
def self.raise_not_enough_transitions
|
122
|
+
raise NotEnoughTransitionsError, "please provide state transitions"
|
152
123
|
end
|
124
|
+
private_class_method :raise_not_enough_transitions
|
153
125
|
end # StateParser
|
154
126
|
end # FiniteMachine
|