state_machine 0.4.1 → 0.4.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.
- data/CHANGELOG.rdoc +8 -0
- data/README.rdoc +55 -1
- data/Rakefile +1 -1
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/lib/state_machine.rb +146 -9
- data/lib/state_machine/assertions.rb +1 -1
- data/lib/state_machine/callback.rb +3 -2
- data/lib/state_machine/event.rb +21 -10
- data/lib/state_machine/extensions.rb +14 -56
- data/lib/state_machine/guard.rb +36 -1
- data/lib/state_machine/integrations/active_record.rb +2 -1
- data/lib/state_machine/integrations/data_mapper/observer.rb +14 -0
- data/lib/state_machine/machine.rb +215 -108
- data/lib/state_machine/state.rb +175 -0
- data/test/active_record.log +35152 -0
- data/test/data_mapper.log +9296 -0
- data/test/functional/state_machine_test.rb +106 -0
- data/test/sequel.log +5208 -0
- data/test/unit/event_test.rb +35 -0
- data/test/unit/guard_test.rb +140 -0
- data/test/unit/integrations/active_record_test.rb +34 -1
- data/test/unit/machine_test.rb +273 -72
- data/test/unit/state_test.rb +583 -0
- metadata +7 -3
- data/examples/Car_state.jpg +0 -0
data/lib/state_machine/guard.rb
CHANGED
|
@@ -15,7 +15,7 @@ module StateMachine
|
|
|
15
15
|
attr_reader :requirements
|
|
16
16
|
|
|
17
17
|
# A list of all of the states known to this guard. This will pull state
|
|
18
|
-
#
|
|
18
|
+
# values from the following requirements:
|
|
19
19
|
# * +to+
|
|
20
20
|
# * +from+
|
|
21
21
|
# * +except_to+
|
|
@@ -75,6 +75,41 @@ module StateMachine
|
|
|
75
75
|
matches_query?(object, query) && matches_conditions?(object)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Draws a representation of this guard on the given graph. This will draw
|
|
79
|
+
# an edge between every state this guard matches *from* to either the
|
|
80
|
+
# configured to state or, if none specified, then a loopback to the from
|
|
81
|
+
# state.
|
|
82
|
+
#
|
|
83
|
+
# For example, if the following from states are configured:
|
|
84
|
+
# * +first_gear+
|
|
85
|
+
# * +idling+
|
|
86
|
+
# * +backing_up+
|
|
87
|
+
#
|
|
88
|
+
# ...and the to state is "parked", then the following edges will be created:
|
|
89
|
+
# * +first_gear+ -> +parked+
|
|
90
|
+
# * +idling+ -> +parked+
|
|
91
|
+
# * +backing_up+ -> +parked+
|
|
92
|
+
#
|
|
93
|
+
# Each edge will be labeled with the name of the event that would cause the
|
|
94
|
+
# transition.
|
|
95
|
+
#
|
|
96
|
+
# The collection of edges generated on the graph will be returned.
|
|
97
|
+
def draw(graph, event_name, valid_states)
|
|
98
|
+
# From states: :from, everything but :except states, or all states
|
|
99
|
+
from_states = requirements[:from] || requirements[:except_from] && (valid_states - requirements[:except_from]) || valid_states
|
|
100
|
+
|
|
101
|
+
# To state can be optional, otherwise it's a loopback
|
|
102
|
+
if to_state = requirements[:to]
|
|
103
|
+
to_state = State.id_for(to_state.first)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Generate an edge between each from and to state
|
|
107
|
+
from_states.collect do |from_state|
|
|
108
|
+
from_state = State.id_for(from_state)
|
|
109
|
+
graph.add_edge(from_state, to_state || from_state, :label => event_name)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
78
113
|
protected
|
|
79
114
|
# Verify that the from state, to state, and event match the query
|
|
80
115
|
def matches_query?(object, query)
|
|
@@ -249,7 +249,8 @@ module StateMachine
|
|
|
249
249
|
# This will always return true regardless of the results of the
|
|
250
250
|
# callbacks.
|
|
251
251
|
def notify(type, object, transition)
|
|
252
|
-
|
|
252
|
+
qualified_event = namespace ? "#{transition.event}_#{namespace}" : transition.event
|
|
253
|
+
["#{type}_#{qualified_event}", "#{type}_transition"].each do |method|
|
|
253
254
|
object.class.class_eval do
|
|
254
255
|
@observer_peers.dup.each do |observer|
|
|
255
256
|
observer.send(method, object, transition) if observer.respond_to?(method)
|
|
@@ -55,10 +55,17 @@ module StateMachine
|
|
|
55
55
|
# # log message
|
|
56
56
|
# end
|
|
57
57
|
#
|
|
58
|
+
# # Target all state machines
|
|
58
59
|
# before_transition :to => 'idling', :from => 'parked', :on => 'ignite' do
|
|
59
60
|
# # put on seatbelt
|
|
60
61
|
# end
|
|
61
62
|
#
|
|
63
|
+
# # Target a specific state machine
|
|
64
|
+
# before_transition :state, :to => 'idling' do
|
|
65
|
+
# # put on seatbelt
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# # Target all state machines without requirements
|
|
62
69
|
# before_transition do |transition|
|
|
63
70
|
# # log message
|
|
64
71
|
# end
|
|
@@ -104,10 +111,17 @@ module StateMachine
|
|
|
104
111
|
# # log message
|
|
105
112
|
# end
|
|
106
113
|
#
|
|
114
|
+
# # Target all state machines
|
|
107
115
|
# after_transition :to => 'idling', :from => 'parked', :on => 'ignite' do
|
|
108
116
|
# # put on seatbelt
|
|
109
117
|
# end
|
|
110
118
|
#
|
|
119
|
+
# # Target a specific state machine
|
|
120
|
+
# after_transition :state, :to => 'idling' do
|
|
121
|
+
# # put on seatbelt
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# # Target all state machines without requirements
|
|
111
125
|
# after_transition do |transition, saved|
|
|
112
126
|
# if saved
|
|
113
127
|
# # log message
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'state_machine/extensions'
|
|
2
|
+
require 'state_machine/state'
|
|
2
3
|
require 'state_machine/event'
|
|
3
4
|
require 'state_machine/callback'
|
|
4
5
|
require 'state_machine/assertions'
|
|
@@ -10,7 +11,7 @@ end
|
|
|
10
11
|
|
|
11
12
|
module StateMachine
|
|
12
13
|
# Represents a state machine for a particular attribute. State machines
|
|
13
|
-
# consist of events and a set of transitions that define how the state
|
|
14
|
+
# consist of states, events and a set of transitions that define how the state
|
|
14
15
|
# changes after a particular event is fired.
|
|
15
16
|
#
|
|
16
17
|
# A state machine may not necessarily know all of the possible states for
|
|
@@ -95,7 +96,7 @@ module StateMachine
|
|
|
95
96
|
#
|
|
96
97
|
# vehicle = Vehicle.new
|
|
97
98
|
# vehicle.park # => false
|
|
98
|
-
# vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "idling"
|
|
99
|
+
# vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
|
|
99
100
|
#
|
|
100
101
|
# == Observers
|
|
101
102
|
#
|
|
@@ -107,8 +108,8 @@ module StateMachine
|
|
|
107
108
|
#
|
|
108
109
|
# class Vehicle
|
|
109
110
|
# state_machine do
|
|
110
|
-
# event :
|
|
111
|
-
# transition :to => '
|
|
111
|
+
# event :park do
|
|
112
|
+
# transition :to => 'parked', :from => 'idling'
|
|
112
113
|
# end
|
|
113
114
|
# ...
|
|
114
115
|
# end
|
|
@@ -183,32 +184,6 @@ module StateMachine
|
|
|
183
184
|
class Machine
|
|
184
185
|
include Assertions
|
|
185
186
|
|
|
186
|
-
# The class that the machine is defined in
|
|
187
|
-
attr_reader :owner_class
|
|
188
|
-
|
|
189
|
-
# The attribute for which the machine is being defined
|
|
190
|
-
attr_reader :attribute
|
|
191
|
-
|
|
192
|
-
# The initial state that the machine will be in when an object is created
|
|
193
|
-
attr_reader :initial_state
|
|
194
|
-
|
|
195
|
-
# The events that trigger transitions
|
|
196
|
-
attr_reader :events
|
|
197
|
-
|
|
198
|
-
# A list of all of the states known to this state machine. This will pull
|
|
199
|
-
# state names from the following sources:
|
|
200
|
-
# * Initial state
|
|
201
|
-
# * Event transitions (:to, :from, :except_to, and :except_from options)
|
|
202
|
-
# * Transition callbacks (:to, :from, :except_to, and :except_from options)
|
|
203
|
-
# * Unreferenced states (using +other_states+ helper)
|
|
204
|
-
attr_reader :states
|
|
205
|
-
|
|
206
|
-
# The callbacks to invoke before/after a transition is performed
|
|
207
|
-
attr_reader :callbacks
|
|
208
|
-
|
|
209
|
-
# The action to invoke when an object transitions
|
|
210
|
-
attr_reader :action
|
|
211
|
-
|
|
212
187
|
class << self
|
|
213
188
|
# Attempts to find or create a state machine for the given class. For
|
|
214
189
|
# example,
|
|
@@ -263,24 +238,66 @@ module StateMachine
|
|
|
263
238
|
end
|
|
264
239
|
|
|
265
240
|
# Draw each of the class's state machines
|
|
266
|
-
klass.state_machines.
|
|
241
|
+
klass.state_machines.each do |name, machine|
|
|
267
242
|
machine.draw(options)
|
|
268
243
|
end
|
|
269
244
|
end
|
|
270
245
|
end
|
|
271
246
|
end
|
|
272
247
|
|
|
248
|
+
# The class that the machine is defined in
|
|
249
|
+
attr_reader :owner_class
|
|
250
|
+
|
|
251
|
+
# The attribute for which the machine is being defined
|
|
252
|
+
attr_reader :attribute
|
|
253
|
+
|
|
254
|
+
# The initial state that the machine will be in when an object is created
|
|
255
|
+
attr_reader :initial_state
|
|
256
|
+
|
|
257
|
+
# The events that trigger transitions
|
|
258
|
+
#
|
|
259
|
+
# Maps "name" => StateMachine::Event
|
|
260
|
+
attr_reader :events
|
|
261
|
+
|
|
262
|
+
# Tracks the order in which events were defined. This is used to determine
|
|
263
|
+
# in what order events are drawn on GraphViz visualizations.
|
|
264
|
+
attr_reader :events_order
|
|
265
|
+
|
|
266
|
+
# A list of all of the states known to this state machine. This will pull
|
|
267
|
+
# state values from the following sources:
|
|
268
|
+
# * Initial state
|
|
269
|
+
# * State behaviors
|
|
270
|
+
# * Event transitions (:to, :from, :except_to, and :except_from options)
|
|
271
|
+
# * Transition callbacks (:to, :from, :except_to, and :except_from options)
|
|
272
|
+
# * Unreferenced states (using +other_states+ helper)
|
|
273
|
+
#
|
|
274
|
+
# Maps value => StateMachine::State
|
|
275
|
+
attr_reader :states
|
|
276
|
+
|
|
277
|
+
# The callbacks to invoke before/after a transition is performed
|
|
278
|
+
attr_reader :callbacks
|
|
279
|
+
|
|
280
|
+
# The action to invoke when an object transitions
|
|
281
|
+
attr_reader :action
|
|
282
|
+
|
|
283
|
+
# An identifier that forces all methods (including state predicates and
|
|
284
|
+
# event methods) to be generated with the value prefixed or suffixed,
|
|
285
|
+
# depending on the context.
|
|
286
|
+
attr_reader :namespace
|
|
287
|
+
|
|
273
288
|
# Creates a new state machine for the given attribute
|
|
274
289
|
def initialize(owner_class, *args, &block)
|
|
275
290
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
276
|
-
assert_valid_keys(options, :initial, :action, :plural, :integration)
|
|
291
|
+
assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration)
|
|
277
292
|
|
|
278
293
|
# Set machine configuration
|
|
279
294
|
@attribute = (args.first || 'state').to_s
|
|
280
295
|
@events = {}
|
|
281
|
-
@
|
|
296
|
+
@events_order = []
|
|
297
|
+
@states = {}
|
|
282
298
|
@callbacks = {:before => [], :after => []}
|
|
283
299
|
@action = options[:action]
|
|
300
|
+
@namespace = options[:namespace]
|
|
284
301
|
|
|
285
302
|
# Add class-/instance-level methods to the owner class for state initialization
|
|
286
303
|
owner_class.class_eval do
|
|
@@ -315,7 +332,14 @@ module StateMachine
|
|
|
315
332
|
events[name] = event
|
|
316
333
|
events
|
|
317
334
|
end
|
|
318
|
-
@
|
|
335
|
+
@events_order = @events_order.dup
|
|
336
|
+
@states = @states.inject({}) do |states, (value, state)|
|
|
337
|
+
state = state.dup
|
|
338
|
+
state.machine = self
|
|
339
|
+
states[value] = state
|
|
340
|
+
states
|
|
341
|
+
end
|
|
342
|
+
@initial_state = @states[@initial_state.value]
|
|
319
343
|
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
|
|
320
344
|
end
|
|
321
345
|
|
|
@@ -340,8 +364,8 @@ module StateMachine
|
|
|
340
364
|
assert_valid_keys(options, :initial, :integration)
|
|
341
365
|
|
|
342
366
|
@owner_class = owner_class
|
|
343
|
-
@initial_state = options[:initial] if options
|
|
344
|
-
|
|
367
|
+
@initial_state = add_states([options[:initial]]).first if options.include?(:initial) || !@initial_state
|
|
368
|
+
states.each {|name, state| state.initial = (state == @initial_state)}
|
|
345
369
|
|
|
346
370
|
# Find an integration that can be used for implementing various parts
|
|
347
371
|
# of the state machine that may behave differently in different libraries
|
|
@@ -391,7 +415,99 @@ module StateMachine
|
|
|
391
415
|
# vehicle.force_idle = false
|
|
392
416
|
# Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
|
|
393
417
|
def initial_state(object)
|
|
394
|
-
@initial_state
|
|
418
|
+
@initial_state && @initial_state.value(object)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Defines a series of behaviors to mixin with objects when the current
|
|
422
|
+
# state matches the given one(s). This allows instance methods to behave
|
|
423
|
+
# a specific way depending on what the value of the object's state is.
|
|
424
|
+
#
|
|
425
|
+
# For example,
|
|
426
|
+
#
|
|
427
|
+
# class Vehicle
|
|
428
|
+
# attr_accessor :driver
|
|
429
|
+
# attr_accessor :passenger
|
|
430
|
+
#
|
|
431
|
+
# state_machine :initial => 'parked' do
|
|
432
|
+
# event :ignite do
|
|
433
|
+
# transition :to => 'idling', :from => 'parked'
|
|
434
|
+
# end
|
|
435
|
+
#
|
|
436
|
+
# state 'parked' do
|
|
437
|
+
# def speed
|
|
438
|
+
# 0
|
|
439
|
+
# end
|
|
440
|
+
#
|
|
441
|
+
# def rotate_driver
|
|
442
|
+
# driver = self.driver
|
|
443
|
+
# self.driver = passenger
|
|
444
|
+
# self.passenger = driver
|
|
445
|
+
# true
|
|
446
|
+
# end
|
|
447
|
+
# end
|
|
448
|
+
#
|
|
449
|
+
# state 'idling', 'first_gear' do
|
|
450
|
+
# def speed
|
|
451
|
+
# 20
|
|
452
|
+
# end
|
|
453
|
+
#
|
|
454
|
+
# def rotate_driver
|
|
455
|
+
# self.state = "parked"
|
|
456
|
+
# rotate_driver
|
|
457
|
+
# end
|
|
458
|
+
# end
|
|
459
|
+
# end
|
|
460
|
+
# end
|
|
461
|
+
#
|
|
462
|
+
# In the above example, there are two dynamic behaviors defined for the
|
|
463
|
+
# class:
|
|
464
|
+
# * +speed+
|
|
465
|
+
# * +rotate_driver+
|
|
466
|
+
#
|
|
467
|
+
# Each of these behaviors are instance methods on the Vehicle class. However,
|
|
468
|
+
# which method actually gets invoked is based on the current state of the
|
|
469
|
+
# object. Using the above class as the example:
|
|
470
|
+
#
|
|
471
|
+
# vehicle = Vehicle.new
|
|
472
|
+
# vehicle.driver = 'John'
|
|
473
|
+
# vehicle.passenger = 'Jane'
|
|
474
|
+
#
|
|
475
|
+
# # Behaviors in the "parked" state
|
|
476
|
+
# vehicle.state # => "parked"
|
|
477
|
+
# vehicle.speed # => 0
|
|
478
|
+
# vehicle.rotate_driver # => true
|
|
479
|
+
# vehicle.driver # => "Jane"
|
|
480
|
+
# vehicle.passenger # => "John"
|
|
481
|
+
#
|
|
482
|
+
# vehicle.ignite # => true
|
|
483
|
+
#
|
|
484
|
+
# # Behaviors in the "idling" state
|
|
485
|
+
# vehicle.state # => "idling"
|
|
486
|
+
# vehicle.speed # => 20
|
|
487
|
+
# vehicle.rotate_driver # => true
|
|
488
|
+
# vehicle.driver # => "John"
|
|
489
|
+
# vehicle.passenger # => "Jane"
|
|
490
|
+
# vehicle.state # => "parked"
|
|
491
|
+
#
|
|
492
|
+
# As can be seen, both the +speed+ and +rotate_driver+ instance method
|
|
493
|
+
# implementations changed how they behave based on what the current state
|
|
494
|
+
# of the vehicle was.
|
|
495
|
+
#
|
|
496
|
+
# == Invalid behaviors
|
|
497
|
+
#
|
|
498
|
+
# If a specific behavior has not been defined for a state, then a
|
|
499
|
+
# NoMethodError exception will be raised, indicating that that method would
|
|
500
|
+
# not normally exist for an object with that state.
|
|
501
|
+
#
|
|
502
|
+
# Using the example from before:
|
|
503
|
+
#
|
|
504
|
+
# vehicle = Vehicle.new
|
|
505
|
+
# vehicle.state = "backing_up"
|
|
506
|
+
# vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
|
|
507
|
+
def state(*values, &block)
|
|
508
|
+
states = add_states(values)
|
|
509
|
+
states.each {|state| state.context(&block)} if block_given?
|
|
510
|
+
states.length == 1 ? states.first : states
|
|
395
511
|
end
|
|
396
512
|
|
|
397
513
|
# Defines additional states that are possible in the state machine, but
|
|
@@ -429,7 +545,27 @@ module StateMachine
|
|
|
429
545
|
add_states(args.flatten)
|
|
430
546
|
end
|
|
431
547
|
|
|
432
|
-
#
|
|
548
|
+
# Gets the order in which states should be displayed based on where they
|
|
549
|
+
# were first referenced. This will order states in the following priority:
|
|
550
|
+
#
|
|
551
|
+
# 1. Initial state
|
|
552
|
+
# 2. Event transitions (:to, :from, :except_to, :except_from options)
|
|
553
|
+
# 3. States with behaviors
|
|
554
|
+
# 4. States referenced via +other_states+
|
|
555
|
+
# 5. States referenced in callbacks
|
|
556
|
+
#
|
|
557
|
+
# This order will determine how the GraphViz visualizations are rendered.
|
|
558
|
+
def states_order
|
|
559
|
+
order = [initial_state(nil)]
|
|
560
|
+
|
|
561
|
+
events.each {|name, event| order |= event.known_states}
|
|
562
|
+
order |= states.select {|value, state| state.methods.any?}.map {|state| state.first}
|
|
563
|
+
order |= states.keys - callbacks.values.flatten.map {|callback| callback.known_states}.flatten
|
|
564
|
+
order |= states.keys
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Defines one or more events for the machine and the transitions that can
|
|
568
|
+
# be performed when those events are run.
|
|
433
569
|
#
|
|
434
570
|
# == Instance methods
|
|
435
571
|
#
|
|
@@ -440,13 +576,19 @@ module StateMachine
|
|
|
440
576
|
# * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state.
|
|
441
577
|
# * <tt>park!(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. If the transition fails, then a StateMachine::InvalidTransition error will be raised.
|
|
442
578
|
#
|
|
579
|
+
# With a namespace of "car", the above names map to the following methods:
|
|
580
|
+
# * <tt>can_park_car?</tt>
|
|
581
|
+
# * <tt>next_park_car_transition</tt>
|
|
582
|
+
# * <tt>park_car</tt>
|
|
583
|
+
# * <tt>park_car!</tt>
|
|
584
|
+
#
|
|
443
585
|
# == Defining transitions
|
|
444
586
|
#
|
|
445
587
|
# +event+ requires a block which allows you to define the possible
|
|
446
588
|
# transitions that can happen as a result of that event. For example,
|
|
447
589
|
#
|
|
448
|
-
# event :park do
|
|
449
|
-
# transition :to => 'parked', :from => '
|
|
590
|
+
# event :park, :stop do
|
|
591
|
+
# transition :to => 'parked', :from => 'idling'
|
|
450
592
|
# end
|
|
451
593
|
#
|
|
452
594
|
# event :first_gear do
|
|
@@ -476,19 +618,35 @@ module StateMachine
|
|
|
476
618
|
#
|
|
477
619
|
# class Vehicle
|
|
478
620
|
# state_machine do
|
|
479
|
-
#
|
|
480
|
-
#
|
|
621
|
+
# # The park, stop, and halt events will all share the given transitions
|
|
622
|
+
# event :park, :stop, :halt do
|
|
623
|
+
# transition :to => 'parked', :from => %w(idling backing_up)
|
|
624
|
+
# end
|
|
625
|
+
#
|
|
626
|
+
# event :stop do
|
|
627
|
+
# transition :to => 'idling', :from => 'first_gear'
|
|
628
|
+
# end
|
|
629
|
+
#
|
|
630
|
+
# event :ignite do
|
|
631
|
+
# transition :to => 'idling', :from => 'parked'
|
|
481
632
|
# end
|
|
482
|
-
# ...
|
|
483
633
|
# end
|
|
484
634
|
# end
|
|
485
|
-
def event(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
635
|
+
def event(*names, &block)
|
|
636
|
+
events = names.collect do |name|
|
|
637
|
+
name = name.to_s
|
|
638
|
+
event = self.events[name] ||= Event.new(self, name)
|
|
639
|
+
@events_order << name unless @events_order.include?(name)
|
|
640
|
+
|
|
641
|
+
if block_given?
|
|
642
|
+
event.instance_eval(&block)
|
|
643
|
+
add_states(event.known_states)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
event
|
|
647
|
+
end
|
|
490
648
|
|
|
491
|
-
|
|
649
|
+
events.length == 1 ? events.first : events
|
|
492
650
|
end
|
|
493
651
|
|
|
494
652
|
# Creates a callback that will be invoked *before* a transition is
|
|
@@ -702,56 +860,21 @@ module StateMachine
|
|
|
702
860
|
|
|
703
861
|
graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
|
|
704
862
|
|
|
705
|
-
# Tracks unique identifiers for dynamic states (via lambda blocks)
|
|
706
|
-
dynamic_states = {}
|
|
707
|
-
|
|
708
863
|
# Add nodes
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
# Use GraphViz-friendly name/label for dynamic/nil states
|
|
713
|
-
if state.is_a?(Proc)
|
|
714
|
-
name = "lambda#{dynamic_states.keys.length}"
|
|
715
|
-
label = '*'
|
|
716
|
-
dynamic_states[state] = name
|
|
717
|
-
else
|
|
718
|
-
name = label = state.nil? ? 'nil' : state.to_s
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
graph.add_node(name, :label => label, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
|
|
864
|
+
Array(state(*states_order)).each do |state|
|
|
865
|
+
node = state.draw(graph)
|
|
866
|
+
node.fontname = options[:font]
|
|
722
867
|
end
|
|
723
868
|
|
|
724
869
|
# Add edges
|
|
725
|
-
|
|
726
|
-
event.
|
|
727
|
-
|
|
728
|
-
from_states = guard.requirements[:from] || guard.requirements[:except_from] && (states - guard.requirements[:except_from]) || states
|
|
729
|
-
if to_state = guard.requirements[:to]
|
|
730
|
-
to_state = to_state.first
|
|
731
|
-
|
|
732
|
-
# Convert to GraphViz-friendly name
|
|
733
|
-
to_state = case to_state
|
|
734
|
-
when Proc; dynamic_states[to_state]
|
|
735
|
-
when nil; 'nil'
|
|
736
|
-
else; to_state.to_s; end
|
|
737
|
-
end
|
|
738
|
-
|
|
739
|
-
from_states.each do |from_state|
|
|
740
|
-
# Convert to GraphViz-friendly name
|
|
741
|
-
from_state = case from_state
|
|
742
|
-
when Proc; dynamic_states[from_state]
|
|
743
|
-
when nil; 'nil'
|
|
744
|
-
else; from_state.to_s; end
|
|
745
|
-
|
|
746
|
-
graph.add_edge(from_state, to_state || from_state, :label => event.name, :fontname => options[:font])
|
|
747
|
-
end
|
|
748
|
-
end
|
|
870
|
+
Array(event(*events_order)).each do |event|
|
|
871
|
+
edges = event.draw(graph)
|
|
872
|
+
edges.each {|edge| edge.fontname = options[:font]}
|
|
749
873
|
end
|
|
750
874
|
|
|
751
875
|
# Generate the graph
|
|
752
876
|
graph.output
|
|
753
|
-
|
|
754
|
-
true
|
|
877
|
+
graph
|
|
755
878
|
rescue LoadError
|
|
756
879
|
$stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
|
|
757
880
|
false
|
|
@@ -825,23 +948,7 @@ module StateMachine
|
|
|
825
948
|
# Tracks the given set of states in the list of all known states for
|
|
826
949
|
# this machine
|
|
827
950
|
def add_states(states)
|
|
828
|
-
|
|
829
|
-
@states += new_states
|
|
830
|
-
|
|
831
|
-
# Add state predicates
|
|
832
|
-
attribute = self.attribute
|
|
833
|
-
new_states.each do |state|
|
|
834
|
-
if state && (state.is_a?(String) || state.is_a?(Symbol))
|
|
835
|
-
name = "#{state}?"
|
|
836
|
-
|
|
837
|
-
owner_class.class_eval do
|
|
838
|
-
# Checks whether the current state is equal to the given value
|
|
839
|
-
define_method(name) do
|
|
840
|
-
self.send(attribute) == state
|
|
841
|
-
end unless method_defined?(name) || private_method_defined?(name)
|
|
842
|
-
end
|
|
843
|
-
end
|
|
844
|
-
end
|
|
951
|
+
states.collect {|state| @states[state] ||= State.new(self, state)}
|
|
845
952
|
end
|
|
846
953
|
end
|
|
847
954
|
end
|