finite_machine 0.10.2 → 0.11.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Gemfile +1 -1
  4. data/README.md +73 -35
  5. data/assets/finite_machine_logo.png +0 -0
  6. data/lib/finite_machine.rb +0 -7
  7. data/lib/finite_machine/async_proxy.rb +1 -2
  8. data/lib/finite_machine/dsl.rb +13 -14
  9. data/lib/finite_machine/event_definition.rb +32 -35
  10. data/lib/finite_machine/events_chain.rb +183 -37
  11. data/lib/finite_machine/hook_event.rb +47 -42
  12. data/lib/finite_machine/logger.rb +3 -4
  13. data/lib/finite_machine/observer.rb +27 -11
  14. data/lib/finite_machine/state_definition.rb +66 -0
  15. data/lib/finite_machine/state_machine.rb +177 -99
  16. data/lib/finite_machine/subscribers.rb +17 -6
  17. data/lib/finite_machine/thread_context.rb +1 -1
  18. data/lib/finite_machine/transition.rb +45 -173
  19. data/lib/finite_machine/transition_builder.rb +24 -6
  20. data/lib/finite_machine/transition_event.rb +5 -4
  21. data/lib/finite_machine/undefined_transition.rb +32 -0
  22. data/lib/finite_machine/version.rb +1 -1
  23. data/spec/spec_helper.rb +1 -0
  24. data/spec/unit/async_events_spec.rb +24 -18
  25. data/spec/unit/callbacks_spec.rb +0 -19
  26. data/spec/unit/event_names_spec.rb +19 -0
  27. data/spec/unit/events_chain/add_spec.rb +25 -0
  28. data/spec/unit/events_chain/cancel_transitions_spec.rb +22 -0
  29. data/spec/unit/events_chain/choice_transition_spec.rb +28 -0
  30. data/spec/unit/events_chain/clear_spec.rb +7 -18
  31. data/spec/unit/events_chain/events_spec.rb +18 -0
  32. data/spec/unit/events_chain/inspect_spec.rb +14 -17
  33. data/spec/unit/events_chain/match_transition_spec.rb +37 -0
  34. data/spec/unit/events_chain/move_to_spec.rb +48 -0
  35. data/spec/unit/events_chain/states_for_spec.rb +17 -0
  36. data/spec/unit/events_spec.rb +119 -27
  37. data/spec/unit/hook_event/build_spec.rb +15 -0
  38. data/spec/unit/hook_event/eql_spec.rb +3 -4
  39. data/spec/unit/hook_event/initialize_spec.rb +14 -11
  40. data/spec/unit/hook_event/notify_spec.rb +14 -0
  41. data/spec/unit/{initialize_spec.rb → initial_spec.rb} +1 -1
  42. data/spec/unit/inspect_spec.rb +1 -1
  43. data/spec/unit/logger_spec.rb +4 -5
  44. data/spec/unit/subscribers_spec.rb +20 -9
  45. data/spec/unit/transition/check_conditions_spec.rb +54 -0
  46. data/spec/unit/transition/inspect_spec.rb +2 -2
  47. data/spec/unit/transition/matches_spec.rb +23 -0
  48. data/spec/unit/transition/states_spec.rb +31 -0
  49. data/spec/unit/transition/to_state_spec.rb +27 -0
  50. data/spec/unit/trigger_spec.rb +22 -0
  51. data/spec/unit/undefined_transition/eql_spec.rb +17 -0
  52. data/tasks/console.rake +1 -0
  53. metadata +39 -23
  54. data/lib/finite_machine/event.rb +0 -146
  55. data/spec/unit/event/add_spec.rb +0 -16
  56. data/spec/unit/event/eql_spec.rb +0 -37
  57. data/spec/unit/event/initialize_spec.rb +0 -38
  58. data/spec/unit/event/inspect_spec.rb +0 -21
  59. data/spec/unit/event/next_transition_spec.rb +0 -35
  60. data/spec/unit/events_chain/check_choice_conditions_spec.rb +0 -20
  61. data/spec/unit/events_chain/insert_spec.rb +0 -26
  62. data/spec/unit/events_chain/select_transition_spec.rb +0 -23
  63. data/spec/unit/transition/parse_states_spec.rb +0 -42
@@ -11,9 +11,8 @@ module FiniteMachine
11
11
  # Initialize a subscribers collection
12
12
  #
13
13
  # @api public
14
- def initialize(machine)
15
- super()
16
- @machine = machine
14
+ def initialize
15
+ super
17
16
  @subscribers = []
18
17
  end
19
18
 
