finite_machine 0.7.1 → 0.8.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.
@@ -67,9 +67,9 @@ module FiniteMachine
67
67
  #
68
68
  # @api public
69
69
  def initial(value)
70
- state, name, self.defer = parse(value)
71
- self.initial_event = name
72
- machine.event(name, from: FiniteMachine::DEFAULT_STATE, to: state)
70
+ state, name, self.defer, silent = parse(value)
71
+ self.initial_event = name
72
+ machine.event(name, FiniteMachine::DEFAULT_STATE => state, silent: silent)
73
73
  end
74
74
 
75
75
  # Trigger initial event
@@ -158,12 +158,13 @@ module FiniteMachine
158
158
  #
159
159
  # @api private
160
160
  def parse(value)
161
- unless value.is_a?(Hash)
162
- [value, FiniteMachine::DEFAULT_EVENT_NAME, false]
163
- else
161
+ if value.is_a?(Hash)
164
162
  [value.fetch(:state) { raise_missing_state },
165
163
  value.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME },
166
- value.fetch(:defer) { false }]
164
+ value.fetch(:defer) { false },
165
+ value.fetch(:silent) { true }]
166
+ else
167
+ [value, FiniteMachine::DEFAULT_EVENT_NAME, false, true]
167
168
  end
168
169
  end
169
170
 
@@ -176,8 +177,8 @@ module FiniteMachine
176
177
  end
177
178
  end # DSL
178
179
 
180
+ # A DSL for describing events
179
181
  class EventsDSL < GenericDSL
180
-
181
182
  # Create event and associate transition
182
183
  #
183
184
  # @example
@@ -191,13 +192,19 @@ module FiniteMachine
191
192
  sync_exclusive do
192
193
  attributes = attrs.merge!(name: name)
193
194
  FiniteMachine::StateParser.new(attrs).parse_states do |from, to|
194
- attributes.merge!(parsed_states: { from => to })
195
- Transition.create(machine, attributes)
195
+ if block_given?
196
+ merger = ChoiceMerger.new(self, attributes)
197
+ merger.instance_eval(&block)
198
+ else
199
+ attributes.merge!(parsed_states: { from => to })
200
+ Transition.create(machine, attributes)
201
+ end
196
202
  end
197
203
  end
198
204
  end
199
205
  end # EventsDSL
200
206
 
207
+ # A DSL for describing error conditions
201
208
  class ErrorsDSL < GenericDSL
202
209
  # Add error handler
203
210
  #
@@ -14,10 +14,16 @@ module FiniteMachine
14
14
  # The reference to the state machine for this event
15
15
  attr_threadsafe :machine
16
16
 
17
+ # The silent option for this transition
18
+ attr_threadsafe :silent
19
+
20
+ # Initialize an Event
21
+ #
17
22
  # @api private
18
23
  def initialize(machine, attrs = {})
19
24
  @machine = machine
20
25
  @name = attrs.fetch(:name, DEFAULT_STATE)
26
+ @silent = attrs.fetch(:silent, false)
21
27
  @state_transitions = []
22
28
  # TODO: add event conditions
23
29
  end
@@ -45,21 +51,46 @@ module FiniteMachine
45
51
  #
46
52
  # @api private
47
53
  def next_transition
48
- state_transitions.find do |transition|
49
- transition.from_state == machine.current ||
50
- transition.from_state == ANY_STATE
51
- end || state_transitions.first
54
+ sync_shared do
55
+ state_transitions.find do |transition|
56
+ transition.from_state == machine.current ||
57
+ transition.from_state == ANY_STATE
58
+ end || state_transitions.first
59
+ end
60
+ end
61
+
62
+ # Find transition matching conditions
63
+ #
64
+ # @param [Array[Object]] args
65
+ #
66
+ # return FiniteMachine::TransitionChoice
67
+ #
68
+ # @api private
69
+ def find_transition(*args)
70
+ sync_shared do
71
+ state_transitions.find { |trans| trans.check_conditions(*args) }
72
+ end
52
73
  end
53
74
 
54
75
  # Trigger this event
55
76
  #
77
+ # If silent option is passed the event will not fire any callbacks
78
+ #
79
+ # @example
80
+ # transition = Transition.create(machine, {})
81
+ # transition.call
82
+ #
56
83
  # @return [nil]
57
84
  #
58
85
  # @api public
59
86
  def call(*args, &block)
