finity 0.1.0 → 0.1.1
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.
- data/README.md +198 -59
- data/lib/finity/event.rb +3 -2
- data/lib/finity/version.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
# Finity
|
2
2
|
|
3
3
|
**Finity** tries to be an extremly lightweight state machine implementation
|
4
|
-
with an easily readable syntax
|
5
|
-
of
|
6
|
-
implementation tightly integrated with ActiveRecord by
|
7
|
-
Timo Rößner.
|
4
|
+
with an easily readable syntax. At the time of this writing, **Finity** is
|
5
|
+
comprised of only ~160 lines of code. It is inspired by [transitions][], a
|
6
|
+
great state machine implementation tightly integrated with ActiveRecord by
|
7
|
+
Jakub Kuźma and Timo Rößner.
|
8
8
|
|
9
|
-
The
|
10
|
-
slim and fast as possible while maintaining a beautiful and readable syntax.
|
11
|
-
you need ActiveModel/ActiveRecord integration,[transitions][]
|
12
|
-
|
13
|
-
optimized for readability and efficiency, give **Finity** a spin.
|
9
|
+
The aim of **Finity** is to provide a state machine implementation which is as
|
10
|
+
slim and fast as possible while maintaining a beautiful and readable syntax.
|
11
|
+
However, if you need ActiveModel/ActiveRecord integration, [transitions][] may
|
12
|
+
be your weapon of choice.
|
14
13
|
|
15
14
|
## Installation
|
16
15
|
|
@@ -21,93 +20,233 @@ command line:
|
|
21
20
|
gem 'finity'
|
22
21
|
```
|
23
22
|
|
24
|
-
|
23
|
+
Otherwise you can install **Finity** with `gem` via command
|
25
24
|
line:
|
26
25
|
|
27
26
|
```
|
28
27
|
gem install finity
|
29
28
|
```
|
30
29
|
|
31
|
-
##
|
30
|
+
## Example
|
32
31
|
|
33
|
-
**Finity** can transform any class into a state machine.
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
**Finity** can transform any class into a state machine. For example, consider
|
33
|
+
a state machine modelling an *elevator* in a building with 3 floors: `ground`,
|
34
|
+
`first` and `second`. The *elevator* can perform the following actions:
|
35
|
+
|
36
|
+
* On floors `ground` and `first`, it can go `up` to the floor above.
|
37
|
+
* On floors `first` and `second`, it can go `down` to the floor below.
|
38
|
+
|
39
|
+
*Leaving* (*entering*) a floor, the doors need to `close` (`open`). Also, when
|
40
|
+
entering a floor or pressing `down` (`up`) on the `ground` (`second`) floor, a
|
41
|
+
bell should `ring`. The following class models the *elevator*:
|
37
42
|
|
38
43
|
``` ruby
|
39
|
-
class
|
44
|
+
class Elevator
|
40
45
|
include Finity
|
41
46
|
|
42
|
-
finity :init => :
|
43
|
-
|
44
|
-
state :opened,
|
45
|
-
:enter => proc { @file = File.open '...' }
|
47
|
+
finity :init => :ground do
|
46
48
|
|
47
|
-
state :
|
48
|
-
:enter =>
|
49
|
+
state :ground,
|
50
|
+
:enter => :open,
|
51
|
+
:cycle => :ring,
|
52
|
+
:leave => :close
|
49
53
|
|
50
|
-
state :
|
51
|
-
:enter =>
|
54
|
+
state :first,
|
55
|
+
:enter => :open,
|
56
|
+
:leave => :close
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
state :second,
|
59
|
+
:enter => :open,
|
60
|
+
:cycle => :ring,
|
61
|
+
:leave => :close
|
57
62
|
|
58
|
-
|
59
|
-
|
63
|
+
event :up do
|
64
|
+
transitions :from => [:ground], :to => :first
|
65
|
+
transitions :from => [:first, :second], :to => :second
|
60
66
|
end
|
61
67
|
|
62
|
-
event :
|
63
|
-
transitions :from => [:
|
64
|
-
|
68
|
+
event :down do
|
69
|
+
transitions :from => [:ground, :first], :to => :ground
|
70
|
+
transitions :from => [:second], :to => :first
|
65
71
|
end
|
66
72
|
end
|
73
|
+
|
74
|
+
def up
|
75
|
+
event! :up
|
76
|
+
end
|
77
|
+
|
78
|
+
def down
|
79
|
+
event! :down
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def open
|
85
|
+
ring and puts "Doors opening on the #{@current} floor."
|
86
|
+
end
|
87
|
+
|
88
|
+
def ring
|
89
|
+
puts "Ring!"
|
90
|
+
end
|
91
|
+
|
92
|
+
def close
|
93
|
+
puts "Doors closing..."
|
94
|
+
end
|
67
95
|
end
|
68
96
|
```
|
69
97
|
|
70
|
-
|
98
|
+
While the different floors are modelled as *states*, the actions `up` and
|
99
|
+
`down` are modelled as *events*. The *elevator* is initialized on the `ground`
|
100
|
+
floor, which in this case is redundant, since, by default, **Finity** treats
|
101
|
+
the first state as the initial state. The instance variable `@current` holds
|
102
|
+
the current floor/state. The methods which are invoked upon entering, leaving
|
103
|
+
or cycling (staying in a state) are declared private, as they should not be
|
104
|
+
accessible from the outside. Only `up` and `down` are public.
|
105
|
+
|
106
|
+
We can now create an instance of the *elevator* and play with it:
|
107
|
+
|
108
|
+
``` ruby
|
109
|
+
elevator = Elevator.new
|
110
|
+
elevator.down # => Ring!
|
111
|
+
elevator.up # => Doors closing...
|
112
|
+
# => Ring!
|
113
|
+
# => Doors opening on the first floor.
|
114
|
+
elevator.up # => Doors closing...
|
115
|
+
# => Ring!
|
116
|
+
# => Doors opening on the second floor.
|
117
|
+
elevator.up # => Ring!
|
118
|
+
elevator.down # => Doors closing.
|
119
|
+
# => Ring!
|
120
|
+
# => Doors opening on the first floor.
|
121
|
+
```
|
122
|
+
|
123
|
+
While this example is very basic, it clearly shows the power of finite state
|
124
|
+
machines to model complex systems with a finite set of states and events
|
125
|
+
triggering transitions between them.
|
126
|
+
|
127
|
+
## States, Events and Transitions
|
128
|
+
|
129
|
+
### States
|
130
|
+
|
131
|
+
A state is uniquely identified by its name and *can* define functions to be
|
132
|
+
executed upon entering, leaving and cycling (staying inside). These functions
|
133
|
+
can be referenced as *Symbols*, *Strings*, *Procs* or *Lambdas*:
|
134
|
+
|
135
|
+
``` ruby
|
136
|
+
state :ground,
|
137
|
+
:enter => :open, # Symbols must reference (private) methods
|
138
|
+
:cycle => proc { ring }, # Procs are evaluated in the context of the instance
|
139
|
+
:leave => -> elevator { elevator.close } # Lambdas are provided with the instance as an argument
|
140
|
+
```
|
71
141
|
|
72
|
-
|
73
|
-
|
74
|
-
|
142
|
+
If there are several states with the same set of transition functions, they can
|
143
|
+
be defined in a single run. Considering our example, the `ground` and the
|
144
|
+
`second` floor bear the same set of actions, so we can combine them:
|
75
145
|
|
76
146
|
``` ruby
|
77
|
-
state :
|
78
|
-
:enter =>
|
79
|
-
:
|
147
|
+
state [:ground, :second],
|
148
|
+
:enter => :open,
|
149
|
+
:cycle => :ring,
|
150
|
+
:leave => :close
|
80
151
|
```
|
81
152
|
|
82
|
-
|
153
|
+
### Events and Transitions
|
83
154
|
|
84
|
-
Events are like states
|
85
|
-
|
86
|
-
in the order they are defined.
|
87
|
-
|
155
|
+
Events are like states identified by their name and may define an arbitrary
|
156
|
+
number of transitions between different states. The transitions are evaluated
|
157
|
+
in the order in which they are defined. A transition is executed, if it is found
|
158
|
+
to be valid, which means that it contains the current state in `from` and the
|
159
|
+
`if`-guard, if defined, returns `true`. If no valid transition is found for a
|
160
|
+
given event, **Finity** will raise an error. Starting with our example, the
|
161
|
+
minimal information needed for the `up`-event is:
|
88
162
|
|
89
163
|
``` ruby
|
90
|
-
event :
|
91
|
-
transitions :from => [:
|
92
|
-
|
93
|
-
|
164
|
+
event :up do
|
165
|
+
transitions :from => [:ground], :to => :first
|
166
|
+
transitions :from => [:first, :second], :to => :second
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
Like for states, multiple events can be defined in a single run with the same
|
171
|
+
set of transitions:
|
94
172
|
|
95
|
-
|
96
|
-
|
173
|
+
``` ruby
|
174
|
+
event [:up, :down] do
|
175
|
+
...
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
In case of an event, we sometimes want to take different actions from the same
|
180
|
+
state, so we need to specify *guards*. If a transition specifies a guard, it is
|
181
|
+
only considered valid if the guard returns `true`. For example, if we want to
|
182
|
+
deactivate the buttons when the elevator is `stuck`, we do the following:
|
183
|
+
|
184
|
+
``` ruby
|
185
|
+
event :up do
|
186
|
+
transitions :from => [:ground], :to => :first,
|
187
|
+
:if => :not_stuck?
|
188
|
+
transitions :from => [:first, :second], :to => :second,
|
189
|
+
:if => :not_stuck?
|
190
|
+
transitions :from => [:ground, :first, :second, :stuck], :to => :stuck
|
97
191
|
end
|
98
192
|
```
|
99
193
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
194
|
+
This implies, that we defined a new state called `stuck` and a method to
|
195
|
+
determine whether the elevator is stuck at the moment. Unless `not_stuck?`
|
196
|
+
returns `true`, the elevator will keep working as in our original example.
|
197
|
+
Otherwise, only the last transition is valid and the elevator will enter
|
198
|
+
the `stuck` state from any other state.
|
199
|
+
|
200
|
+
Additionally, we can define functions to be executed for specific transitions
|
201
|
+
only. This can be achieved with `do`:
|
105
202
|
|
106
203
|
``` ruby
|
107
|
-
|
108
|
-
|
109
|
-
|
204
|
+
event :up do
|
205
|
+
transitions :from => [:ground], :to => :first,
|
206
|
+
:if => :not_stuck?
|
207
|
+
transitions :from => [:first, :second], :to => :second,
|
208
|
+
:if => :not_stuck?
|
209
|
+
transitions :from => [:ground, :first, :second, :stuck], :to => :stuck,
|
210
|
+
:do => proc { puts "The elevator is stuck" }
|
110
211
|
end
|
111
212
|
```
|
112
213
|
|
214
|
+
Now, if the elevator is stuck, a message is displayed everytime a button is
|
215
|
+
pushed. Like for states, all functions can be defined as *Symbols*, *Strings*,
|
216
|
+
*Procs* or *Lambdas*.
|
217
|
+
|
218
|
+
### Definitions
|
219
|
+
|
220
|
+
**Finity** defines two methods on the including instance:
|
221
|
+
|
222
|
+
* `state? name`: Returns `true` if the state machine is in state `name`.
|
223
|
+
* `event! name`: Triggers event `name`.
|
224
|
+
|
225
|
+
Those methods can also be accessed from the outside. The current state is held
|
226
|
+
within the instance variable `@current`, contained in the including instance.
|
227
|
+
|
228
|
+
## License
|
229
|
+
|
230
|
+
Copyright (c) 2012 Martin Donath
|
231
|
+
|
232
|
+
Permission is hereby granted, free of charge, to any person
|
233
|
+
obtaining a copy of this software and associated documentation files
|
234
|
+
(the "Software"), to deal in the Software without restriction,
|
235
|
+
including without limitation the rights to use, copy, modify, merge,
|
236
|
+
publish, distribute, sublicense, and/or sell copies of the Software,
|
237
|
+
and to permit persons to whom the Software is furnished to do so,
|
238
|
+
subject to the following conditions:
|
239
|
+
|
240
|
+
The above copyright notice and this permission notice shall be
|
241
|
+
included in all copies or substantial portions of the Software.
|
242
|
+
|
243
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
244
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
245
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
246
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
247
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
248
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
249
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
250
|
+
SOFTWARE.
|
251
|
+
|
113
252
|
[transitions]: https://github.com/troessner/transitions
|
data/lib/finity/event.rb
CHANGED
@@ -42,11 +42,12 @@ module Finity
|
|
42
42
|
|
43
43
|
# Handle the current state and execute the first allowed transition.
|
44
44
|
def handle object, state
|
45
|
+
raise InvalidState, "No match for (:#{state.name}) on (:#{name})" unless
|
46
|
+
@transitions.key? state.name
|
45
47
|
@transitions[state.name].find do |transition|
|
46
48
|
name = transition.handle object
|
47
49
|
return name unless name.nil?
|
48
|
-
end
|
49
|
-
raise InvalidState, "No handler for :#{name} from :#{state.name}"
|
50
|
+
end
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
data/lib/finity/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: finity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-08-
|
12
|
+
date: 2012-08-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|