@@ -55,13 +54,25 @@ module FiniteMachine
55
54
 
56
55
  # Visit subscribers and notify
57
56
  #
58
- # @param [FiniteMachine::Event] event
57
+ # @param [HookEvent] hook_event
58
+ # the callback event to notify about
59
59
  #
60
60
  # @return [undefined]
61
61
  #
62
62
  # @api public
63
- def visit(event)
64
- each { |subscriber| synchronize { event.notify subscriber } }
63
+ def visit(hook_event, *data)
64
+ each { |subscriber|
65
+ synchronize { hook_event.notify(subscriber, *data) }
66
+ }
67
+ end
68
+
69
+ # Number of subscribed listeners
70
+ #
71
+ # @return [Integer]
72
+ #
73
+ # @api public
74
+ def size
75
+ synchronize { @subscribers.size }
65
76
  end
66
77
 
67
78
  # Reset subscribers
@@ -6,7 +6,7 @@ module FiniteMachine
6
6
 
7
7
  # @api public
8
8
  def event_queue
9
- Thread.current[:finite_machine_event_queue] ||= FiniteMachine::EventQueue.new
9
+ Thread.current[:finite_machine_event_queue]
10
10
  end
11
11
 
12
12
  # @api public
@@ -5,94 +5,57 @@ module FiniteMachine
5
5
  class Transition
6
6
  include Threadable
7
7
 
8
+ # The event name
8
9
  attr_threadsafe :name
9
10
 
10
- # State transitioning from
11
- attr_threadsafe :from_states
12
-
13
- # State transitioning to
14
- attr_threadsafe :to_states
15
-
16
11
  # Predicates before transitioning
17
12
  attr_threadsafe :conditions
18
13
 
19
14
  # The current state machine
20
15
  attr_threadsafe :machine
21
16
 
22
- # The original from state
23
- attr_threadsafe :from_state
24
-
25
17
  # Check if transition should be cancelled
26
18
  attr_threadsafe :cancelled
27
19
 
28
20
  # All states for this transition event
29
21
  attr_threadsafe :states
30
22
 
31
- # Silence callbacks
32
- attr_threadsafe :silent
33
-
34
23
  # Initialize a Transition
35
24
  #
25
+ # @example
26
+ # attributes = {parsed_states: {green: :yellow}}
27
+ # Transition.new(machine, attributes)
28
+ #
36
29
  # @param [StateMachine] machine
30
+ #
37
31
  # @param [Hash] attrs
38
32
  #
33
+ # @return [Transition]
34
+ #
39
35
  # @api public
40
36
  def initialize(machine, attrs = {})
41
37
  @machine = machine
42
- @name = attrs.fetch(:name, DEFAULT_STATE)
43
- @states = attrs.fetch(:parsed_states, {})
44
- @silent = attrs.fetch(:silent, false)
45
- @from_states = @states.keys
46
- @to_states = @states.values
47
- @from_state = @from_states.first
38
+ @name = attrs[:name]
39
+ @states = attrs.fetch(:states, {})
48
40
  @if = Array(attrs.fetch(:if, []))
49
41
  @unless = Array(attrs.fetch(:unless, []))
50
42
  @conditions = make_conditions
51
- @cancelled = false
43
+ @cancelled = attrs.fetch(:cancelled, false)
52
44
  end
53
45
 
54
- # Create transition with associated helper methods
46
+ # Check if this transition is cancelled or not
55
47
  #
56
- # @param [FiniteMachine::StateMachine] machine
57
- #
58
- # @param [Hash] attrs
59
- #
60
- # @example
61
- # attributes = {parsed_states: {green: :yellow}, silent: true}
62
- # Transition.create(machine, attrbiutes)
63
- #
64
- # @return [Transition]
65
- #
66
- # @api public
67
- def self.create(machine, attrs = {})
68
- transition = new(machine, attrs)
69
- transition.update_transitions
70
- transition.define_state_query_methods
71
- transition
72
- end
73
-
74
- # Decide :to state from available transitions for this event
75
- #
76
- # @return [Symbol]
48
+ # @return [Boolean]
77
49
  #
78
50
  # @api public
79
- def to_state(*args)
80
- if transition_choice?
81
- found_trans = machine.select_choice_transition(name, from_state, *args)
82
-
83
- if found_trans.nil? # no choice found
84
- from_state
85
- else
86
- found_trans.states[from_state] || found_trans.states[ANY_STATE]
87
- end
88
- else
89
- available_trans = machine.transitions[name]
90
- available_trans[from_state] || available_trans[ANY_STATE]
91
- end
51
+ def cancelled?
52
+ @cancelled
92
53
  end