60
87
  sync_exclusive do
61
88
  _transition = next_transition
62
- machine.send(:transition, _transition, *args, &block)
89
+ if silent
90
+ _transition.call(*args, &block)
91
+ else
92
+ machine.send(:transition, _transition, *args, &block)
93
+ end
63
94
  end
64
95
  end
65
96
 
@@ -142,8 +142,8 @@ module FiniteMachine
142
142
  #
143
143
  # @api private
144
144
  def handle_callback(hook, event)
145
- trans_event = TransitionEvent.build(event.transition)
146
145
  data = event.data
146
+ trans_event = TransitionEvent.build(event.transition, *data)
147
147
  callable = create_callable(hook)
148
148
 
149
149
  if hook.is_a?(Async)
@@ -165,13 +165,19 @@ module FiniteMachine
165
165
  # @example
166
166
  # fsm.can?(:go) # => true
167
167
  #
168
+ # @example
169
+ # fsm.can?(:go, 'Piotr') # checks condition with parameter 'Piotr'
170
+ #
168
171
  # @param [String] event
169
172
  #
170
173
  # @return [Boolean]
171
174
  #
172
175
  # @api public
173
- def can?(event)
174
- transitions[event].key?(current) || transitions[event].key?(ANY_STATE)
176
+ def can?(*args, &block)
177
+ event = args.shift
178
+ valid_state = transitions[event].key?(current)
179
+ valid_state ||= transitions[event].key?(ANY_STATE)
180
+ valid_state &&= events_chain[event].next_transition.valid?(*args, &block)
175
181
  end
176
182
 
177
183
  # Checks if event cannot be triggered
@@ -184,8 +190,8 @@ module FiniteMachine
184
190
  # @return [Boolean]
185
191
  #
186
192
  # @api public
187
- def cannot?(event)
188
- !can?(event)
193
+ def cannot?(*args, &block)
194
+ !can?(*args, &block)
189
195
  end
190
196
 
191
197
  # Checks if terminal state has been reached
@@ -197,6 +203,17 @@ module FiniteMachine
197
203
  is?(final_state)
198
204
  end
199
205
 
206
+ # Restore this machine to a known state
207
+ #
208
+ # @param [Symbol] state
209
+ #
210
+ # @return nil
211
+ #
212
+ # @api public
213
+ def restore!(state)
214
+ sync_exclusive { self.state = state }
215
+ end
216
+
200
217
  # String representation of this machine
201
218
  #
202
219
  # @return [String]
@@ -243,7 +260,7 @@ module FiniteMachine
243
260
  notify HookEvent::Exit, _transition, *args
244
261
 
245
262
  begin
246
- _transition.call
263
+ _transition.call(*args)
247
264
 
248
265
  notify HookEvent::Transition, _transition, *args
249
266
  rescue Exception => e
@@ -7,6 +7,8 @@ module FiniteMachine
7
7
 
8
8
  attr_threadsafe :attrs
9
9
 
10
+ BLACKLIST = [:name, :if, :unless, :silent].freeze
11
+
10
12
  # Initialize a StateParser
11
13
  #
12
14
  # @example
@@ -83,7 +85,7 @@ module FiniteMachine
83
85
  # @api private
84
86
  def ensure_only_states!(attrs)
85
87
  _attrs = attrs.dup
86
- [:name, :if, :unless].each { |key| _attrs.delete(key) }
88
+ BLACKLIST.each { |key| _attrs.delete(key) }
87
89
  raise_not_enough_transitions unless _attrs.any?
88
90
  _attrs
89
91
  end
@@ -29,6 +29,9 @@ module FiniteMachine
29
29
  # All states for this transition event
30
30
  attr_threadsafe :map
31
31
 
32
+ # Silence callbacks
33
+ attr_threadsafe :silent
34
+
32
35
  # Initialize a Transition
33
36
  #
34
37
  # @param [StateMachine] machine
@@ -39,6 +42,7 @@ module FiniteMachine
39
42
  @machine = machine
40
43
  @name = attrs.fetch(:name, DEFAULT_STATE)
41
44
  @map = attrs.fetch(:parsed_states, {})
45
+ @silent = attrs.fetch(:silent, false)
42
46
  @from_states = @map.keys
43
47
  @to_states = @map.values
44
48
  @from_state = @from_states.first
@@ -60,7 +64,7 @@ module FiniteMachine
60
64
  #
