state_machines 0.1.1 → 0.1.2
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.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65309f4d9983b6dccdc1b0b166250118764346dd
|
4
|
+
data.tar.gz: 6a47d10426af28dc338a77b731396c06fab6c84a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10077a16141c29c4a6af5e8caf8a17b5e5548ab5138a70b7bb387970f4f8d6361a4f6a8ef8d9e1217aaa2cc783951ca52ec39b798e78077fe7ece8790bfcd19d
|
7
|
+
data.tar.gz: 457e4419ab4037322ce5b5d06e8dcbb6111a4874cfe4d5927dfd48948be2ed778ae5a83a1768dab3c15762402c1c8758a89ede19fdb061724533555bcea1c00c
|
data/README.md
CHANGED
@@ -20,7 +20,556 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
+
### Example
|
23
24
|
|
25
|
+
Below is an example of many of the features offered by this plugin, including:
|
26
|
+
|
27
|
+
* Initial states
|
28
|
+
* Namespaced states
|
29
|
+
* Transition callbacks
|
30
|
+
* Conditional transitions
|
31
|
+
* State-driven instance behavior
|
32
|
+
* Customized state values
|
33
|
+
* Parallel events
|
34
|
+
* Path analysis
|
35
|
+
|
36
|
+
Class definition:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class Vehicle
|
40
|
+
attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
|
41
|
+
|
42
|
+
state_machine state, initial: :parked do
|
43
|
+
before_transition parked: :any - :parked, do: :put_on_seatbelt
|
44
|
+
|
45
|
+
after_transition on: :crash, do: :tow
|
46
|
+
after_transition on: :repair, :do: :fix
|
47
|
+
after_transition any => :parked do |vehicle, transition|
|
48
|
+
vehicle.seatbelt_on = false
|
49
|
+
end
|
50
|
+
|
51
|
+
after_failure on: :ignite, do: :log_start_failure
|
52
|
+
|
53
|
+
around_transition do |vehicle, transition, block|
|
54
|
+
start = Time.now
|
55
|
+
block.call
|
56
|
+
vehicle.time_used += Time.now - start
|
57
|
+
end
|
58
|
+
|
59
|
+
event :park do
|
60
|
+
transition [:idling, :first_gear] => :parked
|
61
|
+
end
|
62
|
+
|
63
|
+
event :ignite do
|
64
|
+
transition stalled: same, parked: :idling
|
65
|
+
end
|
66
|
+
|
67
|
+
event :idle do
|
68
|
+
transition first_gear: :idling
|
69
|
+
end
|
70
|
+
|
71
|
+
event :shift_up do
|
72
|
+
transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear
|
73
|
+
end
|
74
|
+
|
75
|
+
event :shift_down do
|
76
|
+
transition third_gear: :second_gear, second_gear: :first_gear
|
77
|
+
end
|
78
|
+
|
79
|
+
event :crash do
|
80
|
+
transition all - [:parked, :stalled] => :stalled, if: ->(vehicle) {!vehicle.passed_inspection?}
|
81
|
+
end
|
82
|
+
|
83
|
+
event :repair do
|
84
|
+
# The first transition that matches the state and passes its conditions
|
85
|
+
# will be used
|
86
|
+
transition stalled: parked, unless: :auto_shop_busy
|
87
|
+
transition stalled: same
|
88
|
+
end
|
89
|
+
|
90
|
+
state :parked do
|
91
|
+
def speed
|
92
|
+
0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
state :idling, :first_gear do
|
97
|
+
def speed
|
98
|
+
10
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
state all - [:parked, :stalled, :idling] do
|
103
|
+
def moving?
|
104
|
+
true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
state :parked, :stalled, :idling do
|
109
|
+
def moving?
|
110
|
+
false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
state_machine :alarm_state, initial: :active, namespace: :'alarm' do
|
116
|
+
event :enable do
|
117
|
+
transition all => :active
|
118
|
+
end
|
119
|
+
|
120
|
+
event :disable do
|
121
|
+
transition all => :off
|
122
|
+
end
|
123
|
+
|
124
|
+
state :active, :value => 1
|
125
|
+
state :off, :value => 0
|
126
|
+
end
|
127
|
+
|
128
|
+
def initialize
|
129
|
+
@seatbelt_on = false
|
130
|
+
@time_used = 0
|
131
|
+
@auto_shop_busy = true
|
132
|
+
super() # NOTE: This *must* be called, otherwise states won't get initialized
|
133
|
+
end
|
134
|
+
|
135
|
+
def put_on_seatbelt
|
136
|
+
@seatbelt_on = true
|
137
|
+
end
|
138
|
+
|
139
|
+
def passed_inspection?
|
140
|
+
false
|
141
|
+
end
|
142
|
+
|
143
|
+
def tow
|
144
|
+
# tow the vehicle
|
145
|
+
end
|
146
|
+
|
147
|
+
def fix
|
148
|
+
# get the vehicle fixed by a mechanic
|
149
|
+
end
|
150
|
+
|
151
|
+
def log_start_failure
|
152
|
+
# log a failed attempt to start the vehicle
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
**Note** the comment made on the `initialize` method in the class. In order for
|
158
|
+
state machine attributes to be properly initialized, `super()` must be called.
|
159
|
+
See `StateMachines:MacroMethods` for more information about this.
|
160
|
+
|
161
|
+
Using the above class as an example, you can interact with the state machine
|
162
|
+
like so:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
|
166
|
+
vehicle.state # => "parked"
|
167
|
+
vehicle.state_name # => :parked
|
168
|
+
vehicle.human_state_name # => "parked"
|
169
|
+
vehicle.parked? # => true
|
170
|
+
vehicle.can_ignite? # => true
|
171
|
+
vehicle.ignite_transition # => #<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
172
|
+
vehicle.state_events # => [:ignite]
|
173
|
+
vehicle.state_transitions # => [#<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
174
|
+
vehicle.speed # => 0
|
175
|
+
vehicle.moving? # => false
|
176
|
+
|
177
|
+
vehicle.ignite # => true
|
178
|
+
vehicle.parked? # => false
|
179
|
+
vehicle.idling? # => true
|
180
|
+
vehicle.speed # => 10
|
181
|
+
vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
|
182
|
+
|
183
|
+
vehicle.shift_up # => true
|
184
|
+
vehicle.speed # => 10
|
185
|
+
vehicle.moving? # => true
|
186
|
+
vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
|
187
|
+
|
188
|
+
# A generic event helper is available to fire without going through the event's instance method
|
189
|
+
vehicle.fire_state_event(:shift_up) # => true
|
190
|
+
|
191
|
+
# Call state-driven behavior that's undefined for the state raises a NoMethodError
|
192
|
+
vehicle.speed # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
|
193
|
+
vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
|
194
|
+
|
195
|
+
# The bang (!) operator can raise exceptions if the event fails
|
196
|
+
vehicle.park! # => StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear
|
197
|
+
|
198
|
+
# Generic state predicates can raise exceptions if the value does not exist
|
199
|
+
vehicle.state?(:parked) # => false
|
200
|
+
vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
|
201
|
+
|
202
|
+
# Namespaced machines have uniquely-generated methods
|
203
|
+
vehicle.alarm_state # => 1
|
204
|
+
vehicle.alarm_state_name # => :active
|
205
|
+
|
206
|
+
vehicle.can_disable_alarm? # => true
|
207
|
+
vehicle.disable_alarm # => true
|
208
|
+
vehicle.alarm_state # => 0
|
209
|
+
vehicle.alarm_state_name # => :off
|
210
|
+
vehicle.can_enable_alarm? # => true
|
211
|
+
|
212
|
+
vehicle.alarm_off? # => true
|
213
|
+
vehicle.alarm_active? # => false
|
214
|
+
|
215
|
+
# Events can be fired in parallel
|
216
|
+
vehicle.fire_events(:shift_down, :enable_alarm) # => true
|
217
|
+
vehicle.state_name # => :first_gear
|
218
|
+
vehicle.alarm_state_name # => :active
|
219
|
+
|
220
|
+
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
|
221
|
+
|
222
|
+
# Human-friendly names can be accessed for states/events
|
223
|
+
Vehicle.human_state_name(:first_gear) # => "first gear"
|
224
|
+
Vehicle.human_alarm_state_name(:active) # => "active"
|
225
|
+
|
226
|
+
Vehicle.human_state_event_name(:shift_down) # => "shift down"
|
227
|
+
Vehicle.human_alarm_state_event_name(:enable) # => "enable"
|
228
|
+
|
229
|
+
# States / events can also be references by the string version of their name
|
230
|
+
Vehicle.human_state_name('first_gear') # => "first gear"
|
231
|
+
Vehicle.human_state_event_name('shift_down') # => "shift down"
|
232
|
+
|
233
|
+
# Available transition paths can be analyzed for an object
|
234
|
+
vehicle.state_paths # => [[#<StateMachines:Transition ...], [#<StateMachines:Transition ...], ...]
|
235
|
+
vehicle.state_paths.to_states # => [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
|
236
|
+
vehicle.state_paths.events # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]
|
237
|
+
|
238
|
+
# Find all paths that start and end on certain states
|
239
|
+
vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
|
240
|
+
# #<StateMachines:Transition attribute=:state event=:ignite from="parked" ...>,
|
241
|
+
# #<StateMachines:Transition attribute=:state event=:shift_up from="idling" ...>
|
242
|
+
# ]]
|
243
|
+
# Skipping state_machine and writing to attributes directly
|
244
|
+
vehicle.state = "parked"
|
245
|
+
vehicle.state # => "parked"
|
246
|
+
vehicle.state_name # => :parked
|
247
|
+
|
248
|
+
# *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine):
|
249
|
+
# vehicle.state = :parked
|
250
|
+
```
|
251
|
+
|
252
|
+
## Additional Topics
|
253
|
+
|
254
|
+
### Explicit vs. Implicit Event Transitions
|
255
|
+
|
256
|
+
Every event defined for a state machine generates an instance method on the
|
257
|
+
class that allows the event to be explicitly triggered. Most of the examples in
|
258
|
+
the state_machine documentation use this technique. However, with some types of
|
259
|
+
integrations, like ActiveRecord, you can also *implicitly* fire events by
|
260
|
+
setting a special attribute on the instance.
|
261
|
+
|
262
|
+
Suppose you're using the ActiveRecord integration and the following model is
|
263
|
+
defined:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class Vehicle < ActiveRecord::Base
|
267
|
+
state_machine initial: :parked do
|
268
|
+
event :ignite do
|
269
|
+
transition parked: :idling
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
To trigger the `ignite` event, you would typically call the `Vehicle#ignite`
|
276
|
+
method like so:
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
|
280
|
+
vehicle.ignite # => true
|
281
|
+
vehicle.state # => "idling"
|
282
|
+
```
|
283
|
+
|
284
|
+
This is referred to as an *explicit* event transition. The same behavior can
|
285
|
+
also be achieved *implicitly* by setting the state event attribute and invoking
|
286
|
+
the action associated with the state machine. For example:
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
|
290
|
+
vehicle.state_event = 'ignite' # => 'ignite'
|
291
|
+
vehicle.save # => true
|
292
|
+
vehicle.state # => 'idling'
|
293
|
+
vehicle.state_event # => nil
|
294
|
+
```
|
295
|
+
|
296
|
+
As you can see, the `ignite` event was automatically triggered when the `save`
|
297
|
+
action was called. This is particularly useful if you want to allow users to
|
298
|
+
drive the state transitions from a web API.
|
299
|
+
|
300
|
+
See each integration's API documentation for more information on the implicit
|
301
|
+
approach.
|
302
|
+
|
303
|
+
### Symbols vs. Strings
|
304
|
+
|
305
|
+
In all of the examples used throughout the documentation, you'll notice that
|
306
|
+
states and events are almost always referenced as symbols. This isn't a
|
307
|
+
requirement, but rather a suggested best practice.
|
308
|
+
|
309
|
+
You can very well define your state machine with Strings like so:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
class Vehicle
|
313
|
+
state_machine initial: 'parked' do
|
314
|
+
event 'ignite' do
|
315
|
+
transition 'parked' => 'idling'
|
316
|
+
end
|
317
|
+
|
318
|
+
# ...
|
319
|
+
end
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
You could even use numbers as your state / event names. The **important** thing
|
324
|
+
to keep in mind is that the type being used for referencing states / events in
|
325
|
+
your machine definition must be **consistent**. If you're using Symbols, then
|
326
|
+
all states / events must use Symbols. Otherwise you'll encounter the following
|
327
|
+
error:
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
class Vehicle
|
331
|
+
state_machine do
|
332
|
+
event :ignite do
|
333
|
+
transition parked: 'idling'
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent
|
339
|
+
```
|
340
|
+
|
341
|
+
There **is** an exception to this rule. The consistency is only required within
|
342
|
+
the definition itself. However, when the machine's helper methods are called
|
343
|
+
with input from external sources, such as a web form, state_machine will map
|
344
|
+
that input to a String / Symbol. For example:
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
class Vehicle
|
348
|
+
state_machine initial: :parked do
|
349
|
+
event :ignite do
|
350
|
+
transition parked: :idling
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state="parked">
|
356
|
+
v.state?('parked') # => true
|
357
|
+
v.state?(:parked) # => true
|
358
|
+
```
|
359
|
+
|
360
|
+
**Note** that none of this actually has to do with the type of the value that
|
361
|
+
gets stored. By default, all state values are assumed to be string -- regardless
|
362
|
+
of whether the state names are symbols or strings. If you want to store states
|
363
|
+
as symbols instead you'll have to be explicit about it:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class Vehicle
|
367
|
+
state_machine initial: :parked do
|
368
|
+
event :ignite do
|
369
|
+
transition parked: :idling
|
370
|
+
end
|
371
|
+
|
372
|
+
states.each do |state|
|
373
|
+
self.state(state.name, :value => state.name.to_sym)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state=:parked>
|
379
|
+
v.state?('parked') # => true
|
380
|
+
v.state?(:parked) # => true
|
381
|
+
```
|
382
|
+
|
383
|
+
### Syntax flexibility
|
384
|
+
|
385
|
+
Although state_machine introduces a simplified syntax, it still remains
|
386
|
+
backwards compatible with previous versions and other state-related libraries by
|
387
|
+
providing some flexibility around how transitions are defined. See below for an
|
388
|
+
overview of these syntaxes.
|
389
|
+
|
390
|
+
#### Verbose syntax
|
391
|
+
|
392
|
+
In general, it's recommended that state machines use the implicit syntax for
|
393
|
+
transitions. However, you can be a little more explicit and verbose about
|
394
|
+
transitions by using the `:from`, `:except_from`, `:to`,
|
395
|
+
and `:except_to` options.
|
396
|
+
|
397
|
+
For example, transitions and callbacks can be defined like so:
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
class Vehicle
|
401
|
+
state_machine initial: :parked do
|
402
|
+
before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
|
403
|
+
after_transition to: :parked do |transition|
|
404
|
+
self.seatbelt = 'off' # self is the record
|
405
|
+
end
|
406
|
+
|
407
|
+
event :ignite do
|
408
|
+
transition from: :parked, to: :idling
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
#### Transition context
|
415
|
+
|
416
|
+
Some flexibility is provided around the context in which transitions can be
|
417
|
+
defined. In almost all examples throughout the documentation, transitions are
|
418
|
+
defined within the context of an event. If you prefer to have state machines
|
419
|
+
defined in the context of a **state** either out of preference or in order to
|
420
|
+
easily migrate from a different library, you can do so as shown below:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
class Vehicle
|
424
|
+
state_machine initial: :parked do
|
425
|
+
...
|
426
|
+
|
427
|
+
state :parked do
|
428
|
+
transition to::idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
|
429
|
+
|
430
|
+
def speed
|
431
|
+
0
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
state :first_gear do
|
436
|
+
transition to: :second_gear, on: :shift_up
|
437
|
+
|
438
|
+
def speed
|
439
|
+
10
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
state :idling, :first_gear do
|
444
|
+
transition to: :parked, on: :park
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
```
|
449
|
+
|
450
|
+
In the above example, there's no need to specify the `from` state for each
|
451
|
+
transition since it's inferred from the context.
|
452
|
+
|
453
|
+
You can also define transitions completely outside the context of a particular
|
454
|
+
state / event. This may be useful in cases where you're building a state
|
455
|
+
machine from a data store instead of part of the class definition. See the
|
456
|
+
example below:
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
class Vehicle
|
460
|
+
state_machine initial: :parked do
|
461
|
+
...
|
462
|
+
|
463
|
+
transition parked: :idling, :on => [:ignite, :shift_up]
|
464
|
+
transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
|
465
|
+
transition [:idling, :first_gear] => :parked, on: :park
|
466
|
+
transition [:idling, :first_gear] => :parked, on: :park
|
467
|
+
transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy?
|
468
|
+
end
|
469
|
+
end
|
470
|
+
```
|
471
|
+
|
472
|
+
Notice that in these alternative syntaxes:
|
473
|
+
|
474
|
+
* You can continue to configure `:if` and `:unless` conditions
|
475
|
+
* You can continue to define `from` states (when in the machine context) using
|
476
|
+
the `all`, `any`, and `same` helper methods
|
477
|
+
|
478
|
+
### Static / Dynamic definitions
|
479
|
+
|
480
|
+
In most cases, the definition of a state machine is **static**. That is to say,
|
481
|
+
the states, events and possible transitions are known ahead of time even though
|
482
|
+
they may depend on data that's only known at runtime. For example, certain
|
483
|
+
transitions may only be available depending on an attribute on that object it's
|
484
|
+
being run on. All of the documentation in this library define static machines
|
485
|
+
like so:
|
486
|
+
|
487
|
+
```ruby
|
488
|
+
class Vehicle
|
489
|
+
state_machine :state, initial: :parked do
|
490
|
+
event :park do
|
491
|
+
transition [:idling, :first_gear] => :parked
|
492
|
+
end
|
493
|
+
|
494
|
+
...
|
495
|
+
end
|
496
|
+
end
|
497
|
+
```
|
498
|
+
|
499
|
+
However, there may be cases where the definition of a state machine is **dynamic**.
|
500
|
+
This means that you don't know the possible states or events for a machine until
|
501
|
+
runtime. For example, you may allow users in your application to manage the
|
502
|
+
state machine of a project or task in your system. This means that the list of
|
503
|
+
transitions (and their associated states / events) could be stored externally,
|
504
|
+
such as in a database. In a case like this, you can define dynamically-generated
|
505
|
+
state machines like so:
|
506
|
+
|
507
|
+
```ruby
|
508
|
+
class Vehicle
|
509
|
+
attr_accessor :state
|
510
|
+
|
511
|
+
# Make sure the machine gets initialized so the initial state gets set properly
|
512
|
+
def initialize(*)
|
513
|
+
super
|
514
|
+
machine
|
515
|
+
end
|
516
|
+
|
517
|
+
# Replace this with an external source (like a db)
|
518
|
+
def transitions
|
519
|
+
[
|
520
|
+
{parked: :idling, on: :ignite},
|
521
|
+
{idling: :first_gear, first_gear: :second_gear, on: :shift_up}
|
522
|
+
# ...
|
523
|
+
]
|
524
|
+
end
|
525
|
+
|
526
|
+
# Create a state machine for this vehicle instance dynamically based on the
|
527
|
+
# transitions defined from the source above
|
528
|
+
def machine
|
529
|
+
vehicle = self
|
530
|
+
@machine ||= Machine.new(vehicle, initial: :parked, action: :save) do
|
531
|
+
vehicle.transitions.each {|attrs| transition(attrs)}
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def save
|
536
|
+
# Save the state change...
|
537
|
+
true
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
# Generic class for building machines
|
542
|
+
class Machine
|
543
|
+
def self.new(object, *args, &block)
|
544
|
+
machine_class = Class.new
|
545
|
+
machine = machine_class.state_machine(*args, &block)
|
546
|
+
attribute = machine.attribute
|
547
|
+
action = machine.action
|
548
|
+
|
549
|
+
# Delegate attributes
|
550
|
+
machine_class.class_eval do
|
551
|
+
define_method(:definition) { machine }
|
552
|
+
define_method(attribute) { object.send(attribute) }
|
553
|
+
define_method("#{attribute}=") {|value| object.send("#{attribute}=", value) }
|
554
|
+
define_method(action) { object.send(action) } if action
|
555
|
+
end
|
556
|
+
|
557
|
+
machine_class.new
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
vehicle = Vehicle.new # => #<Vehicle:0xb708412c @state="parked" ...>
|
562
|
+
vehicle.state # => "parked"
|
563
|
+
vehicle.machine.ignite # => true
|
564
|
+
vehicle.machine.state # => "idling
|
565
|
+
vehicle.state # => "idling"
|
566
|
+
vehicle.machine.state_transitions # => [#<StateMachines:Transition ...>]
|
567
|
+
vehicle.machine.definition.states.keys # => :first_gear, :second_gear, :parked, :idling
|
568
|
+
```
|
569
|
+
|
570
|
+
As you can see, state_machine provides enough flexibility for you to be able
|
571
|
+
to create new machine definitions on the fly based on an external source of
|
572
|
+
transitions.
|
24
573
|
|
25
574
|
## Dependencies
|
26
575
|
|
@@ -41,8 +590,8 @@ For documenting state machines:
|
|
41
590
|
|
42
591
|
## TODO
|
43
592
|
|
44
|
-
Add matchers/assertions for rspec and minitest
|
45
|
-
Fix
|
593
|
+
* Add matchers/assertions for rspec and minitest
|
594
|
+
* Fix integrations dependency
|
46
595
|
|
47
596
|
## Contributing
|
48
597
|
|
data/lib/state_machines/error.rb
CHANGED
@@ -14,8 +14,27 @@ module StateMachines
|
|
14
14
|
# An invalid integration was specified
|
15
15
|
class IntegrationNotFound < Error
|
16
16
|
def initialize(name)
|
17
|
-
|
18
|
-
|
17
|
+
super(nil, "#{name.inspect} is an invalid integration. #{error_message}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_integrations
|
21
|
+
"Valid integrations are: #{valid_integrations_name}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_integrations_name
|
25
|
+
Integrations.list.collect(&:integration_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def no_integrations
|
29
|
+
'No integrations registered'
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_message
|
33
|
+
if Integrations.list.size.zero?
|
34
|
+
no_integrations
|
35
|
+
else
|
36
|
+
valid_integrations
|
37
|
+
end
|
19
38
|
end
|
20
39
|
end
|
21
40
|
|
@@ -40,8 +40,8 @@ module StateMachines
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def reset #:nodoc:#
|
43
|
-
name_spaced_integrations
|
44
43
|
@integrations = Set.new
|
44
|
+
name_spaced_integrations
|
45
45
|
true
|
46
46
|
end
|
47
47
|
|
@@ -59,6 +59,7 @@ module StateMachines
|
|
59
59
|
name_spaced_integrations
|
60
60
|
@integrations
|
61
61
|
end
|
62
|
+
|
62
63
|
alias_method :list, :integrations
|
63
64
|
|
64
65
|
|
@@ -115,7 +116,7 @@ module StateMachines
|
|
115
116
|
|
116
117
|
def name_spaced_integrations
|
117
118
|
# FIXME, Integrations should be add before their dependencies.
|
118
|
-
self.constants.
|
119
|
+
self.constants.reject{ |i| i==:Base }.each do |const|
|
119
120
|
integration = self.const_get(const)
|
120
121
|
add(integration)
|
121
122
|
end
|
@@ -17,13 +17,6 @@ module StateMachines
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
# Whether this integration is available for the current library. This
|
21
|
-
# is only true if the ORM that the integration is for is currently
|
22
|
-
# defined.
|
23
|
-
def available?
|
24
|
-
matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
|
25
|
-
end
|
26
|
-
|
27
20
|
# The list of ancestor names that cause this integration to matched.
|
28
21
|
def matching_ancestors
|
29
22
|
[]
|
@@ -5,12 +5,12 @@ class IntegrationFinderTest < StateMachinesTest
|
|
5
5
|
StateMachines::Integrations.reset
|
6
6
|
end
|
7
7
|
|
8
|
-
def test_should_find_base
|
9
|
-
assert_equal StateMachines::Integrations::Base, StateMachines::Integrations.find_by_name(:base)
|
10
|
-
end
|
11
|
-
|
12
8
|
def test_should_raise_an_exception_if_invalid
|
13
9
|
exception = assert_raises(StateMachines::IntegrationNotFound) { StateMachines::Integrations.find_by_name(:invalid) }
|
14
|
-
assert_equal ':invalid is an invalid integration.
|
10
|
+
assert_equal ':invalid is an invalid integration. No integrations registered', exception.message
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_should_have_no_integrations
|
14
|
+
assert_equal(Set.new, StateMachines::Integrations.list)
|
15
15
|
end
|
16
16
|
end
|