93
54
 
94
55
  # Reduce conditions
95
56
  #
57
+ # @return [Array[Callable]]
58
+ #
96
59
  # @api private
97
60
  def make_conditions
98
61
  @if.map { |c| Callable.new(c) } +
@@ -101,153 +64,62 @@ module FiniteMachine
101
64
 
102
65
  # Verify conditions returning true if all match, false otherwise
103
66
  #
67
+ # @param [Array[Object]] args
68
+ # the arguments for the condition
69
+ #
104
70
  # @return [Boolean]
105
71
  #
106
72
  # @api private
107
- def check_conditions(*args, &block)
73
+ def check_conditions(*args)
108
74
  conditions.all? do |condition|
109
- condition.call(machine.target, *args, &block)
75
+ condition.call(machine.target, *args)
110
76
  end
111
77
  end
112
78
 
113
- # Check if moved to different state or not
79
+ # Check if this transition matches from state
114
80
  #
115
- # @param [Symbol] state
116
- # the current state name
117
- #
118
- # @return [Boolean]
119
- #
120
- # @api public
121
- def same?(state)
122
- states[state] == state || (states[ANY_STATE] == state && from_state == state)
123
- end
124
-
125
- # Check if from matches current state
81
+ # @param [Symbol] from
82
+ # the from state to match against
126
83
  #
127
84
  # @example
128
- # transition.current? # => true
85
+ # transition = Transition.new(machine, states: {:green => :red})
86
+ # transition.matches?(:green) # => true
129
87
  #
130
88
  # @return [Boolean]
131
89
  # Return true if match is found, false otherwise.
132
90
  #
133
91
  # @api public
134
- def current?
135
- [machine.current, ANY_STATE].any? { |state| state == from_state }
136
- end
137
-
138
- # Check if this transition has branching choice or not
139
- #
140
- # @return [Boolean]
141
- #
142
- # @api public
143
- def transition_choice?
144
- matching = machine.transitions[name]
145
- [matching[from_state], matching[ANY_STATE]].any? do |match|
146
- match.is_a?(Array)
147
- end
92
+ def matches?(from)
93
+ states.keys.any? { |state| [ANY_STATE, from].include?(state) }
148
94
  end
149
95
 
150
- # Check if transition can be performed according to constraints
96
+ # Find to state for this transition given the from state
151
97
  #
152
- # @param [Array] args
98
+ # @param [Symbol] from
99
+ # the from state to check
153
100
  #
154
- # @param [Proc] block
101
+ # @example
102
+ # transition = Transition.new(machine, states: {:green => :red})
103
+ # transition.to_state(:green) # => :red
155
104
  #
156
- # @return [Boolean]
105
+ # @return [Symbol]
106
+ # the to state
157
107
  #
158
108
  # @api public
159
- def valid?(*args, &block)
160
- if transition_choice?
161
- machine.check_choice_conditions(name, *args, &block)
162
- else
163
- check_conditions(*args, &block)
164
- end
165
- end
166
-
167
- # Add transition to the machine
168
- #
169
- # @return [Transition]
170
- #
171
- # @api private
172
- def update_transitions
173
- from_states.each do |from|
174
- if (value = machine.transitions[name][from])
175
- machine.transitions[name][from] = [value, states[from]].flatten
176
- else
177
- machine.transitions[name][from] = states[from] || ANY_STATE
178
- end
179
- end
180
- self
181
- end
182
-
183
- # Define helper state mehods for the transition states
184
- #
185
- # @return [Transition]
186
- #
187
- # @api private
188
- def define_state_query_methods
189
- from_states.concat(to_states).each do |state|
190
- define_state_query_method(state)
191
- end
192
- self
193
- end
194
-
195
- # Define state helper method
196
- #
197
- # @param [Symbol] state
198
- #
199
- # @api private
200
- def define_state_query_method(state)
201
- return if machine.respond_to?("#{state}?")
202
- machine.send(:define_singleton_method, "#{state}?") do
203
- machine.is?(state.to_sym)
204
- end
205
- end
206
-
207
- # Set state on the machine
208
- #
209
- # @api private
210
- def update_state(*args)
211
- if transition_choice?
212
- found_trans = machine.select_transition(name, *args)
213
- machine.state = found_trans.to_states.first
109
+ def to_state(from)
110
+ if cancelled?
111
+ from
214
112
  else