61
65
  # @api public
62
66
  def self.create(machine, attrs = {})
63
- _transition = self.new(machine, attrs)
67
+ _transition = new(machine, attrs)
64
68
  _transition.update_transitions
65
69
  _transition.define_state_methods
66
70
  _transition.define_event
@@ -72,8 +76,13 @@ module FiniteMachine
72
76
  # @return [Symbol]
73
77
  #
74
78
  # @api public
75
- def to_state
76
- machine.transitions[name][from_state]
79
+ def to_state(*args)
80
+ if machine.transitions[name][from_state].is_a? Array
81
+ found_trans = machine.events_chain[name].find_transition(*args)
82
+ found_trans.map[from_state]
83
+ else
84
+ machine.transitions[name][from_state]
85
+ end
77
86
  end
78
87
 
79
88
  # Reduce conditions
@@ -84,6 +93,17 @@ module FiniteMachine
84
93
  @unless.map { |c| Callable.new(c).invert }
85
94
  end
86
95
 
96
+ # Verify conditions returning true if all match, false otherwise
97
+ #
98
+ # @return [Boolean]
99
+ #
100
+ # @api private
101
+ def check_conditions(*args, &block)
102
+ conditions.all? do |condition|
103
+ condition.call(machine.target, *args, &block)
104
+ end
105
+ end
106
+
87
107
  # Check if moved to different state or not
88
108
  #
89
109
  # @param [Symbol] state
@@ -106,19 +126,27 @@ module FiniteMachine
106
126
  #
107
127
  # @api public
108
128
  def valid?(*args, &block)
109
- conditions.all? do |condition|
110
- condition.call(machine.target, *args, &block)
129
+ if machine.transitions[name][from_state].is_a? Array
130
+ machine.events_chain[name].state_transitions.any? do |trans|
131
+ trans.check_conditions(*args, &block)
132
+ end
133
+ else
134
+ check_conditions(*args, &block)
111
135
  end
112
136
  end
113
137
 
114
138
  # Add transition to the machine
115
139
  #
116
- # @return [Transition]
140
+ # @return [FiniteMachine::Transition]
117
141
  #
118
142
  # @api private
119
143
  def update_transitions
120
144
  from_states.each do |from|
121
- machine.transitions[name][from] = map[from] || ANY_STATE
145
+ if value = machine.transitions[name][from]
146
+ machine.transitions[name][from] = [value, map[from]].flatten
147
+ else
148
+ machine.transitions[name][from] = map[from] || ANY_STATE
149
+ end
122
150
  end
123
151
  end
124
152
 
@@ -158,7 +186,7 @@ module FiniteMachine
158
186
  #
159
187
  # @api private
160
188
  def define_event_transition(name)
161
- _event = FiniteMachine::Event.new(machine, name: name)
189
+ _event = FiniteMachine::Event.new(machine, name: name, silent: silent)
162
190
  _event << self
163
191
  machine.events_chain[name] = _event
164
192
 
@@ -182,12 +210,17 @@ module FiniteMachine
182
210
  # @return [nil]
183
211
  #
184
212
  # @api private
185
- def call
213
+ def call(*args)
186
214
  sync_exclusive do
187
215
  return if cancelled
188
216
  transitions = machine.transitions[name]
189
217
  self.from_state = machine.state
190
- machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
218
+ if machine.transitions[name][from_state].is_a? Array
219
+ found_trans = machine.events_chain[name].find_transition(*args)
220
+ machine.state = found_trans.to_states.first
221
+ else
222
+ machine.state = transitions[machine.state] || transitions[ANY_STATE] || name
223
+ end
191
224
  machine.initial_state = machine.state if from_state == DEFAULT_STATE
192
225
  end
193
226
  end
@@ -17,11 +17,11 @@ module FiniteMachine
17
17
  # @return [self]
18
18
  #
19
19
  # @api private
20
- def self.build(transition)
20
+ def self.build(transition, *data)
21
21
  instance = new
22
22
  instance.name = transition.name
23
23
  instance.from = transition.from_state
24
- instance.to = transition.to_state
24
+ instance.to = transition.to_state(*data)
25
25
  instance
26
26
  end
27
27
  end # TransitionEvent
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.7.1"
4
+ VERSION = "0.8.0"
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -30,7 +30,7 @@ RSpec.configure do |config|
30
30
 
31
31
  # Remove defined constants
32
32
  config.before :each do
