state_machine 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +26 -0
- data/README.rdoc +254 -46
- data/Rakefile +29 -3
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.jpg +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/lib/state_machine.rb +161 -116
- data/lib/state_machine/assertions.rb +21 -0
- data/lib/state_machine/callback.rb +168 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +135 -101
- data/lib/state_machine/extensions.rb +83 -0
- data/lib/state_machine/guard.rb +115 -0
- data/lib/state_machine/integrations/active_record.rb +242 -0
- data/lib/state_machine/integrations/data_mapper.rb +198 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +153 -0
- data/lib/state_machine/integrations/sequel.rb +169 -0
- data/lib/state_machine/machine.rb +746 -352
- data/lib/state_machine/transition.rb +104 -212
- data/test/active_record.log +34865 -0
- data/test/classes/switch.rb +11 -0
- data/test/data_mapper.log +14015 -0
- data/test/functional/state_machine_test.rb +249 -15
- data/test/sequel.log +3835 -0
- data/test/test_helper.rb +3 -12
- data/test/unit/assertions_test.rb +13 -0
- data/test/unit/callback_test.rb +189 -0
- data/test/unit/eval_helpers_test.rb +92 -0
- data/test/unit/event_test.rb +247 -113
- data/test/unit/guard_test.rb +420 -0
- data/test/unit/integrations/active_record_test.rb +515 -0
- data/test/unit/integrations/data_mapper_test.rb +407 -0
- data/test/unit/integrations/sequel_test.rb +244 -0
- data/test/unit/invalid_transition_test.rb +1 -1
- data/test/unit/machine_test.rb +1056 -98
- data/test/unit/state_machine_test.rb +14 -113
- data/test/unit/transition_test.rb +269 -495
- metadata +44 -30
- data/test/app_root/app/models/auto_shop.rb +0 -34
- data/test/app_root/app/models/car.rb +0 -19
- data/test/app_root/app/models/highway.rb +0 -3
- data/test/app_root/app/models/motorcycle.rb +0 -3
- data/test/app_root/app/models/switch.rb +0 -23
- data/test/app_root/app/models/switch_observer.rb +0 -20
- data/test/app_root/app/models/toggle_switch.rb +0 -2
- data/test/app_root/app/models/vehicle.rb +0 -78
- data/test/app_root/config/environment.rb +0 -7
- data/test/app_root/db/migrate/001_create_switches.rb +0 -12
- data/test/app_root/db/migrate/002_create_auto_shops.rb +0 -13
- data/test/app_root/db/migrate/003_create_highways.rb +0 -11
- data/test/app_root/db/migrate/004_create_vehicles.rb +0 -16
- data/test/factory.rb +0 -77
@@ -0,0 +1,169 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
# Adds support for integrating state machines with Sequel models.
|
4
|
+
#
|
5
|
+
# == Examples
|
6
|
+
#
|
7
|
+
# Below is an example of a simple state machine defined within a
|
8
|
+
# Sequel model:
|
9
|
+
#
|
10
|
+
# class Vehicle < Sequel::Model
|
11
|
+
# state_machine :initial => 'parked' do
|
12
|
+
# event :ignite do
|
13
|
+
# transition :to => 'idling', :from => 'parked'
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# The examples in the sections below will use the above class as a
|
19
|
+
# reference.
|
20
|
+
#
|
21
|
+
# == Actions
|
22
|
+
#
|
23
|
+
# By default, the action that will be invoked when a state is transitioned
|
24
|
+
# is the +save+ action. This will cause the resource to save the changes
|
25
|
+
# made to the state machine's attribute. *Note* that if any other changes
|
26
|
+
# were made to the resource prior to transition, then those changes will
|
27
|
+
# be made as well.
|
28
|
+
#
|
29
|
+
# For example,
|
30
|
+
#
|
31
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state=nil>
|
32
|
+
# vehicle.name = 'Ford Explorer'
|
33
|
+
# vehicle.ignite # => true
|
34
|
+
# vehicle.refresh # => #<Vehicle id=1 name="Ford Explorer" state="idling">
|
35
|
+
#
|
36
|
+
# == Transactions
|
37
|
+
#
|
38
|
+
# In order to ensure that any changes made during transition callbacks
|
39
|
+
# are rolled back during a failed attempt, every transition is wrapped
|
40
|
+
# within a transaction.
|
41
|
+
#
|
42
|
+
# For example,
|
43
|
+
#
|
44
|
+
# class Message < Sequel::Model
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# Vehicle.state_machine do
|
48
|
+
# before_transition do |transition|
|
49
|
+
# Message.create(:content => transition.inspect)
|
50
|
+
# false
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state=nil>
|
55
|
+
# vehicle.ignite # => false
|
56
|
+
# Message.count # => 0
|
57
|
+
#
|
58
|
+
# *Note* that only before callbacks that halt the callback chain and
|
59
|
+
# failed attempts to save the record will result in the transaction being
|
60
|
+
# rolled back. If an after callback halts the chain, the previous result
|
61
|
+
# still applies and the transaction is *not* rolled back.
|
62
|
+
#
|
63
|
+
# == Scopes
|
64
|
+
#
|
65
|
+
# To assist in filtering models with specific states, a series of class
|
66
|
+
# methods are defined on the model for finding records with or without a
|
67
|
+
# particular set of states.
|
68
|
+
#
|
69
|
+
# These named scopes are the functional equivalent of the following
|
70
|
+
# definitions:
|
71
|
+
#
|
72
|
+
# class Vehicle < Sequel::Model
|
73
|
+
# class << self
|
74
|
+
# def with_states(*values)
|
75
|
+
# filter(:state => values)
|
76
|
+
# end
|
77
|
+
# alias_method :with_state, :with_states
|
78
|
+
#
|
79
|
+
# def without_states(*values)
|
80
|
+
# filter(~{:state => values})
|
81
|
+
# end
|
82
|
+
# alias_method :without_state, :without_states
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# Because of the way scopes work in Sequel, they can be chained like so:
|
87
|
+
#
|
88
|
+
# Vehicle.with_state('parked').with_state('idling').order(:id.desc)
|
89
|
+
#
|
90
|
+
# == Callbacks
|
91
|
+
#
|
92
|
+
# All before/after transition callbacks defined for Sequel resources
|
93
|
+
# behave in the same way that other Sequel hooks behave. Rather than
|
94
|
+
# passing in the record as an argument to the callback, the callback is
|
95
|
+
# instead bound to the object and evaluated within its context.
|
96
|
+
#
|
97
|
+
# For example,
|
98
|
+
#
|
99
|
+
# class Vehicle < Sequel::Model
|
100
|
+
# state_machine :initial => 'parked' do
|
101
|
+
# before_transition :to => 'idling' do
|
102
|
+
# put_on_seatbelt
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# before_transition do |transition|
|
106
|
+
# # log message
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# event :ignite do
|
110
|
+
# transition :to => 'idling', :from => 'parked'
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# def put_on_seatbelt
|
115
|
+
# ...
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# Note, also, that the transition can be accessed by simply defining
|
120
|
+
# additional arguments in the callback block.
|
121
|
+
module Sequel
|
122
|
+
# Should this integration be used for state machines in the given class?
|
123
|
+
# Classes that include Sequel::Model will automatically use the Sequel
|
124
|
+
# integration.
|
125
|
+
def self.matches?(klass)
|
126
|
+
defined?(::Sequel::Model) && klass <= ::Sequel::Model
|
127
|
+
end
|
128
|
+
|
129
|
+
# Runs a new database transaction, rolling back any changes if the
|
130
|
+
# yielded block fails (i.e. returns false).
|
131
|
+
def within_transaction(object)
|
132
|
+
object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
|
133
|
+
end
|
134
|
+
|
135
|
+
protected
|
136
|
+
# Sets the default action for all Sequel state machines to +save+
|
137
|
+
def default_action
|
138
|
+
:save
|
139
|
+
end
|
140
|
+
|
141
|
+
# Defines a scope for finding records *with* a particular value or
|
142
|
+
# values for the attribute
|
143
|
+
def define_with_scope(name)
|
144
|
+
attribute = self.attribute
|
145
|
+
(class << owner_class; self; end).class_eval do
|
146
|
+
define_method(name) {|*values| filter(attribute.to_sym => values.flatten)}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Defines a scope for finding records *without* a particular value or
|
151
|
+
# values for the attribute
|
152
|
+
def define_without_scope(name)
|
153
|
+
attribute = self.attribute
|
154
|
+
(class << owner_class; self; end).class_eval do
|
155
|
+
define_method(name) {|*values| filter(~{attribute.to_sym => values.flatten})}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Creates a new callback in the callback chain, always ensuring that
|
160
|
+
# it's configured to bind to the object as this is the convention for
|
161
|
+
# Sequel callbacks
|
162
|
+
def add_callback(type, options, &block)
|
163
|
+
options[:bind_to_object] = true
|
164
|
+
options[:terminator] = @terminator ||= lambda {|result| result == false}
|
165
|
+
super
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -1,412 +1,806 @@
|
|
1
|
+
require 'state_machine/extensions'
|
1
2
|
require 'state_machine/event'
|
3
|
+
require 'state_machine/callback'
|
4
|
+
require 'state_machine/assertions'
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
# Load each available integration
|
7
|
+
Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
|
8
|
+
require "state_machine/integrations/#{File.basename(path)}"
|
9
|
+
end
|
10
|
+
|
11
|
+
module StateMachine
|
12
|
+
# 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
|
+
# changes after a particular event is fired.
|
15
|
+
#
|
16
|
+
# A state machine may not necessarily know all of the possible states for
|
17
|
+
# an object since they can be any arbitrary value. As a result, anything
|
18
|
+
# that relies on a list of all possible states should keep in mind that if
|
19
|
+
# a state has not been referenced *anywhere* in the state machine definition,
|
20
|
+
# then it will *not* be a known state unless the +other_states+ is used.
|
21
|
+
#
|
22
|
+
# == State values
|
23
|
+
#
|
24
|
+
# While string are the most common object type used for setting values on
|
25
|
+
# the state of the machine, there are no restrictions on what can be used.
|
26
|
+
# This means that symbols, integers, dates/times, etc. can all be used.
|
27
|
+
#
|
28
|
+
# With string states:
|
29
|
+
#
|
30
|
+
# class Vehicle
|
31
|
+
# state_machine :initial => 'parked' do
|
32
|
+
# event :ignite do
|
33
|
+
# transition :to => 'idling', :from => 'parked'
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# With symbolic states:
|
39
|
+
#
|
40
|
+
# class Vehicle
|
41
|
+
# state_machine :initial => :parked do
|
42
|
+
# event :ignite do
|
43
|
+
# transition :to => :idling, :from => :parked
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# With time states:
|
49
|
+
#
|
50
|
+
# class Switch
|
51
|
+
# state_machine :activated_at
|
52
|
+
# event :activate do
|
53
|
+
# transition :to => lambda {Time.now}
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# event :deactivate do
|
57
|
+
# transition :to => nil
|
58
|
+
# end
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# == Callbacks
|
63
|
+
#
|
64
|
+
# Callbacks are supported for hooking before and after every possible
|
65
|
+
# transition in the machine. Each callback is invoked in the order in which
|
66
|
+
# it was defined. See StateMachine::Machine#before_transition
|
67
|
+
# and StateMachine::Machine#after_transition for documentation
|
68
|
+
# on how to define new callbacks.
|
69
|
+
#
|
70
|
+
# === Canceling callbacks
|
71
|
+
#
|
72
|
+
# Callbacks can be canceled by throwing :halt at any point during the
|
73
|
+
# callback. For example,
|
74
|
+
#
|
75
|
+
# ...
|
76
|
+
# throw :halt
|
77
|
+
# ...
|
78
|
+
#
|
79
|
+
# If a +before+ callback halts the chain, the associated transition and all
|
80
|
+
# later callbacks are canceled. If an +after+ callback halts the chain,
|
81
|
+
# the later callbacks are canceled, but the transition is still successful.
|
82
|
+
#
|
83
|
+
# *Note* that if a +before+ callback fails and the bang version of an event
|
84
|
+
# was invoked, an exception will be raised instead of returning false. For
|
85
|
+
# example,
|
86
|
+
#
|
87
|
+
# class Vehicle
|
88
|
+
# state_machine, :initial => 'parked' do
|
89
|
+
# before_transition :to => 'idling', :do => lambda {|vehicle| throw :halt}
|
90
|
+
# ...
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# vehicle = Vehicle.new
|
95
|
+
# vehicle.park # => false
|
96
|
+
# vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "idling"
|
97
|
+
#
|
98
|
+
# == Observers
|
99
|
+
#
|
100
|
+
# Observers, in the sense of external classes and *not* Ruby's Observable
|
101
|
+
# mechanism, can hook into state machines as well. Such observers use the
|
102
|
+
# same callback api that's used internally.
|
103
|
+
#
|
104
|
+
# Below are examples of defining observers for the following state machine:
|
105
|
+
#
|
106
|
+
# class Vehicle
|
107
|
+
# state_machine do
|
108
|
+
# event :ignite do
|
109
|
+
# transition :to => 'idling', :from => 'parked'
|
110
|
+
# end
|
111
|
+
# ...
|
112
|
+
# end
|
113
|
+
# ...
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# Event/Transition behaviors:
|
117
|
+
#
|
118
|
+
# class VehicleObserver
|
119
|
+
# def self.before_park(vehicle, transition)
|
120
|
+
# logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# def self.after_park(vehicle, transition, result)
|
124
|
+
# logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# def self.before_transition(vehicle, transition)
|
128
|
+
# logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# def self.after_transition(vehicle, transition, result)
|
132
|
+
# logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# Vehicle.state_machine do
|
137
|
+
# before_transition :on => :park, :do => VehicleObserver.method(:before_park)
|
138
|
+
# before_transition VehicleObserver.method(:before_transition)
|
139
|
+
#
|
140
|
+
# after_transition :on => :park, :do => VehicleObserver.method(:after_park)
|
141
|
+
# after_transition VehicleObserver.method(:after_transition)
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# One common callback is to record transitions for all models in the system
|
145
|
+
# for auditing/debugging purposes. Below is an example of an observer that
|
146
|
+
# can easily automate this process for all models:
|
147
|
+
#
|
148
|
+
# class StateMachineObserver
|
149
|
+
# def self.before_transition(object, transition)
|
150
|
+
# Audit.log_transition(object.attributes)
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# [Vehicle, Switch, Project].each do |klass|
|
155
|
+
# klass.state_machines.each do |machine|
|
156
|
+
# machine.before_transition klass.method(:before_transition)
|
157
|
+
# end
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# Additional observer-like behavior may be exposed by the various
|
161
|
+
# integrations available. See below for more information.
|
162
|
+
#
|
163
|
+
# == Integrations
|
164
|
+
#
|
165
|
+
# By default, state machines are library-agnostic, meaning that they work
|
166
|
+
# on any Ruby class and have no external dependencies. However, there are
|
167
|
+
# certain libraries which expose additional behavior that can be taken
|
168
|
+
# advantage of by state machines.
|
169
|
+
#
|
170
|
+
# This library is built to work out of the box with a few popular Ruby
|
171
|
+
# libraries that allow for additional behavior to provide a cleaner and
|
172
|
+
# smoother experience. This is especially the case for objects backed by a
|
173
|
+
# database that may allow for transactions, persistent storage,
|
174
|
+
# search/filters, callbacks, etc.
|
175
|
+
#
|
176
|
+
# When a state machine is defined for classes using any of the above libraries,
|
177
|
+
# it will try to automatically determine the integration to use (Agnostic,
|
178
|
+
# ActiveRecord, DataMapper, or Sequel) based on the class definition. To
|
179
|
+
# see how each integration affects the machine's behavior, refer to all
|
180
|
+
# constants defined under the StateMachine::Integrations namespace.
|
181
|
+
class Machine
|
182
|
+
include Assertions
|
183
|
+
|
184
|
+
# The class that the machine is defined in
|
185
|
+
attr_reader :owner_class
|
186
|
+
|
187
|
+
# The attribute for which the machine is being defined
|
188
|
+
attr_reader :attribute
|
189
|
+
|
190
|
+
# The initial state that the machine will be in when an object is created
|
191
|
+
attr_reader :initial_state
|
192
|
+
|
193
|
+
# The events that trigger transitions
|
194
|
+
attr_reader :events
|
195
|
+
|
196
|
+
# A list of all of the states known to this state machine. This will pull
|
197
|
+
# state names from the following sources:
|
198
|
+
# * Initial state
|
199
|
+
# * Event transitions (:to, :from, :except_to, and :except_from options)
|
200
|
+
# * Transition callbacks (:to, :from, :except_to, and :except_from options)
|
201
|
+
# * Unreferenced states (using +other_states+ helper)
|
202
|
+
attr_reader :states
|
203
|
+
|
204
|
+
# The callbacks to invoke before/after a transition is performed
|
205
|
+
attr_reader :callbacks
|
206
|
+
|
207
|
+
# The action to invoke when an object transitions
|
208
|
+
attr_reader :action
|
209
|
+
|
210
|
+
class << self
|
211
|
+
# Attempts to find or create a state machine for the given class. For
|
212
|
+
# example,
|
213
|
+
#
|
214
|
+
# StateMachine::Machine.find_or_create(Switch)
|
215
|
+
# StateMachine::Machine.find_or_create(Switch, :initial => 'off')
|
216
|
+
# StateMachine::Machine.find_or_create(Switch, 'status')
|
217
|
+
# StateMachine::Machine.find_or_create(Switch, 'status', :initial => 'off')
|
218
|
+
#
|
219
|
+
# If a machine of the given name already exists in one of the class's
|
220
|
+
# superclasses, then a copy of that machine will be created and stored
|
221
|
+
# in the new owner class (the original will remain unchanged).
|
222
|
+
def find_or_create(owner_class, *args)
|
223
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
224
|
+
attribute = args.any? ? args.first.to_s : 'state'
|
225
|
+
|
226
|
+
# Attempts to find an existing machine
|
227
|
+
if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
|
228
|
+
machine = machine.within_context(owner_class, options) unless machine.owner_class == owner_class
|
229
|
+
else
|
230
|
+
# No existing machine: create a new one
|
231
|
+
machine = new(owner_class, attribute, options)
|
232
|
+
end
|
233
|
+
|
234
|
+
machine
|
235
|
+
end
|
236
|
+
|
237
|
+
# Draws the state machines defined in the given classes using GraphViz.
|
238
|
+
# The given classes must be a comma-delimited string of class names.
|
239
|
+
#
|
240
|
+
# Configuration options:
|
241
|
+
# * +file+ - A comma-delimited string of files to load that contain the state machine definitions to draw
|
242
|
+
# * +path+ - The path to write the graph file to
|
243
|
+
# * +format+ - The image format to generate the graph in
|
244
|
+
# * +font+ - The name of the font to draw state names in
|
245
|
+
def draw(class_names, options = {})
|
246
|
+
raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
|
247
|
+
|
248
|
+
# Load any files
|
249
|
+
if files = options.delete(:file)
|
250
|
+
files.split(',').each {|file| require file}
|
251
|
+
end
|
252
|
+
|
253
|
+
class_names.split(',').each do |class_name|
|
254
|
+
# Navigate through the namespace structure to get to the class
|
255
|
+
klass = Object
|
256
|
+
class_name.split('::').each do |name|
|
257
|
+
klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
|
258
|
+
end
|
259
|
+
|
260
|
+
# Draw each of the class's state machines
|
261
|
+
klass.state_machines.values.each do |machine|
|
262
|
+
machine.draw(options)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Creates a new state machine for the given attribute
|
269
|
+
def initialize(owner_class, *args, &block)
|
270
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
271
|
+
assert_valid_keys(options, :initial, :action, :plural, :integration)
|
272
|
+
|
273
|
+
# Set machine configuration
|
274
|
+
@attribute = (args.first || 'state').to_s
|
275
|
+
@events = {}
|
276
|
+
@states = []
|
277
|
+
@callbacks = {:before => [], :after => []}
|
278
|
+
@action = options[:action]
|
279
|
+
|
280
|
+
# Add class-/instance-level methods to the owner class for state initialization
|
281
|
+
owner_class.class_eval do
|
282
|
+
extend StateMachine::ClassMethods
|
283
|
+
include StateMachine::InstanceMethods
|
284
|
+
end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
|
285
|
+
|
286
|
+
# Initialize the context of the machine
|
287
|
+
set_context(owner_class, :initial => options[:initial], :integration => options[:integration], &block)
|
288
|
+
|
289
|
+
# Set integration-specific configurations
|
290
|
+
@action ||= default_action unless options.include?(:action)
|
291
|
+
define_attribute_accessor
|
292
|
+
define_scopes(options[:plural])
|
293
|
+
|
294
|
+
# Call after hook for integration-specific extensions
|
295
|
+
after_initialize
|
296
|
+
end
|
297
|
+
|
298
|
+
# Creates a copy of this machine in addition to copies of each associated
|
299
|
+
# event, so that the list of transitions for each event don't conflict
|
300
|
+
# with different machines
|
301
|
+
def initialize_copy(orig) #:nodoc:
|
302
|
+
super
|
303
|
+
|
304
|
+
@events = @events.inject({}) do |events, (name, event)|
|
305
|
+
event = event.dup
|
306
|
+
event.machine = self
|
307
|
+
events[name] = event
|
308
|
+
events
|
309
|
+
end
|
310
|
+
@states = @states.dup
|
311
|
+
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
|
312
|
+
end
|
313
|
+
|
314
|
+
# Creates a copy of this machine within the context of the given class.
|
315
|
+
# This should be used for inheritance support of state machines.
|
316
|
+
def within_context(owner_class, options = {}) #:nodoc:
|
317
|
+
machine = dup
|
318
|
+
machine.set_context(owner_class, {:integration => @integration}.merge(options))
|
319
|
+
machine
|
320
|
+
end
|
321
|
+
|
322
|
+
# Changes the context of this machine to the given class so that new
|
323
|
+
# events and transitions are created in the proper context.
|
324
|
+
#
|
325
|
+
# Configuration options:
|
326
|
+
# * +initial+ - The initial value to set the attribute to
|
327
|
+
# * +integration+ - The name of the integration for extending this machine with library-specific behavior
|
328
|
+
#
|
329
|
+
# All other configuration options for the machine can only be set on
|
330
|
+
# creation.
|
331
|
+
def set_context(owner_class, options = {}) #:nodoc:
|
332
|
+
assert_valid_keys(options, :initial, :integration)
|
333
|
+
|
334
|
+
@owner_class = owner_class
|
335
|
+
if options[:initial]
|
336
|
+
@initial_state = options[:initial]
|
337
|
+
add_states([@initial_state]) unless @initial_state.is_a?(Proc)
|
338
|
+
end
|
339
|
+
|
340
|
+
# Find an integration that can be used for implementing various parts
|
341
|
+
# of the state machine that may behave differently in different libraries
|
342
|
+
if @integration = options[:integration] || StateMachine::Integrations.constants.find {|name| StateMachine::Integrations.const_get(name).matches?(owner_class)}
|
343
|
+
extend StateMachine::Integrations.const_get(@integration.to_s.gsub(/(?:^|_)(.)/) {$1.upcase})
|
344
|
+
end
|
345
|
+
|
346
|
+
# Record this machine as matched to the attribute in the current owner
|
347
|
+
# class. This will override any machines mapped to the same attribute
|
348
|
+
# in any superclasses.
|
349
|
+
owner_class.state_machines[attribute] = self
|
350
|
+
end
|
351
|
+
|
352
|
+
# Gets the initial state of the machine for the given object. If a dynamic
|
353
|
+
# initial state was configured for this machine, then the object will be
|
354
|
+
# passed into the proc to help determine the actual value of the initial
|
355
|
+
# state.
|
356
|
+
#
|
357
|
+
# == Examples
|
358
|
+
#
|
359
|
+
# With static initial state:
|
360
|
+
#
|
361
|
+
# class Vehicle
|
362
|
+
# state_machine :initial => 'parked' do
|
363
|
+
# ...
|
364
|
+
# end
|
365
|
+
# end
|
366
|
+
#
|
367
|
+
# Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
|
368
|
+
#
|
369
|
+
# With dynamic initial state:
|
370
|
+
#
|
371
|
+
# class Vehicle
|
372
|
+
# state_machine :initial => lambda {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
|
373
|
+
# ...
|
374
|
+
# end
|
375
|
+
# end
|
376
|
+
#
|
377
|
+
# Vehicle.state_machines['state'].initial_state(vehicle) # => "idling"
|
378
|
+
def initial_state(object)
|
379
|
+
@initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state
|
380
|
+
end
|
381
|
+
|
382
|
+
# Defines additional states that are possible in the state machine, but
|
383
|
+
# which are derived outside of any events/transitions or possibly
|
384
|
+
# dynamically via Proc. This allows the creation of state conditionals
|
385
|
+
# which are not defined in the standard :to or :from structure.
|
8
386
|
#
|
9
|
-
#
|
10
|
-
# an object since they can be any arbitrary value. As a result, anything
|
11
|
-
# that relies on a list of all possible states should keep in mind that if
|
12
|
-
# a state has not been referenced *anywhere* in the state machine definition,
|
13
|
-
# then it will *not* be a known state.
|
387
|
+
# == Example
|
14
388
|
#
|
15
|
-
#
|
389
|
+
# class Vehicle
|
390
|
+
# state_machine :initial => 'parked' do
|
391
|
+
# event :ignite do
|
392
|
+
# transition :to => 'idling', :from => 'parked'
|
393
|
+
# end
|
394
|
+
#
|
395
|
+
# other_states %w(stalled stopped)
|
396
|
+
# end
|
397
|
+
#
|
398
|
+
# def stop
|
399
|
+
# self.state = 'stopped'
|
400
|
+
# end
|
401
|
+
# end
|
16
402
|
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
403
|
+
# In the above state machine, the known states would be:
|
404
|
+
# * +idling+
|
405
|
+
# * +parked+
|
406
|
+
# * +stalled+
|
407
|
+
# * +stopped+
|
22
408
|
#
|
23
|
-
#
|
409
|
+
# Since +stalled+ and +stopped+ are not referenced in any transitions or
|
410
|
+
# callbacks, they are explicitly defined.
|
411
|
+
def other_states(*args)
|
412
|
+
add_states(args.flatten)
|
413
|
+
end
|
414
|
+
|
415
|
+
# Defines an event for the machine.
|
24
416
|
#
|
25
|
-
#
|
26
|
-
# associated transition are cancelled. If an +after+ callback returns false,
|
27
|
-
# the later callbacks are cancelled, but the transition is still successful.
|
28
|
-
# This is the same behavior as exposed by ActiveRecord's callback support.
|
417
|
+
# == Instance methods
|
29
418
|
#
|
30
|
-
#
|
31
|
-
#
|
419
|
+
# The following instance methods are generated when a new event is defined
|
420
|
+
# (the "park" event is used as an example):
|
421
|
+
# * <tt>can_park?</tt> - Checks whether the "park" event can be fired given the current state of the object.
|
422
|
+
# * <tt>next_park_transition</tt> - Gets the next transition that would be performed if the "park" event were to be fired now on the object or nil if no transitions can be performed.
|
423
|
+
# * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state.
|
424
|
+
# * <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.
|
32
425
|
#
|
33
|
-
# ==
|
426
|
+
# == Defining transitions
|
34
427
|
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
# types of behaviors can be observed:
|
38
|
-
# * events (e.g. before_park/after_park, before_ignite/after_ignite)
|
39
|
-
# * transitions (before_transition/after_transition)
|
428
|
+
# +event+ requires a block which allows you to define the possible
|
429
|
+
# transitions that can happen as a result of that event. For example,
|
40
430
|
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
431
|
+
# event :park do
|
432
|
+
# transition :to => 'parked', :from => 'idle'
|
433
|
+
# end
|
434
|
+
#
|
435
|
+
# event :first_gear do
|
436
|
+
# transition :to => 'first_gear', :from => 'parked', :if => :seatbelt_on?
|
437
|
+
# end
|
44
438
|
#
|
45
|
-
#
|
439
|
+
# See StateMachine::Event#transition for more information on
|
440
|
+
# the possible options that can be passed in.
|
441
|
+
#
|
442
|
+
# *Note* that this block is executed within the context of the actual event
|
443
|
+
# object. As a result, you will not be able to reference any class methods
|
444
|
+
# on the model without referencing the class itself. For example,
|
445
|
+
#
|
446
|
+
# class Vehicle
|
447
|
+
# def self.safe_states
|
448
|
+
# %w(parked idling stalled)
|
449
|
+
# end
|
450
|
+
#
|
46
451
|
# state_machine do
|
47
452
|
# event :park do
|
48
|
-
# transition :to => 'parked', :from =>
|
453
|
+
# transition :to => 'parked', :from => Car.safe_states
|
454
|
+
# end
|
455
|
+
# end
|
456
|
+
# end
|
457
|
+
#
|
458
|
+
# == Example
|
459
|
+
#
|
460
|
+
# class Vehicle
|
461
|
+
# state_machine do
|
462
|
+
# event :park do
|
463
|
+
# transition :to => 'parked', :from => %w(first_gear reverse)
|
49
464
|
# end
|
50
465
|
# ...
|
51
466
|
# end
|
52
|
-
# ...
|
53
467
|
# end
|
468
|
+
def event(name, &block)
|
469
|
+
name = name.to_s
|
470
|
+
event = events[name] ||= Event.new(self, name)
|
471
|
+
event.instance_eval(&block)
|
472
|
+
add_states(event.known_states)
|
473
|
+
|
474
|
+
event
|
475
|
+
end
|
476
|
+
|
477
|
+
# Creates a callback that will be invoked *before* a transition is
|
478
|
+
# performed so long as the given configuration options match the transition.
|
479
|
+
# Each part of the transition (event, to state, from state) must match in
|
480
|
+
# order for the callback to get invoked.
|
481
|
+
#
|
482
|
+
# Configuration options:
|
483
|
+
# * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
|
484
|
+
# * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
|
485
|
+
# * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
|
486
|
+
# * +except_to+ - One more states *not* being transitioned to
|
487
|
+
# * +except_from+ - One or more states *not* being transitioned from
|
488
|
+
# * +except_on+ - One or more events that *did not* fire the transition
|
489
|
+
# * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
|
490
|
+
# * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
|
491
|
+
# * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
|
492
|
+
#
|
493
|
+
# The +except+ group of options (+except_to+, +exception_from+, and
|
494
|
+
# +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
|
495
|
+
# +from+, and +on+, respectively)
|
54
496
|
#
|
55
|
-
#
|
497
|
+
# == The callback
|
56
498
|
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
499
|
+
# When defining additional configuration options, callbacks must be defined
|
500
|
+
# in either the :do option or as a block. For example,
|
501
|
+
#
|
502
|
+
# class Vehicle
|
503
|
+
# state_machine do
|
504
|
+
# before_transition :to => 'parked', :do => :set_alarm
|
505
|
+
# before_transition :to => 'parked' do |vehicle, transition|
|
506
|
+
# vehicle.set_alarm
|
507
|
+
# end
|
508
|
+
# ...
|
60
509
|
# end
|
510
|
+
# end
|
511
|
+
#
|
512
|
+
# === Accessing the transition
|
513
|
+
#
|
514
|
+
# In addition to passing the object being transitioned, the actual
|
515
|
+
# transition describing the context (e.g. event, from state, to state)
|
516
|
+
# can be accessed as well. This additional argument is only passed if the
|
517
|
+
# callback allows for it.
|
518
|
+
#
|
519
|
+
# For example,
|
520
|
+
#
|
521
|
+
# class Vehicle
|
522
|
+
# # Only specifies one parameter (the object being transitioned)
|
523
|
+
# before_transition :to => 'parked', :do => lambda {|vehicle| vehicle.set_alarm}
|
61
524
|
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
# end
|
525
|
+
# # Specifies 2 parameters (object being transitioned and actual transition)
|
526
|
+
# before_transition :to => 'parked', :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
|
65
527
|
# end
|
66
528
|
#
|
67
|
-
#
|
529
|
+
# *Note* that the object in the callback will only be passed in as an
|
530
|
+
# argument if callbacks are configured to *not* be bound to the object
|
531
|
+
# involved. This is the default and may change on a per-integration basis.
|
532
|
+
#
|
533
|
+
# See StateMachine::Transition for more information about the
|
534
|
+
# attributes available on the transition.
|
68
535
|
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
536
|
+
# == Examples
|
537
|
+
#
|
538
|
+
# Below is an example of a class with one state machine and various types
|
539
|
+
# of +before+ transitions defined for it:
|
540
|
+
#
|
541
|
+
# class Vehicle
|
542
|
+
# state_machine do
|
543
|
+
# # Before all transitions
|
544
|
+
# before_transition :update_dashboard
|
545
|
+
#
|
546
|
+
# # Before specific transition:
|
547
|
+
# before_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
|
548
|
+
#
|
549
|
+
# # With conditional callback:
|
550
|
+
# before_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
|
551
|
+
#
|
552
|
+
# # Using :except counterparts:
|
553
|
+
# before_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
|
554
|
+
# ...
|
72
555
|
# end
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
556
|
+
# end
|
557
|
+
#
|
558
|
+
# As can be seen, any number of transitions can be created using various
|
559
|
+
# combinations of configuration options.
|
560
|
+
def before_transition(options = {}, &block)
|
561
|
+
add_callback(:before, options.is_a?(Hash) ? options : {:do => options}, &block)
|
562
|
+
end
|
563
|
+
|
564
|
+
# Creates a callback that will be invoked *after* a transition is
|
565
|
+
# performed, so long as the given configuration options match the transition.
|
566
|
+
# Each part of the transition (event, to state, from state) must match
|
567
|
+
# in order for the callback to get invoked.
|
568
|
+
#
|
569
|
+
# Configuration options:
|
570
|
+
# * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
|
571
|
+
# * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
|
572
|
+
# * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
|
573
|
+
# * +except_to+ - One more states *not* being transitioned to
|
574
|
+
# * +except_from+ - One or more states *not* being transitioned from
|
575
|
+
# * +except_on+ - One or more events that *did not* fire the transition
|
576
|
+
# * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
|
577
|
+
# * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
|
578
|
+
# * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
|
579
|
+
#
|
580
|
+
# The +except+ group of options (+except_to+, +exception_from+, and
|
581
|
+
# +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
|
582
|
+
# +from+, and +on+, respectively)
|
583
|
+
#
|
584
|
+
# == The callback
|
585
|
+
#
|
586
|
+
# When defining additional configuration options, callbacks must be defined
|
587
|
+
# in either the :do option or as a block. For example,
|
588
|
+
#
|
589
|
+
# class Vehicle
|
590
|
+
# state_machine do
|
591
|
+
# after_transition :to => 'parked', :do => :set_alarm
|
592
|
+
# after_transition :to => 'parked' do |vehicle, transition, result|
|
593
|
+
# vehicle.set_alarm
|
594
|
+
# end
|
595
|
+
# ...
|
76
596
|
# end
|
77
597
|
# end
|
78
598
|
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
599
|
+
# === Accessing the transition / result
|
600
|
+
#
|
601
|
+
# In addition to passing the object being transitioned, the actual
|
602
|
+
# transition describing the context (e.g. event, from state, to state) and
|
603
|
+
# the result from calling the object's action can be optionally passed as
|
604
|
+
# well. These additional arguments are only passed if the callback allows
|
605
|
+
# for it.
|
82
606
|
#
|
83
|
-
#
|
84
|
-
#
|
607
|
+
# For example,
|
608
|
+
#
|
609
|
+
# class Vehicle
|
610
|
+
# # Only specifies one parameter (the object being transitioned)
|
611
|
+
# after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.set_alarm}
|
85
612
|
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
613
|
+
# # Specifies 3 parameters (object being transitioned, transition, and action result)
|
614
|
+
# after_transition :to => 'parked', :do => lambda {|vehicle, transition, result| vehicle.set_alarm(transition) if result}
|
615
|
+
# end
|
616
|
+
#
|
617
|
+
# *Note* that the object in the callback will only be passed in as an
|
618
|
+
# argument if callbacks are configured to *not* be bound to the object
|
619
|
+
# involved. This is the default and may change on a per-integration basis.
|
620
|
+
#
|
621
|
+
# See StateMachine::Transition for more information about the
|
622
|
+
# attributes available on the transition.
|
623
|
+
#
|
624
|
+
# == Examples
|
625
|
+
#
|
626
|
+
# Below is an example of a model with one state machine and various types
|
627
|
+
# of +after+ transitions defined for it:
|
628
|
+
#
|
629
|
+
# class Vehicle
|
630
|
+
# state_machine do
|
631
|
+
# # After all transitions
|
632
|
+
# after_transition :update_dashboard
|
633
|
+
#
|
634
|
+
# # After specific transition:
|
635
|
+
# after_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
|
636
|
+
#
|
637
|
+
# # With conditional callback:
|
638
|
+
# after_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
|
639
|
+
#
|
640
|
+
# # Using :except counterparts:
|
641
|
+
# after_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
|
642
|
+
# ...
|
89
643
|
# end
|
90
644
|
# end
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
645
|
+
#
|
646
|
+
# As can be seen, any number of transitions can be created using various
|
647
|
+
# combinations of configuration options.
|
648
|
+
def after_transition(options = {}, &block)
|
649
|
+
add_callback(:after, options.is_a?(Hash) ? options : {:do => options}, &block)
|
650
|
+
end
|
651
|
+
|
652
|
+
# Runs a transaction, rolling back any changes if the yielded block fails.
|
653
|
+
#
|
654
|
+
# This is only applicable to integrations that involve databases. By
|
655
|
+
# default, this will not run any transactions, since the changes aren't
|
656
|
+
# taking place within the context of a database.
|
657
|
+
def within_transaction(object)
|
658
|
+
yield
|
659
|
+
end
|
660
|
+
|
661
|
+
# Draws a directed graph of the machine for visualizing the various events,
|
662
|
+
# states, and their transitions.
|
663
|
+
#
|
664
|
+
# This requires both the Ruby graphviz gem and the graphviz library be
|
665
|
+
# installed on the system.
|
666
|
+
#
|
667
|
+
# Configuration options:
|
668
|
+
# * +name+ - The name of the file to write to (without the file extension). Default is "#{owner_class.name}_#{attribute}"
|
669
|
+
# * +path+ - The path to write the graph file to. Default is the current directory (".").
|
670
|
+
# * +format+ - The image format to generate the graph in. Default is "png'.
|
671
|
+
# * +font+ - The name of the font to draw state names in. Default is "Arial'.
|
672
|
+
def draw(options = {})
|
673
|
+
options = {
|
674
|
+
:name => "#{owner_class.name}_#{attribute}",
|
675
|
+
:path => '.',
|
676
|
+
:format => 'png',
|
677
|
+
:font => 'Arial'
|
678
|
+
}.merge(options)
|
679
|
+
assert_valid_keys(options, :name, :font, :path, :format)
|
106
680
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
#
|
112
|
-
# == Scopes
|
113
|
-
#
|
114
|
-
# This will automatically create a named scope called with_#{attribute}
|
115
|
-
# that will find all records that have the attribute set to a given value.
|
116
|
-
# For example,
|
117
|
-
#
|
118
|
-
# Switch.with_state('on') # => Finds all switches where the state is on
|
119
|
-
# Switch.with_states('on', 'off') # => Finds all switches where the state is either on or off
|
120
|
-
#
|
121
|
-
# *Note* that if class methods already exist with those names (i.e. "with_state"
|
122
|
-
# or "with_states"), then a scope will not be defined for that name.
|
123
|
-
def initialize(owner_class, attribute = 'state', options = {})
|
124
|
-
set_context(owner_class, options)
|
681
|
+
begin
|
682
|
+
# Load the graphviz library
|
683
|
+
require 'rubygems'
|
684
|
+
require 'graphviz'
|
125
685
|
|
126
|
-
|
127
|
-
@states = []
|
128
|
-
@events = {}
|
686
|
+
graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
|
129
687
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
# with different machines
|
137
|
-
def initialize_copy(orig) #:nodoc:
|
138
|
-
super
|
688
|
+
# Add nodes
|
689
|
+
states.each do |state|
|
690
|
+
shape = state == @initial_state ? 'doublecircle' : 'circle'
|
691
|
+
state = state.is_a?(Proc) ? 'lambda' : state.to_s
|
692
|
+
graph.add_node(state, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
|
693
|
+
end
|
139
694
|
|
140
|
-
|
141
|
-
|
142
|
-
event
|
143
|
-
|
144
|
-
|
145
|
-
|
695
|
+
# Add edges
|
696
|
+
events.values.each do |event|
|
697
|
+
event.guards.each do |guard|
|
698
|
+
# From states: :from, everything but :except states, or all states
|
699
|
+
from_states = Array(guard.requirements[:from]) || guard.requirements[:except_from] && (states - Array(guard.requirements[:except_from])) || states
|
700
|
+
to_state = guard.requirements[:to]
|
701
|
+
to_state = to_state.is_a?(Proc) ? 'lambda' : to_state.to_s if to_state
|
702
|
+
|
703
|
+
from_states.each do |from_state|
|
704
|
+
from_state = from_state.to_s
|
705
|
+
graph.add_edge(from_state, to_state || from_state, :label => event.name, :fontname => options[:font])
|
706
|
+
end
|
707
|
+
end
|
146
708
|
end
|
709
|
+
|
710
|
+
# Generate the graph
|
711
|
+
graph.output
|
712
|
+
|
713
|
+
true
|
714
|
+
rescue LoadError
|
715
|
+
$stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
|
716
|
+
false
|
147
717
|
end
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
machine.set_context(owner_class, options)
|
154
|
-
machine
|
718
|
+
end
|
719
|
+
|
720
|
+
protected
|
721
|
+
# Runs additional initialization hooks. By default, this is a no-op.
|
722
|
+
def after_initialize
|
155
723
|
end
|
156
724
|
|
157
|
-
#
|
158
|
-
#
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
@owner_class = owner_class
|
163
|
-
@initial_state = options[:initial] if options[:initial]
|
725
|
+
# Gets the default action that should be invoked when performing a
|
726
|
+
# transition on the attribute for this machine. This may change
|
727
|
+
# depending on the configured integration for the owner class.
|
728
|
+
def default_action
|
164
729
|
end
|
165
730
|
|
166
|
-
#
|
167
|
-
# is
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
#
|
183
|
-
# With dynamic initial state:
|
184
|
-
#
|
185
|
-
# class Vehicle < ActiveRecord::Base
|
186
|
-
# state_machine :initial => lambda {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
|
187
|
-
# ...
|
188
|
-
# end
|
189
|
-
# end
|
190
|
-
#
|
191
|
-
# Vehicle.state_machines['state'].initial_state(@vehicle) # => "idling"
|
192
|
-
def initial_state(record)
|
193
|
-
@initial_state.is_a?(Proc) ? @initial_state.call(record) : @initial_state
|
731
|
+
# Adds reader/writer methods for accessing the attribute that this state
|
732
|
+
# machine is defined for.
|
733
|
+
def define_attribute_accessor
|
734
|
+
attribute = self.attribute
|
735
|
+
|
736
|
+
owner_class.class_eval do
|
737
|
+
attr_reader attribute unless method_defined?(attribute) || private_method_defined?(attribute)
|
738
|
+
attr_writer attribute unless method_defined?("#{attribute}=") || private_method_defined?("#{attribute}=")
|
739
|
+
|
740
|
+
# Checks whether the current state is a given value. If the value
|
741
|
+
# is not a known state, then an ArgumentError is raised.
|
742
|
+
define_method("#{attribute}?") do |state|
|
743
|
+
raise ArgumentError, "#{state.inspect} is not a known #{attribute} value" unless self.class.state_machines[attribute].states.include?(state)
|
744
|
+
send(attribute) == state
|
745
|
+
end unless method_defined?("#{attribute}?") || private_method_defined?("#{attribute}?")
|
746
|
+
end
|
194
747
|
end
|
195
748
|
|
196
|
-
# Defines
|
197
|
-
#
|
198
|
-
#
|
199
|
-
#
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
# * <tt>park!</tt> - Fires the "park" event, transitioning from the current state to the next valid state. If the transition cannot happen (for validation, database, etc. reasons), then an error will be raised.
|
204
|
-
# * <tt>can_park?</tt> - Checks whether the "park" event can be fired given the current state of the record.
|
205
|
-
#
|
206
|
-
# == Defining transitions
|
207
|
-
#
|
208
|
-
# +event+ requires a block which allows you to define the possible
|
209
|
-
# transitions that can happen as a result of that event. For example,
|
210
|
-
#
|
211
|
-
# event :park do
|
212
|
-
# transition :to => 'parked', :from => 'idle'
|
213
|
-
# end
|
214
|
-
#
|
215
|
-
# event :first_gear do
|
216
|
-
# transition :to => 'first_gear', :from => 'parked', :if => :seatbelt_on?
|
217
|
-
# end
|
218
|
-
#
|
219
|
-
# See PluginAWeek::StateMachine::Event#transition for more information on
|
220
|
-
# the possible options that can be passed in.
|
221
|
-
#
|
222
|
-
# *Note* that this block is executed within the context of the actual event
|
223
|
-
# object. As a result, you will not be able to reference any class methods
|
224
|
-
# on the model without referencing the class itself. For example,
|
225
|
-
#
|
226
|
-
# class Car < ActiveRecord::Base
|
227
|
-
# def self.safe_states
|
228
|
-
# %w(parked idling stalled)
|
229
|
-
# end
|
230
|
-
#
|
231
|
-
# state_machine :state do
|
232
|
-
# event :park do
|
233
|
-
# transition :to => 'parked', :from => Car.safe_states
|
234
|
-
# end
|
235
|
-
# end
|
236
|
-
# end
|
237
|
-
#
|
238
|
-
# == Example
|
239
|
-
#
|
240
|
-
# class Car < ActiveRecord::Base
|
241
|
-
# state_machine(:state, :initial => 'parked') do
|
242
|
-
# event :park, :after => :release_seatbelt do
|
243
|
-
# transition :to => 'parked', :from => %w(first_gear reverse)
|
244
|
-
# end
|
245
|
-
# ...
|
246
|
-
# end
|
247
|
-
# end
|
248
|
-
def event(name, &block)
|
249
|
-
name = name.to_s
|
250
|
-
event = events[name] ||= Event.new(self, name)
|
251
|
-
event.instance_eval(&block)
|
749
|
+
# Defines the with/without scope helpers for this attribute. Both the
|
750
|
+
# singular and plural versions of the attribute are defined for each
|
751
|
+
# scope helper. A custom plural can be specified if it cannot be
|
752
|
+
# automatically determined by either calling +pluralize+ on the attribute
|
753
|
+
# name or adding an "s" to the end of the name.
|
754
|
+
def define_scopes(custom_plural = nil)
|
755
|
+
plural = custom_plural || (attribute.respond_to?(:pluralize) ? attribute.pluralize : "#{attribute}s")
|
252
756
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
@states |= [transition.options[:to]] + Array(transition.options[:from]) + Array(transition.options[:except_from])
|
257
|
-
@states.sort!
|
757
|
+
[attribute, plural].uniq.each do |name|
|
758
|
+
define_with_scope("with_#{name}") unless owner_class.respond_to?("with_#{name}")
|
759
|
+
define_without_scope("without_#{name}") unless owner_class.respond_to?("without_#{name}")
|
258
760
|
end
|
259
|
-
|
260
|
-
event
|
261
761
|
end
|
262
762
|
|
263
|
-
#
|
264
|
-
#
|
265
|
-
# Each part of the transition (to state, from state, and event) must match
|
266
|
-
# in order for the callback to get invoked.
|
267
|
-
#
|
268
|
-
# Configuration options:
|
269
|
-
# * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
|
270
|
-
# * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
|
271
|
-
# * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
|
272
|
-
# * +except_to+ - One more states *not* being transitioned to
|
273
|
-
# * +except_from+ - One or more states *not* being transitioned from
|
274
|
-
# * +except_on+ - One or more events that *did not* fire the transition
|
275
|
-
# * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
|
276
|
-
# * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
|
277
|
-
# * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
|
278
|
-
#
|
279
|
-
# The +except+ group of options (+except_to+, +exception_from+, and
|
280
|
-
# +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
|
281
|
-
# +from+, and +on+, respectively)
|
282
|
-
#
|
283
|
-
# == The callback
|
284
|
-
#
|
285
|
-
# When defining additional configuration options, callbacks must be defined
|
286
|
-
# in the :do option like so:
|
763
|
+
# Defines a scope for finding objects *with* a particular value or
|
764
|
+
# values for the attribute.
|
287
765
|
#
|
288
|
-
#
|
289
|
-
|
290
|
-
# before_transition :to => 'parked', :do => :set_alarm
|
291
|
-
# ...
|
292
|
-
# end
|
293
|
-
# end
|
294
|
-
#
|
295
|
-
# == Examples
|
296
|
-
#
|
297
|
-
# Below is an example of a model with one state machine and various types
|
298
|
-
# of +before+ transitions defined for it:
|
299
|
-
#
|
300
|
-
# class Vehicle < ActiveRecord::Base
|
301
|
-
# state_machine do
|
302
|
-
# # Before all transitions
|
303
|
-
# before_transition :update_dashboard
|
304
|
-
#
|
305
|
-
# # Before specific transition:
|
306
|
-
# before_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
|
307
|
-
#
|
308
|
-
# # With conditional callback:
|
309
|
-
# before_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
|
310
|
-
#
|
311
|
-
# # Using :except counterparts:
|
312
|
-
# before_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
|
313
|
-
# ...
|
314
|
-
# end
|
315
|
-
# end
|
316
|
-
#
|
317
|
-
# As can be seen, any number of transitions can be created using various
|
318
|
-
# combinations of configuration options.
|
319
|
-
def before_transition(options = {})
|
320
|
-
add_transition_callback(:before, options)
|
766
|
+
# This is only applicable to specific integrations.
|
767
|
+
def define_with_scope(name)
|
321
768
|
end
|
322
769
|
|
323
|
-
#
|
324
|
-
#
|
325
|
-
# Each part of the transition (to state, from state, and event) must match
|
326
|
-
# in order for the callback to get invoked.
|
327
|
-
#
|
328
|
-
# Configuration options:
|
329
|
-
# * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
|
330
|
-
# * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
|
331
|
-
# * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
|
332
|
-
# * +except_to+ - One more states *not* being transitioned to
|
333
|
-
# * +except_from+ - One or more states *not* being transitioned from
|
334
|
-
# * +except_on+ - One or more events that *did not* fire the transition
|
335
|
-
# * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
|
336
|
-
# * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
|
337
|
-
# * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
|
338
|
-
#
|
339
|
-
# The +except+ group of options (+except_to+, +exception_from+, and
|
340
|
-
# +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
|
341
|
-
# +from+, and +on+, respectively)
|
342
|
-
#
|
343
|
-
# == The callback
|
344
|
-
#
|
345
|
-
# When defining additional configuration options, callbacks must be defined
|
346
|
-
# in the :do option like so:
|
347
|
-
#
|
348
|
-
# class Vehicle < ActiveRecord::Base
|
349
|
-
# state_machine do
|
350
|
-
# after_transition :to => 'parked', :do => :set_alarm
|
351
|
-
# ...
|
352
|
-
# end
|
353
|
-
# end
|
354
|
-
#
|
355
|
-
# == Examples
|
770
|
+
# Defines a scope for finding objects *without* a particular value or
|
771
|
+
# values for the attribute.
|
356
772
|
#
|
357
|
-
#
|
358
|
-
|
359
|
-
#
|
360
|
-
# class Vehicle < ActiveRecord::Base
|
361
|
-
# state_machine do
|
362
|
-
# # After all transitions
|
363
|
-
# after_transition :update_dashboard
|
364
|
-
#
|
365
|
-
# # After specific transition:
|
366
|
-
# after_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
|
367
|
-
#
|
368
|
-
# # With conditional callback:
|
369
|
-
# after_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
|
370
|
-
#
|
371
|
-
# # Using :except counterparts:
|
372
|
-
# after_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
|
373
|
-
# ...
|
374
|
-
# end
|
375
|
-
# end
|
376
|
-
#
|
377
|
-
# As can be seen, any number of transitions can be created using various
|
378
|
-
# combinations of configuration options.
|
379
|
-
def after_transition(options = {})
|
380
|
-
add_transition_callback(:after, options)
|
773
|
+
# This is only applicable to specific integrations.
|
774
|
+
def define_without_scope(name)
|
381
775
|
end
|
382
776
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
# Add before/after callbacks for when the attribute transitions to a
|
397
|
-
# different value
|
398
|
-
def add_transition_callbacks
|
399
|
-
%w(before after).each {|type| owner_class.define_callbacks("#{type}_transition_#{attribute}") }
|
400
|
-
end
|
777
|
+
# Adds a new transition callback of the given type.
|
778
|
+
def add_callback(type, options, &block)
|
779
|
+
@callbacks[type] << callback = Callback.new(options, &block)
|
780
|
+
add_states(callback.known_states)
|
781
|
+
callback
|
782
|
+
end
|
783
|
+
|
784
|
+
# Tracks the given set of states in the list of all known states for
|
785
|
+
# this machine
|
786
|
+
def add_states(states)
|
787
|
+
new_states = states - @states
|
788
|
+
@states += new_states
|
401
789
|
|
402
|
-
# Add
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
name = "
|
407
|
-
|
790
|
+
# Add state predicates
|
791
|
+
attribute = self.attribute
|
792
|
+
new_states.each do |state|
|
793
|
+
if state.is_a?(String) || state.is_a?(Symbol)
|
794
|
+
name = "#{state}?"
|
795
|
+
|
796
|
+
owner_class.class_eval do
|
797
|
+
# Checks whether the current state is equal to the given value
|
798
|
+
define_method(name) do
|
799
|
+
self.send(attribute) == state
|
800
|
+
end unless method_defined?(name) || private_method_defined?(name)
|
801
|
+
end
|
408
802
|
end
|
409
803
|
end
|
410
|
-
|
804
|
+
end
|
411
805
|
end
|
412
806
|
end
|