finite_machine 0.8.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile +1 -1
- data/README.md +150 -35
- data/lib/finite_machine.rb +1 -0
- data/lib/finite_machine/catchable.rb +1 -1
- data/lib/finite_machine/definition.rb +61 -0
- data/lib/finite_machine/dsl.rb +45 -10
- data/lib/finite_machine/event.rb +17 -1
- data/lib/finite_machine/hook_event.rb +66 -7
- data/lib/finite_machine/observer.rb +3 -3
- data/lib/finite_machine/state_machine.rb +36 -28
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +2 -2
- data/spec/unit/async_events_spec.rb +1 -1
- data/spec/unit/callbacks_spec.rb +23 -23
- data/spec/unit/can_spec.rb +22 -22
- data/spec/unit/event/eql_spec.rb +37 -0
- data/spec/unit/event/initialize_spec.rb +38 -0
- data/spec/unit/event/inspect_spec.rb +1 -1
- data/spec/unit/event_queue_spec.rb +2 -2
- data/spec/unit/events_chain/check_choice_conditions_spec.rb +2 -2
- data/spec/unit/events_chain/clear_spec.rb +1 -1
- data/spec/unit/events_chain/insert_spec.rb +1 -1
- data/spec/unit/events_spec.rb +17 -20
- data/spec/unit/hook_event/eql_spec.rb +37 -0
- data/spec/unit/hook_event/initialize_spec.rb +22 -0
- data/spec/unit/if_unless_spec.rb +6 -6
- data/spec/unit/initialize_spec.rb +6 -6
- data/spec/unit/is_spec.rb +12 -12
- data/spec/unit/logger_spec.rb +1 -1
- data/spec/unit/respond_to_spec.rb +2 -2
- data/spec/unit/standalone_spec.rb +72 -0
- data/spec/unit/subscribers_spec.rb +2 -2
- data/spec/unit/target_spec.rb +59 -10
- data/spec/unit/{finished_spec.rb → terminated_spec.rb} +38 -8
- metadata +15 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 605485a3bc03f37c758232982a53c6a1454f3f75
|
4
|
+
data.tar.gz: ea94531b99ed2901d14a801388ab26e220f0b6df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac08b6ac004a47a21c6ff3c104b470ebb50b083c3f692f6ce5d865ed2b6527cf0e58dce4983bfd08ed2d000ea5b80277895befada19e26e326506b6326d1bf1f
|
7
|
+
data.tar.gz: b00888c384d18a8f1654b11b26e53a6024c8da9127139ee4f6e3f906c1c0a03f7a5f85982f2af28f494b6097d4ddcdce86401f6bf532ead9a7bda2084418c01c
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
0.9.0 (August 3, 2014)
|
2
|
+
|
3
|
+
* Add Definition class to allow to define standalone state machine
|
4
|
+
* Upgrade RSpec dependency and refactor specs
|
5
|
+
* Change initial helper to simply state name with options
|
6
|
+
* Change HookEvent to be immutable and extend comparison
|
7
|
+
* Change Event to be immutable and extend comparison
|
8
|
+
* Add #build method to HookEvent
|
9
|
+
* Change finished? to terminated? and allow for multiple terminal states
|
10
|
+
* Change to require explicit context to call target methods
|
11
|
+
|
1
12
|
0.8.1 (July 5, 2014)
|
2
13
|
|
3
14
|
* Add EventsChain to handle internal events logic
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -17,7 +17,7 @@ A minimal finite state machine with a straightforward and intuitive syntax. You
|
|
17
17
|
|
18
18
|
* plain object state machine
|
19
19
|
* easy custom object integration
|
20
|
-
* natural DSL for declaring events,
|
20
|
+
* natural DSL for declaring events, callbacks and exceptions
|
21
21
|
* observers (pub/sub) for state changes
|
22
22
|
* ability to check reachable states
|
23
23
|
* ability to check for terminal state
|
@@ -57,6 +57,7 @@ Or install it yourself as:
|
|
57
57
|
* [2.3 Asynchronous transitions](#23-asynchronous-transitions)
|
58
58
|
* [2.4 Single event with multiple from states](#24-single-event-with-multiple-from-states)
|
59
59
|
* [2.5 Grouping states under single event](#25-grouping-states-under-single-event)
|
60
|
+
* [2.6 Silent transitions](#26-silent-transitions)
|
60
61
|
* [3. Conditional transitions](#3-conditional-transitions)
|
61
62
|
* [3.1 Using a Proc](#31-using-a-proc)
|
62
63
|
* [3.2 Using a Symbol](#32-using-a-symbol)
|
@@ -80,10 +81,14 @@ Or install it yourself as:
|
|
80
81
|
* [5.14 Cancelling inside callbacks](#514-cancelling-inside-callbacks)
|
81
82
|
* [6. Errors](#6-errors)
|
82
83
|
* [6.1 Using target](#61-using-target)
|
83
|
-
* [7.
|
84
|
-
* [7.1
|
85
|
-
* [7.2
|
86
|
-
* [8.
|
84
|
+
* [7. Stand-Alone FiniteMachine](#7-stand-alone-finitemachine)
|
85
|
+
* [7.1 Creating a Definition](#71-creating-a-definition)
|
86
|
+
* [7.2 Targeting definition](#72-targeting-definition)
|
87
|
+
* [8. Integration](#8-integration)
|
88
|
+
* [8.1 Plain Ruby Objects](#81-plain-ruby-objects)
|
89
|
+
* [8.2 ActiveRecord](#82-activerecord)
|
90
|
+
* [8.3 Transactions](#83-transactions)
|
91
|
+
* [9. Tips](#9-tips)
|
87
92
|
|
88
93
|
## 1 Usage
|
89
94
|
|
@@ -176,7 +181,7 @@ If you want to defer setting the initial state, pass the `:defer` option to the
|
|
176
181
|
|
177
182
|
```ruby
|
178
183
|
fm = FiniteMachine.define do
|
179
|
-
initial
|
184
|
+
initial :green, defer: true # Defer calling :init event
|
180
185
|
|
181
186
|
events {
|
182
187
|
event :slow, :green => :yellow
|
@@ -184,7 +189,7 @@ fm = FiniteMachine.define do
|
|
184
189
|
}
|
185
190
|
end
|
186
191
|
fm.current # => :none
|
187
|
-
fm.init
|
192
|
+
fm.init # execute initial transition
|
188
193
|
fm.current # => :green
|
189
194
|
```
|
190
195
|
|
@@ -192,7 +197,7 @@ If your target object already has `init` method or one of the events names redef
|
|
192
197
|
|
193
198
|
```ruby
|
194
199
|
fm = FiniteMachine.define do
|
195
|
-
initial
|
200
|
+
initial :green, event: :start, defer: true # Rename event from :init to :start
|
196
201
|
|
197
202
|
events {
|
198
203
|
event :slow, :green => :yellow
|
@@ -201,7 +206,7 @@ fm = FiniteMachine.define do
|
|
201
206
|
end
|
202
207
|
|
203
208
|
fm.current # => :none
|
204
|
-
fm.start
|
209
|
+
fm.start # => call the renamed event
|
205
210
|
fm.current # => :green
|
206
211
|
```
|
207
212
|
|
@@ -209,7 +214,7 @@ By default the `initial` does not trigger any callbacks. If you need to fire cal
|
|
209
214
|
|
210
215
|
```ruby
|
211
216
|
fm = FiniteMachine.define do
|
212
|
-
initial
|
217
|
+
initial :green, silent: false # callbacks are triggered
|
213
218
|
|
214
219
|
events {
|
215
220
|
event :slow, :green => :yellow
|
@@ -220,28 +225,30 @@ end
|
|
220
225
|
|
221
226
|
### 1.3 terminal
|
222
227
|
|
223
|
-
To specify a final state **FiniteMachine** uses the `terminal` method.
|
228
|
+
To specify a final state **FiniteMachine** uses the `terminal` method. The `terminal` can accpet more than one state.
|
224
229
|
|
225
230
|
```ruby
|
226
231
|
fm = FiniteMachine.define do
|
227
232
|
initial :green
|
233
|
+
|
228
234
|
terminal :red
|
229
235
|
|
230
236
|
events {
|
231
237
|
event :slow, :green => :yellow
|
232
238
|
event :stop, :yellow => :red
|
239
|
+
event :go, :red => :green
|
233
240
|
}
|
234
241
|
end
|
235
242
|
```
|
236
243
|
|
237
|
-
When the terminal state has been specified, you can use `
|
244
|
+
When the terminal state has been specified, you can use `terminated?` method on the state machine instance to verify if the terminal state has been reached or not.
|
238
245
|
|
239
246
|
```ruby
|
240
|
-
fm.
|
247
|
+
fm.terminated? # => false
|
241
248
|
fm.slow
|
242
|
-
fm.
|
249
|
+
fm.terminated? # => false
|
243
250
|
fm.stop
|
244
|
-
fm.
|
251
|
+
fm.terminated? # => true
|
245
252
|
```
|
246
253
|
|
247
254
|
### 1.4 is?
|
@@ -317,7 +324,7 @@ fm = FiniteMachine.define do
|
|
317
324
|
end
|
318
325
|
```
|
319
326
|
|
320
|
-
Furthermore, the context created through `target` helper will allow you to reference and call methods from another object.
|
327
|
+
Furthermore, the context created through `target` helper will allow you to reference and call methods from another object inside your callbacks. You can reference external context by calling `target`.
|
321
328
|
|
322
329
|
```ruby
|
323
330
|
car = Car.new
|
@@ -332,13 +339,13 @@ fm = FiniteMachine.define do
|
|
332
339
|
}
|
333
340
|
|
334
341
|
callbacks {
|
335
|
-
on_enter_start do |event| turn_engine_on end
|
336
|
-
on_exit_start do |event| turn_engine_off end
|
342
|
+
on_enter_start do |event| target.turn_engine_on end
|
343
|
+
on_exit_start do |event| target.turn_engine_off end
|
337
344
|
}
|
338
345
|
end
|
339
346
|
```
|
340
347
|
|
341
|
-
For more complex example see [Integration](#
|
348
|
+
For more complex example see [Integration](#8-integration) section.
|
342
349
|
|
343
350
|
Finally, you can always reference an external context inside the **FiniteMachine** by simply calling `target`, for instance, to reference it inside a callback:
|
344
351
|
|
@@ -476,6 +483,24 @@ fm = FiniteMachine.define do
|
|
476
483
|
}
|
477
484
|
```
|
478
485
|
|
486
|
+
### 2.6 Silent transitions
|
487
|
+
|
488
|
+
The **FiniteMachine** allows to selectively silence events and thus prevent any callbacks from firing. Using the `silent` option passed to event definition like so:
|
489
|
+
|
490
|
+
```ruby
|
491
|
+
fm = FiniteMachine.define do
|
492
|
+
initial :yellow
|
493
|
+
|
494
|
+
events {
|
495
|
+
event :go :yellow => :green, silent: true
|
496
|
+
event :stop, :green => :red
|
497
|
+
}
|
498
|
+
end
|
499
|
+
|
500
|
+
fsm.go # no callbacks
|
501
|
+
fms.stop # callbacks are fired
|
502
|
+
```
|
503
|
+
|
479
504
|
## 3 Conditional transitions
|
480
505
|
|
481
506
|
Each event takes an optional `:if` and `:unless` options which act as a predicate for the transition. The `:if` and `:unless` can take a symbol, a string, a Proc or an array. Use `:if` option when you want to specify when the transition **should** happen. If you want to specify when the transition **should not** happen then use `:unless` option.
|
@@ -524,9 +549,9 @@ fm = FiniteMachine.define do
|
|
524
549
|
target car
|
525
550
|
|
526
551
|
events {
|
527
|
-
event :start, :neutral => :one, if: -> (
|
528
|
-
|
529
|
-
|
552
|
+
event :start, :neutral => :one, if: -> (target, state) {
|
553
|
+
target.engine_on = state
|
554
|
+
target.engine_on?
|
530
555
|
}
|
531
556
|
}
|
532
557
|
end
|
@@ -838,8 +863,8 @@ fm = FiniteMachine.define do
|
|
838
863
|
}
|
839
864
|
|
840
865
|
callbacks {
|
841
|
-
on_enter_reverse { |event| turn_reverse_lights_on }
|
842
|
-
on_exit_reverse { |event| turn_reverse_lights_off }
|
866
|
+
on_enter_reverse { |event| target.turn_reverse_lights_on }
|
867
|
+
on_exit_reverse { |event| target.turn_reverse_lights_off }
|
843
868
|
}
|
844
869
|
end
|
845
870
|
```
|
@@ -850,8 +875,6 @@ Note that you can also fire events from callbacks.
|
|
850
875
|
fm = FiniteMachine.define do
|
851
876
|
initial :neutral
|
852
877
|
|
853
|
-
target car
|
854
|
-
|
855
878
|
events {
|
856
879
|
event :forward, [:reverse, :neutral] => :one
|
857
880
|
event :back, [:neutral, :one] => :reverse
|
@@ -865,7 +888,7 @@ end
|
|
865
888
|
fm.back # => Go Piotr!
|
866
889
|
```
|
867
890
|
|
868
|
-
For more complex example see [Integration](#
|
891
|
+
For more complex example see [Integration](#8-integration) section.
|
869
892
|
|
870
893
|
### 5.12 Defining callbacks
|
871
894
|
|
@@ -987,11 +1010,89 @@ fm = FiniteMachine.define do
|
|
987
1010
|
end
|
988
1011
|
```
|
989
1012
|
|
990
|
-
## 7
|
1013
|
+
## 7 Stand-Alone FiniteMachine
|
1014
|
+
|
1015
|
+
**FiniteMachine** allows you to seperate your state machine from the target class so that you can keep your concerns broken in small maintainable pieces.
|
1016
|
+
|
1017
|
+
### 7.1 Creating a Definition
|
1018
|
+
|
1019
|
+
You can turn a class into a **FiniteMachine** by simply subclassing `FiniteMachine::Definition`. As a rule of thumb, every single public method of the **FiniteMachine** is available inside your class:
|
1020
|
+
|
1021
|
+
```ruby
|
1022
|
+
class Engine < FiniteMachine::Definition
|
1023
|
+
initial :neutral
|
1024
|
+
|
1025
|
+
events {
|
1026
|
+
event :forward, [:reverse, :neutral] => :one
|
1027
|
+
event :shift, :one => :two
|
1028
|
+
event :back, [:neutral, :one] => :reverse
|
1029
|
+
}
|
1030
|
+
|
1031
|
+
callbacks {
|
1032
|
+
on_enter :reverse do |event|
|
1033
|
+
target.turn_reverse_lights_on
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
on_exit :reverse do |event|
|
1037
|
+
target.turn_reverse_lights_off
|
1038
|
+
end
|
1039
|
+
}
|
1040
|
+
|
1041
|
+
handlers {
|
1042
|
+
handle FiniteMachine::InvalidStateError do |exception| ... end
|
1043
|
+
}
|
1044
|
+
end
|
1045
|
+
```
|
1046
|
+
|
1047
|
+
### 7.2 Targeting definition
|
1048
|
+
|
1049
|
+
The next step is to instantiate your state machine and use `target` to load specific context.
|
1050
|
+
|
1051
|
+
```ruby
|
1052
|
+
class Car
|
1053
|
+
def turn_reverse_lights_off
|
1054
|
+
@reverse_lights = false
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def turn_reverse_lights_on
|
1058
|
+
@reverse_lights = true
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
def reverse_lights?
|
1062
|
+
@reverse_lights ||= false
|
1063
|
+
end
|
1064
|
+
end
|
1065
|
+
```
|
1066
|
+
Thus, to associate `Engine` to `Car` do:
|
1067
|
+
|
1068
|
+
```ruby
|
1069
|
+
car = Car.new
|
1070
|
+
engine = Engine.new
|
1071
|
+
engine.target car
|
1072
|
+
|
1073
|
+
car.reverse_lignts? # => false
|
1074
|
+
engine.back
|
1075
|
+
car.reverse_lights? # => true
|
1076
|
+
```
|
1077
|
+
|
1078
|
+
Alternatively, create method inside the `Car` that will do the integration like so
|
1079
|
+
|
1080
|
+
```ruby
|
1081
|
+
class Car
|
1082
|
+
... # as above
|
1083
|
+
def engine
|
1084
|
+
@engine ||= Engine.new
|
1085
|
+
@engine.target(self)
|
1086
|
+
@engine
|
1087
|
+
end
|
1088
|
+
end
|
1089
|
+
```
|
1090
|
+
|
1091
|
+
## 8 Integration
|
991
1092
|
|
992
1093
|
Since **FiniteMachine** is an object in its own right, it leaves integration with other systems up to you. In contrast to other Ruby libraries, it does not extend from models (i.e. ActiveRecord) to transform them into a state machine or require mixing into exisiting classes.
|
993
1094
|
|
994
|
-
###
|
1095
|
+
### 8.1 Plain Ruby Objects
|
995
1096
|
|
996
1097
|
In order to use **FiniteMachine** with an object, you need to define a method that will construct the state machine. You can implement the state machine using the `define` DSL or create a seperate object that can be instantiated. To complete integration you will need to specify `target` context to allow state machine to communicate with the other methods inside the class like so:
|
997
1098
|
|
@@ -1025,11 +1126,11 @@ class Car
|
|
1025
1126
|
|
1026
1127
|
callbacks {
|
1027
1128
|
on_enter :reverse do |event|
|
1028
|
-
turn_reverse_lights_on
|
1129
|
+
target.turn_reverse_lights_on
|
1029
1130
|
end
|
1030
1131
|
|
1031
1132
|
on_exit :reverse do |event|
|
1032
|
-
turn_reverse_lights_off
|
1133
|
+
target.turn_reverse_lights_off
|
1033
1134
|
end
|
1034
1135
|
|
1035
1136
|
on_transition do |event|
|
@@ -1055,7 +1156,7 @@ car.gears.current # => :reverse
|
|
1055
1156
|
car.reverse_lights_on? # => true
|
1056
1157
|
```
|
1057
1158
|
|
1058
|
-
###
|
1159
|
+
### 8.2 ActiveRecord
|
1059
1160
|
|
1060
1161
|
In order to integrate **FiniteMachine** with ActiveRecord use the `target` helper to reference the current class and call ActiveRecord methods inside the callbacks to persist the state.
|
1061
1162
|
|
@@ -1082,8 +1183,8 @@ class Account < ActiveRecord::Base
|
|
1082
1183
|
|
1083
1184
|
callbacks {
|
1084
1185
|
on_enter_state do |event|
|
1085
|
-
|
1086
|
-
save
|
1186
|
+
target.state = event.to
|
1187
|
+
target.save
|
1087
1188
|
end
|
1088
1189
|
}
|
1089
1190
|
end
|
@@ -1098,7 +1199,21 @@ account.manage.authorize
|
|
1098
1199
|
account.state # => :access
|
1099
1200
|
```
|
1100
1201
|
|
1101
|
-
|
1202
|
+
### 8.3 Transactions
|
1203
|
+
|
1204
|
+
When using **FiniteMachine** with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section 8.2 one can run event in transaction in the following way:
|
1205
|
+
|
1206
|
+
```ruby
|
1207
|
+
ActiveRecord::Base.transaction do
|
1208
|
+
account.manage.enqueue
|
1209
|
+
end
|
1210
|
+
```
|
1211
|
+
|
1212
|
+
If the transition fails it will raise `TransitionError` which will cause the transaction to rollback.
|
1213
|
+
|
1214
|
+
Please check the ORM of your choice if it supports database transactions.
|
1215
|
+
|
1216
|
+
## 9 Tips
|
1102
1217
|
|
1103
1218
|
Creating a standalone **FiniteMachine** brings a number of benefits, one of them being easier testing. This is especially true if the state machine is extremely complex itself. Ideally, you would test the machine in isolation and then integrate it with other objects or ORMs.
|
1104
1219
|
|
data/lib/finite_machine.rb
CHANGED
@@ -24,6 +24,7 @@ require "finite_machine/logger"
|
|
24
24
|
require "finite_machine/transition"
|
25
25
|
require "finite_machine/transition_event"
|
26
26
|
require "finite_machine/dsl"
|
27
|
+
require "finite_machine/definition"
|
27
28
|
require "finite_machine/state_machine"
|
28
29
|
require "finite_machine/subscribers"
|
29
30
|
require "finite_machine/state_parser"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# A class responsible for defining standalone state machine
|
5
|
+
class Definition
|
6
|
+
# The machine deferreds
|
7
|
+
#
|
8
|
+
# @return [Array[Proc]]
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
def self.deferreds
|
12
|
+
@deferreds ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add deferred
|
16
|
+
#
|
17
|
+
# @param [Proc] deferred
|
18
|
+
# the deferred execution
|
19
|
+
#
|
20
|
+
# @return [Array[Proc]]
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
def self.add_deferred(deferred)
|
24
|
+
deferreds << deferred
|
25
|
+
end
|
26
|
+
|
27
|
+
# Instantiate a new Definition
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# class Engine < FiniteMachine::Definition
|
31
|
+
# ...
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# engine = Engine.new
|
35
|
+
#
|
36
|
+
# @return [FiniteMachine::StateMachine]
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def self.new(*args)
|
40
|
+
context = self
|
41
|
+
FiniteMachine.define(*args) do
|
42
|
+
context.deferreds.each { |d| d.call(self) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Delay lookup of DSL method
|
47
|
+
#
|
48
|
+
# @param [Symbol] method_name
|
49
|
+
#
|
50
|
+
# @return [nil]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
def self.method_missing(method_name, *arguments, &block)
|
54
|
+
deferred = proc do |name, args, blok, object|
|
55
|
+
object.send(name, *args, &blok)
|
56
|
+
end
|
57
|
+
deferred = deferred.curry(4)[method_name][arguments][block]
|
58
|
+
add_deferred(deferred)
|
59
|
+
end
|
60
|
+
end # Definition
|
61
|
+
end # FiniteMachine
|