finite_machine 0.3.0 → 0.4.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 +10 -0
- data/README.md +85 -14
- data/examples/atm.rb +45 -0
- data/examples/bug_system.rb +145 -0
- data/lib/finite_machine.rb +16 -4
- data/lib/finite_machine/async_call.rb +10 -1
- data/lib/finite_machine/dsl.rb +27 -8
- data/lib/finite_machine/event_queue.rb +12 -10
- data/lib/finite_machine/logger.rb +23 -0
- data/lib/finite_machine/observer.rb +11 -3
- data/lib/finite_machine/state_machine.rb +2 -4
- data/lib/finite_machine/subscribers.rb +36 -2
- data/lib/finite_machine/thread_context.rb +3 -1
- data/lib/finite_machine/transition.rb +32 -6
- data/lib/finite_machine/version.rb +1 -1
- data/spec/unit/async_events_spec.rb +4 -4
- data/spec/unit/callbacks_spec.rb +48 -6
- data/spec/unit/events_spec.rb +15 -0
- data/spec/unit/if_unless_spec.rb +90 -60
- data/spec/unit/initialize_spec.rb +48 -1
- data/spec/unit/inspect_spec.rb +25 -0
- data/spec/unit/logger_spec.rb +33 -0
- data/spec/unit/subscribers_spec.rb +31 -0
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b69fcd7446203267dc8130e4c986ea334e4b26ec
|
4
|
+
data.tar.gz: 3a4bca63a58bf4a3c0a509cc99778d66771a535e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d5c2636ea7607b138737e263771b5269d950f1403079e67dfd41d4940241f41cf62d1118cfe7ae177cfe3a716985cad9f33f76313ada9ce4424ba3fcbbd89511
|
7
|
+
data.tar.gz: c40fda5ae25615ca3d2ee239dc59a952596c3860f309c22b8d01e8b4b8acd10f0d7aeb7ddb2f1dc80aecd46c9bd4b3b6e0fd456b28b14dfc9f34144b7ef80fac
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
0.4.0 (April 13, 2014)
|
2
|
+
|
3
|
+
* Change initial state to stop firing event notification
|
4
|
+
* Fix initial to accept any state object
|
5
|
+
* Add logger
|
6
|
+
* Add ability to cancel transitions inside callbacks
|
7
|
+
* Fix proc conditions to accept aditional arguments
|
8
|
+
* Increase test coverage to 97%
|
9
|
+
* Add ability to force transitions
|
10
|
+
|
1
11
|
0.3.0 (March 30, 2014)
|
2
12
|
|
3
13
|
* Move development dependencies to Gemfile
|
data/README.md
CHANGED
@@ -43,11 +43,38 @@ Or install it yourself as:
|
|
43
43
|
## Contents
|
44
44
|
|
45
45
|
* [1. Usage](#1-usage)
|
46
|
+
* [1.1 Current](#11-current)
|
47
|
+
* [1.2 Initial](#12-initial)
|
48
|
+
* [1.3 Terminal](#13-terminal)
|
49
|
+
* [1.4 is?](#14-is)
|
50
|
+
* [1.5 can? and cannot?](#15-can-and-cannot)
|
51
|
+
* [1.6 states](#16-states)
|
52
|
+
* [1.7 target](#17-target)
|
46
53
|
* [2. Transitions](#2-transitions)
|
54
|
+
* [2.1 Performing transitions](#21-performing-transitions)
|
55
|
+
* [2.2 Forcing transitions](#22-forcing-transitions)
|
56
|
+
* [2.3 Asynchronous transitions](#23-asynchronous-transitions)
|
57
|
+
* [2.4 Single event with multiple from states](#24-single-event-with-multiple-from-states)
|
47
58
|
* [3. Conditional transitions](#3-conditional-transitions)
|
59
|
+
* [3.1 Using a Proc](#31-using-a-proc)
|
60
|
+
* [3.2 Using a Symbol](#32-using-a-symbol)
|
61
|
+
* [3.3 Using a String](#33-using-a-string)
|
62
|
+
* [3.4 Combining transition conditions](#34-combining-transition-conditions)
|
48
63
|
* [4. Callbacks](#4-callbacks)
|
64
|
+
* [4.1 on_enter](#41-on_enter)
|
65
|
+
* [4.2 on_transition](#42-on_transition)
|
66
|
+
* [4.3 on_exit](#43-on_exit)
|
67
|
+
* [4.4 once_on](#44-once_on)
|
68
|
+
* [4.5 parameters](#45-parameters)
|
69
|
+
* [4.6 Same kind of callbacks](#46-same-kind-of-callbacks)
|
70
|
+
* [4.7 Fluid callbacks](#47-fluid-callbacks)
|
71
|
+
* [4.8 Executing methods inside callbacks](#48-executing-methods-inside-callbacks)
|
72
|
+
* [4.9 Defining callbacks](#49-defining-callbacks)
|
73
|
+
* [4.10 Cancelling inside callbacks](#410-cancelling-inside-callbacks)
|
49
74
|
* [5. Errors](#5-errors)
|
75
|
+
* [5.1 Using target](#51-using-target)
|
50
76
|
* [6. Integration](#6-integration)
|
77
|
+
* [6.1 ActiveRecord](#61-activerecord)
|
51
78
|
* [7. Tips](#7-tips)
|
52
79
|
|
53
80
|
## 1 Usage
|
@@ -98,9 +125,11 @@ fm = FiniteMachine.define do
|
|
98
125
|
end
|
99
126
|
|
100
127
|
fm.current # => :none
|
128
|
+
fm.start
|
129
|
+
fm.current # => :green
|
101
130
|
```
|
102
131
|
|
103
|
-
If you specify initial state using the `initial` helper,
|
132
|
+
If you specify initial state using the `initial` helper, the state machine will be created already in that state.
|
104
133
|
|
105
134
|
```ruby
|
106
135
|
fm = FiniteMachine.define do
|
@@ -115,34 +144,36 @@ end
|
|
115
144
|
fm.current # => :green
|
116
145
|
```
|
117
146
|
|
118
|
-
If
|
147
|
+
If you want to defer setting the initial state, pass the `:defer` option to the `initial` helper. By default **FiniteMachine** will create `init` event that will allow to transition from `:none` state to the new state.
|
119
148
|
|
120
149
|
```ruby
|
121
150
|
fm = FiniteMachine.define do
|
122
|
-
initial :green,
|
151
|
+
initial state: :green, defer: true
|
123
152
|
|
124
153
|
events {
|
125
154
|
event :slow, :green => :yellow
|
126
155
|
event :stop, :yellow => :red
|
127
156
|
}
|
128
157
|
end
|
129
|
-
|
158
|
+
fm.current # => :none
|
159
|
+
fm.init
|
130
160
|
fm.current # => :green
|
131
161
|
```
|
132
162
|
|
133
|
-
If
|
163
|
+
If your target object already has `init` method or one of the events names redefines `init`, you can use different name by passing `:event` option to `initial` helper.
|
134
164
|
|
135
165
|
```ruby
|
136
166
|
fm = FiniteMachine.define do
|
137
|
-
initial state: :green, defer: true
|
167
|
+
initial state: :green, event: :start, defer: true
|
138
168
|
|
139
169
|
events {
|
140
170
|
event :slow, :green => :yellow
|
141
171
|
event :stop, :yellow => :red
|
142
172
|
}
|
143
173
|
end
|
174
|
+
|
144
175
|
fm.current # => :none
|
145
|
-
fm.
|
176
|
+
fm.start
|
146
177
|
fm.current # => :green
|
147
178
|
```
|
148
179
|
|
@@ -286,7 +317,11 @@ fm.go('Piotr!')
|
|
286
317
|
fm.current # => :green
|
287
318
|
```
|
288
319
|
|
289
|
-
### 2.2
|
320
|
+
### 2.2 Forcing transitions
|
321
|
+
|
322
|
+
When you declare event, for instance `ready`, the **FiniteMachine** will provide a dangerous version with a bang `ready!`. In the case when you need to perform transition disregarding current state reachable states, the `ready!` will transition without any validations or callbacks.
|
323
|
+
|
324
|
+
### 2.3 Asynchronous transitions
|
290
325
|
|
291
326
|
By default the transitions will be fired synchronosuly.
|
292
327
|
|
@@ -303,7 +338,7 @@ In order to fire the event transition asynchronously use the `async` scope like
|
|
303
338
|
fm.async.ready # => executes in separate Thread
|
304
339
|
```
|
305
340
|
|
306
|
-
### 2.
|
341
|
+
### 2.4 Single event with multiple from states
|
307
342
|
|
308
343
|
If an event transitions from multiple states to the same state then all the states can be grouped into an array.
|
309
344
|
Altenatively, you can create separte events under the same name for each transition that needs combining.
|
@@ -342,10 +377,12 @@ fm.slow # doesn't transition to :yellow state
|
|
342
377
|
fm.current # => :green
|
343
378
|
```
|
344
379
|
|
345
|
-
|
380
|
+
Provided your **FiniteMachine** is associated with another object through `target` helper. Then the target object together with event arguments will be passed to the `:if` or `:unless` condition scope.
|
346
381
|
|
347
382
|
```ruby
|
348
383
|
class Car
|
384
|
+
attr_accessor :engine_on
|
385
|
+
|
349
386
|
def turn_engine_on
|
350
387
|
@engine_on = true
|
351
388
|
end
|
@@ -368,14 +405,21 @@ fm = FiniteMachine.define do
|
|
368
405
|
target car
|
369
406
|
|
370
407
|
events {
|
371
|
-
event :start, :neutral => :one, if:
|
408
|
+
event :start, :neutral => :one, if: -> (_car, state) {
|
409
|
+
_car.engine_on = state
|
410
|
+
_car.engine_on?
|
411
|
+
}
|
372
412
|
}
|
373
413
|
end
|
374
414
|
|
375
|
-
fm.start
|
376
|
-
fm.current
|
415
|
+
fm.start(false)
|
416
|
+
fm.current # => :neutral
|
417
|
+
fm.start(true)
|
418
|
+
fm.current # => :one
|
377
419
|
```
|
378
420
|
|
421
|
+
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [4.10 Cancelling inside callbacks](#410-cancelling-inside-callbacks)
|
422
|
+
|
379
423
|
### 3.2 Using a Symbol
|
380
424
|
|
381
425
|
You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
|
@@ -643,6 +687,33 @@ fm.on_enter_yellow do |event|
|
|
643
687
|
end
|
644
688
|
```
|
645
689
|
|
690
|
+
### 4.10 Cancelling inside callbacks
|
691
|
+
|
692
|
+
Preferred way to handle cancelling transitions is to use [3 Conditional transitions](#3-conditional-transitions). However if the logic is more than one liner you can cancel the event, hence the transition by returning `FiniteMachine::CANCELLED` constant from the callback scope. The two ways you can affect the event are
|
693
|
+
|
694
|
+
* `on_exit :state_name`
|
695
|
+
* `on_enter :event_name`
|
696
|
+
|
697
|
+
For example
|
698
|
+
|
699
|
+
```ruby
|
700
|
+
fm = FiniteMachine.define do
|
701
|
+
initial :red
|
702
|
+
|
703
|
+
events {
|
704
|
+
event :ready, :red => :yellow
|
705
|
+
event :go, :yellow => :green
|
706
|
+
event :stop, :green => :red
|
707
|
+
}
|
708
|
+
callbacks {
|
709
|
+
on_exit :red do |event| FiniteMachine::CANCELLED end
|
710
|
+
}
|
711
|
+
end
|
712
|
+
|
713
|
+
fm.ready
|
714
|
+
fm.current # => :red
|
715
|
+
```
|
716
|
+
|
646
717
|
## 5 Errors
|
647
718
|
|
648
719
|
By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
|
@@ -792,7 +863,7 @@ account.state # => :access
|
|
792
863
|
|
793
864
|
## 7 Tips
|
794
865
|
|
795
|
-
Creating a standalone **FiniteMachine** brings
|
866
|
+
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.
|
796
867
|
|
797
868
|
## Contributing
|
798
869
|
|
data/examples/atm.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'finite_machine'
|
4
|
+
|
5
|
+
class Account
|
6
|
+
attr_accessor :number
|
7
|
+
|
8
|
+
def verify(account_number, pin)
|
9
|
+
return account_number == 123456 && pin == 666
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
account = Account.new
|
14
|
+
|
15
|
+
atm = FiniteMachine.define do
|
16
|
+
initial :unauthorized
|
17
|
+
|
18
|
+
target account
|
19
|
+
|
20
|
+
events {
|
21
|
+
event :authorize, :unauthorized => :authorized, if: -> (account, account_number, pin) {
|
22
|
+
account.verify(account_number, pin)
|
23
|
+
}
|
24
|
+
event :deauthorize, :authorized => :unauthorized
|
25
|
+
}
|
26
|
+
|
27
|
+
callbacks {
|
28
|
+
on_exit :unauthorized do |event, account_number, pin|
|
29
|
+
# if verify(account_number, pin)
|
30
|
+
self.number = account_number
|
31
|
+
# else
|
32
|
+
# puts "Invalid Account and/or PIN"
|
33
|
+
# FiniteMachine::CANCELLED
|
34
|
+
# end
|
35
|
+
end
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
atm.authorize(111222, 666)
|
40
|
+
puts "authorized: #{atm.authorized?}"
|
41
|
+
puts "Number: #{account.number}"
|
42
|
+
|
43
|
+
atm.authorize(123456, 666)
|
44
|
+
puts "authorized: #{atm.authorized?}"
|
45
|
+
puts "Number: #{account.number}"
|
@@ -0,0 +1,145 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
|
3
|
+
require 'finite_machine'
|
4
|
+
|
5
|
+
class User
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
def initialize(name)
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Manager < User
|
14
|
+
attr_accessor :developers
|
15
|
+
|
16
|
+
def initialize(name)
|
17
|
+
super
|
18
|
+
@developers = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def manages(developer)
|
22
|
+
@developers << developer
|
23
|
+
end
|
24
|
+
|
25
|
+
def assign(bug)
|
26
|
+
developer = @developers.first
|
27
|
+
bug.assign
|
28
|
+
developer.bug = bug
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Tester < User
|
33
|
+
def report(bug)
|
34
|
+
bug.report
|
35
|
+
end
|
36
|
+
|
37
|
+
def reopen(bug)
|
38
|
+
bug.reopen
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Developer < User
|
43
|
+
attr_accessor :bug
|
44
|
+
|
45
|
+
def work_on
|
46
|
+
bug.start
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve
|
50
|
+
bug.close
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class BugSystem
|
55
|
+
attr_accessor :managers
|
56
|
+
|
57
|
+
def initialize(managers = [])
|
58
|
+
@managers = managers
|
59
|
+
end
|
60
|
+
|
61
|
+
def notify_manager(bug)
|
62
|
+
manager = @managers.first
|
63
|
+
manager.assign(bug)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Bug
|
68
|
+
attr_accessor :name
|
69
|
+
attr_accessor :priority
|
70
|
+
# fake belongs_to relationship
|
71
|
+
attr_accessor :bug_system
|
72
|
+
|
73
|
+
def initialize(name, priority)
|
74
|
+
@name = name
|
75
|
+
@priority = priority
|
76
|
+
end
|
77
|
+
|
78
|
+
def report
|
79
|
+
status.report
|
80
|
+
end
|
81
|
+
|
82
|
+
def assign
|
83
|
+
status.assign
|
84
|
+
end
|
85
|
+
|
86
|
+
def start
|
87
|
+
status.start
|
88
|
+
end
|
89
|
+
|
90
|
+
def close
|
91
|
+
status.close
|
92
|
+
end
|
93
|
+
|
94
|
+
def reopen
|
95
|
+
status.reopen
|
96
|
+
end
|
97
|
+
|
98
|
+
def status
|
99
|
+
context = self
|
100
|
+
@status ||= FiniteMachine.define do
|
101
|
+
target context
|
102
|
+
|
103
|
+
events {
|
104
|
+
event :report, :none => :new
|
105
|
+
event :assign, :new => :assigned
|
106
|
+
event :start, :assigned => :in_progress
|
107
|
+
event :close, [:in_progress, :reopened] => :resolved
|
108
|
+
event :reopen, :resolved => :reopened
|
109
|
+
}
|
110
|
+
|
111
|
+
callbacks {
|
112
|
+
on_enter :new do |event|
|
113
|
+
bug_system.notify_manager(self)
|
114
|
+
end
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
tester = Tester.new("John")
|
121
|
+
manager = Manager.new("David")
|
122
|
+
developer = Developer.new("Piotr")
|
123
|
+
manager.manages(developer)
|
124
|
+
|
125
|
+
bug_system = BugSystem.new([manager])
|
126
|
+
bug = Bug.new(:trojan, :high)
|
127
|
+
bug.bug_system = bug_system
|
128
|
+
|
129
|
+
puts "A BUG's LIFE"
|
130
|
+
puts "#1 #{bug.status.current}"
|
131
|
+
|
132
|
+
tester.report(bug)
|
133
|
+
puts "#2 #{bug.status.current}"
|
134
|
+
|
135
|
+
developer.work_on
|
136
|
+
puts "#3 #{bug.status.current}"
|
137
|
+
|
138
|
+
developer.resolve
|
139
|
+
puts "#4 #{bug.status.current}"
|
140
|
+
|
141
|
+
tester.reopen(bug)
|
142
|
+
puts "#5 #{bug.status.current}"
|
143
|
+
|
144
|
+
developer.resolve
|
145
|
+
puts "#6 #{bug.status.current}"
|
data/lib/finite_machine.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require "logger"
|
3
4
|
require "thread"
|
4
5
|
require "sync"
|
5
6
|
|
@@ -13,6 +14,7 @@ require "finite_machine/async_call"
|
|
13
14
|
require "finite_machine/event"
|
14
15
|
require "finite_machine/event_queue"
|
15
16
|
require "finite_machine/hooks"
|
17
|
+
require "finite_machine/logger"
|
16
18
|
require "finite_machine/transition"
|
17
19
|
require "finite_machine/dsl"
|
18
20
|
require "finite_machine/state_machine"
|
@@ -56,11 +58,21 @@ module FiniteMachine
|
|
56
58
|
# Raised when event has no transitions
|
57
59
|
NotEnoughTransitionsError = Class.new(::ArgumentError)
|
58
60
|
|
61
|
+
# Raised when initial event specified without state name
|
62
|
+
MissingInitialStateError = Class.new(::StandardError)
|
63
|
+
|
59
64
|
Environment = Struct.new(:target)
|
60
65
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
66
|
+
class << self
|
67
|
+
attr_accessor :logger
|
68
|
+
|
69
|
+
# TODO: this should instantiate system not the state machine
|
70
|
+
# and then delegate calls to StateMachine instance etc...
|
71
|
+
def define(*args, &block)
|
72
|
+
StateMachine.new(*args, &block)
|
73
|
+
end
|
65
74
|
end
|
75
|
+
|
66
76
|
end # FiniteMachine
|
77
|
+
|
78
|
+
FiniteMachine.logger = Logger.new(STDERR)
|