215
- transitions = machine.transitions[name]
216
- machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
217
- end
218
- end
219
-
220
- # Find latest from state
221
- #
222
- # Note that for the exit hook the call hasn't happened yet so
223
- # we need to find previous to state when the from is :any.
224
- #
225
- # @return [Object] from_state
226
- #
227
- # @api private
228
- def latest_from_state
229
- sync_shared do
230
- from_state == ANY_STATE ? machine.previous_state : from_state
231
- end
232
- end
233
-
234
- # Execute current transition
235
- #
236
- # @return [nil]
237
- #
238
- # @api private
239
- def execute(*args)
240
- sync_exclusive do
241
- return if cancelled
242
- self.from_state = machine.state
243
- update_state(*args)
244
- machine.previous_state = machine.state
245
- machine.initial_state = machine.state if from_state == DEFAULT_STATE
113
+ states[from] || states[ANY_STATE]
246
114
  end
247
115
  end
248
116
 
249
117
  # Return transition name
250
118
  #
119
+ # @example
120
+ # transition = Transition.new(machine, name: :go)
121
+ # transition.to_s # => 'go'
122
+ #
251
123
  # @return [String]
252
124
  #
253
125
  # @api public
@@ -1,8 +1,14 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'finite_machine/state_parser'
4
+ require 'finite_machine/event_definition'
5
+ require 'finite_machine/state_definition'
6
+
3
7
  module FiniteMachine
4
8
  # A class reponsible for building transition out of parsed states
5
9
  #
10
+ # Used internally by {DSL} to
11
+ #
6
12
  # @api private
7
13
  class TransitionBuilder
8
14
  include Threadable
@@ -14,6 +20,8 @@ module FiniteMachine
14
20
 
15
21
  attr_threadsafe :event_definition
16
22
 
23
+ attr_threadsafe :state_definition
24
+
17
25
  # Initialize a TransitionBuilder
18
26
  #
19
27
  # @example
@@ -24,25 +32,35 @@ module FiniteMachine
24
32
  @machine = machine
25
33
  @attributes = attributes
26
34
  @event_definition = EventDefinition.new(machine)
35
+ @state_definition = StateDefinition.new(machine)
27
36
  end
28
37
 
29
38
  # Creates transitions for the states
30
39
  #
31
40
  # @example
32
- # transition_parser.call([:green, :yellow] => :red)
41
+ # transition_builder.call([:green, :yellow] => :red)
33
42
  #
34
43
  # @param [Hash[Symbol]] states
35
44
  # The states to extract
36
45
  #
37
- # @return [nil]
46
+ # @return [self]
38
47
  #
39
48
  # @api public
40
49
  def call(states)
41
- FiniteMachine::StateParser.new(states).parse do |from, to|
42
- attributes.merge!(parsed_states: { from => to })
43
- transition = Transition.create(machine, attributes)
44
- event_definition.apply(transition)
50
+ StateParser.new(states).parse do |from, to|
51
+ attributes.merge!(states: { from => to })
52
+ transition = Transition.new(machine, attributes)
53
+ name = attributes[:name]
54
+ silent = attributes.fetch(:silent, false)
55
+
56
+ machine.events_chain.add(name, transition)
57
+
58
+ unless machine.respond_to?(name)
59
+ event_definition.apply(name, silent)
60
+ end
61
+ state_definition.apply({ from => to })
45
62
  end
63
+ self
46
64
  end
47
65
  end # TransitionBuilder
48
66
  end # FiniteMachine
@@ -35,10 +35,11 @@ module FiniteMachine
35
35
  # @return [self]
36
36
  #
37
37
  # @api private
38
- def initialize(transition, *data)
39
- @name = transition.name
40
- @from = transition.latest_from_state
41
- @to = transition.to_state(*data)
38
+ # def initialize(transition, *data)
39
+ def initialize(hook_event, to)
40
+ @name = hook_event.event_name
41
+ @from = hook_event.from
42
+ @to = to
42
43
  freeze
43
44
  end
44
45
  end # TransitionEvent
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module FiniteMachine
4
+ # Stand in for lack of matching transition.
5
+ #
6
+ # Used internally by {EventsChain}
7
+ #
8
+ # @api private
9
+ class UndefinedTransition
10
+ include Threadable
11
+
12
+ # Initialize an undefined transition
13
+ #
14
+ # @api private
15
+ def initialize(name)
16
+ self.name = name
17
+ end
18
+
19
+ def to_state(from)
20
+ from
21
+ end
22
+
23
+ def ==(other)
24
+ other.is_a?(UndefinedTransition) && name == other.name
25
+ end
26
+
27
+ protected
28
+
29
+ attr_threadsafe :name
30
+
31
+ end # UndefinedTransition
32
+ end # FiniteMachine