33
- [:Car, :Logger].each do |class_name|
33
+ [:Car, :Logger, :Bug, :User].each do |class_name|
34
34
  if Object.const_defined?(class_name)
35
35
  Object.send(:remove_const, class_name)
36
36
  end
@@ -71,7 +71,7 @@ describe FiniteMachine, 'async_events' do
71
71
  it "permits async callback" do
72
72
  called = []
73
73
  fsm = FiniteMachine.define do
74
- initial :green
74
+ initial state: :green, silent: false
75
75
 
76
76
  events {
77
77
  event :slow, :green => :yellow
@@ -7,7 +7,7 @@ describe FiniteMachine, 'callbacks' do
7
7
  it "triggers default init event" do
8
8
  called = []
9
9
  fsm = FiniteMachine.define do
10
- initial state: :green, defer: true
10
+ initial state: :green, defer: true, silent: false
11
11
 
12
12
  callbacks {
13
13
  # generic state callbacks
@@ -54,7 +54,7 @@ describe FiniteMachine, 'callbacks' do
54
54
  it "executes callbacks in order" do
55
55
  called = []
56
56
  fsm = FiniteMachine.define do
57
- initial :green
57
+ initial state: :green, silent: false
58
58
 
59
59
  events {
60
60
  event :slow, :green => :yellow
@@ -173,7 +173,7 @@ describe FiniteMachine, 'callbacks' do
173
173
  it "maintains transition execution sequence from UML statechart" do
174
174
  called = []
175
175
  fsm = FiniteMachine.define do
176
- initial :previous
176
+ initial state: :previous, silent: false
177
177
 
178
178
  events {
179
179
  event :go, :previous => :next, if: -> { called << 'guard'; true}
@@ -207,7 +207,7 @@ describe FiniteMachine, 'callbacks' do
207
207
  it "allows multiple callbacks for the same state" do
208
208
  called = []
209
209
  fsm = FiniteMachine.define do
210
- initial :green
210
+ initial state: :green, silent: false
211
211
 
212
212
  events {
213
213
  event :slow, :green => :yellow
@@ -549,7 +549,7 @@ describe FiniteMachine, 'callbacks' do
549
549
  it "triggers callbacks only once" do
550
550
  called = []
551
551
  fsm = FiniteMachine.define do
552
- initial :green
552
+ initial state: :green, silent: false
553
553
 
554
554
  events {
555
555
  event :slow, :green => :yellow
@@ -646,7 +646,7 @@ describe FiniteMachine, 'callbacks' do
646
646
  it "groups states from separate events with the same name" do
647
647
  callbacks = []
648
648
  fsm = FiniteMachine.define do
649
- initial :initial
649
+ initial state: :initial, silent: false
650
650
 
651
651
  events {
652
652
  event :bump, :initial => :low
@@ -720,7 +720,7 @@ describe FiniteMachine, 'callbacks' do
720
720
  it "groups states under event name" do
721
721
  callbacks = []
722
722
  fsm = FiniteMachine.define do
723
- initial :initial
723
+ initial state: :initial, silent: false
724
724
 
725
725
  events {
726
726
  event :bump, :initial => :low,
@@ -770,7 +770,7 @@ describe FiniteMachine, 'callbacks' do
770
770
  it "permits state and event with the same name" do
771
771
  called = []
772
772
  fsm = FiniteMachine.define do
773
- initial :on_hook
773
+ initial state: :on_hook, silent: false
774
774
 
775
775
  events {
776
776
  event :off_hook, :on_hook => :off_hook
@@ -795,4 +795,25 @@ describe FiniteMachine, 'callbacks' do
795
795
  'on_enter_on_hook'
796
796
  ]);
797
797
  end
798
+
799
+ it "allows to selectively silence events" do
800
+ called = []
801
+ fsm = FiniteMachine.define do
802
+ initial :yellow
803
+
804
+ events {
805
+ event :go, :yellow => :green, silent: true
806
+ event :stop, :green => :red
807
+ }
808
+
809
+ callbacks {
810
+ on_enter :green do |event| called << 'on_enter_yellow' end
811
+ on_enter :red do |event| called << 'on_enter_red' end
812
+ }
813
+ end
814
+ expect(fsm.current).to eq(:yellow)
815
+ fsm.go
816
+ fsm.stop
817
+ expect(called).to eq(['on_enter_red'])
818
+ end
798
819
  end