finite_machine 0.7.1 → 0.8.0

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