state_machine 0.3.1 → 0.4.0
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 +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
|