finite_machine 0.10.2 → 0.11.0

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