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
Binary file
|
Binary file
|
Binary file
|
data/lib/state_machine.rb
CHANGED
@@ -1,123 +1,168 @@
|
|
1
1
|
require 'state_machine/machine'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
3
|
+
# A state machine is a model of behavior composed of states, events, and
|
4
|
+
# transitions. This helper adds support for defining this type of
|
5
|
+
# functionality on any Ruby class.
|
6
|
+
module StateMachine
|
7
|
+
module MacroMethods
|
8
|
+
# Creates a new state machine for the given attribute. The default
|
9
|
+
# attribute, if not specified, is "state".
|
10
|
+
#
|
11
|
+
# Configuration options:
|
12
|
+
# * +initial+ - The initial value to set the attribute to. This can be a static value or a dynamic proc which will be evaluated at runtime. Default is nil.
|
13
|
+
# * +action+ - The action to invoke when an object transitions. Default is nil unless otherwise specified by the configured integration.
|
14
|
+
# * +plural+ - The pluralized name of the attribute. By default, this will attempt to call +pluralize+ on the attribute, otherwise an "s" is appended.
|
15
|
+
# * +integration+ - The name of the integration to use for adding library-specific behavior to the machine. Built-in integrations include :data_mapper and :active_record. By default, this is determined automatically.
|
16
|
+
#
|
17
|
+
# This also requires a block which will be used to actually configure the
|
18
|
+
# events and transitions for the state machine. *Note* that this block
|
19
|
+
# will be executed within the context of the state machine. As a result,
|
20
|
+
# you will not be able to access any class methods unless you refer to
|
21
|
+
# them directly (i.e. specifying the class name).
|
22
|
+
#
|
23
|
+
# For examples on the types of configured state machines and blocks, see
|
24
|
+
# the section below.
|
25
|
+
#
|
26
|
+
# == Examples
|
27
|
+
#
|
28
|
+
# With the default attribute and no configuration:
|
29
|
+
#
|
30
|
+
# class Vehicle
|
31
|
+
# state_machine do
|
32
|
+
# event :park do
|
33
|
+
# ...
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# The above example will define a state machine for the attribute "state"
|
39
|
+
# on the class. Every vehicle will start without an initial state.
|
40
|
+
#
|
41
|
+
# With a custom attribute:
|
42
|
+
#
|
43
|
+
# class Vehicle
|
44
|
+
# state_machine :status do
|
45
|
+
# ...
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# With a static initial state:
|
50
|
+
#
|
51
|
+
# class Vehicle
|
52
|
+
# state_machine :status, :initial => 'Vehicle' do
|
53
|
+
# ...
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# With a dynamic initial state:
|
58
|
+
#
|
59
|
+
# class Switch
|
60
|
+
# state_machine :status, :initial => lambda {|switch| (8..22).include?(Time.now.hour) ? 'on' : 'off'} do
|
61
|
+
# ...
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# == Attribute accessor
|
66
|
+
#
|
67
|
+
# The attribute for each machine stores the value for the current state
|
68
|
+
# of the machine. In order to access this value and modify it during
|
69
|
+
# transitions, a reader/writer must be available. The following methods
|
70
|
+
# will be automatically generated if they are not already defined
|
71
|
+
# (assuming the attribute is called "state"):
|
72
|
+
# * <tt>state</tt> - Gets the current value for the attribute
|
73
|
+
# * <tt>state=(value)</tt> - Sets the current value for the attribute
|
74
|
+
# * <tt>state?(value)</tt> - Checks the given value against the current value. If the value is not a known state, then an ArgumentError is raised.
|
75
|
+
#
|
76
|
+
# For example, the following machine definition will not generate any
|
77
|
+
# accessor methods since the class has already defined an attribute
|
78
|
+
# accessor:
|
79
|
+
#
|
80
|
+
# class Vehicle
|
81
|
+
# attr_accessor :state
|
82
|
+
#
|
83
|
+
# state_machine do
|
84
|
+
# ...
|
85
|
+
# end
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# On the other hand, the following state machine will define both a
|
89
|
+
# reader and writer method, which is functionally equivalent to the
|
90
|
+
# example above:
|
91
|
+
#
|
92
|
+
# class Vehicle
|
93
|
+
# state_machine do
|
94
|
+
# ...
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# == States
|
99
|
+
#
|
100
|
+
# All of the valid states for the machine are automatically tracked based
|
101
|
+
# on the events, transitions, and callbacks defined for the machine. If
|
102
|
+
# there are additional states that are never referenced, these should be
|
103
|
+
# explicitly added using the StateMachine::Machine#other_states
|
104
|
+
# helper.
|
105
|
+
#
|
106
|
+
# For each state tracked, a predicate method for that state is generated
|
107
|
+
# on the class. For example,
|
108
|
+
#
|
109
|
+
# class Vehicle
|
110
|
+
# state_machine :initial => 'parked' do
|
111
|
+
# event :ignite do
|
112
|
+
# transition :to => 'idling'
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# ...will generate the following instance methods (assuming they're not
|
118
|
+
# already defined in the class):
|
119
|
+
# * <tt>parked?</tt>
|
120
|
+
# * <tt>idling?</tt>
|
121
|
+
#
|
122
|
+
# Each predicate method will return true if it matches the object's
|
123
|
+
# current state. Otherwise, it will return false.
|
124
|
+
#
|
125
|
+
# == Events and Transitions
|
126
|
+
#
|
127
|
+
# For more information about how to configure an event and its associated
|
128
|
+
# transitions, see StateMachine::Machine#event.
|
129
|
+
#
|
130
|
+
# == Defining callbacks
|
131
|
+
#
|
132
|
+
# Within the +state_machine+ block, you can also define callbacks for
|
133
|
+
# particular states. For more information about defining these callbacks,
|
134
|
+
# see StateMachine::Machine#before_transition and
|
135
|
+
# StateMachine::Machine#after_transition.
|
136
|
+
#
|
137
|
+
# == Scopes
|
138
|
+
#
|
139
|
+
# For integrations that support it, a group of default scope filters will
|
140
|
+
# be automatically created for assisting in finding objects that have the
|
141
|
+
# attribute set to a given value.
|
142
|
+
#
|
143
|
+
# For example,
|
144
|
+
#
|
145
|
+
# Vehicle.with_state('parked') # => Finds all vehicles where the state is parked
|
146
|
+
# Vehicle.with_states('parked', 'idling') # => Finds all vehicles where the state is either parked or idling
|
147
|
+
#
|
148
|
+
# Vehicle.without_state('parked') # => Finds all vehicles where the state is *not* parked
|
149
|
+
# Vehicle.without_states('parked', 'idling') # => Finds all vehicles where the state is *not* parked or idling
|
150
|
+
#
|
151
|
+
# *Note* that if class methods already exist with those names (i.e.
|
152
|
+
# "with_state", "with_states", "without_state", or "without_states"), then
|
153
|
+
# a scope will not be defined for that name.
|
154
|
+
#
|
155
|
+
# See StateMachine::Machine for more information about using
|
156
|
+
# integrations and the individual integration docs for information about
|
157
|
+
# the actual scopes that are generated.
|
158
|
+
def state_machine(*args, &block)
|
159
|
+
machine = StateMachine::Machine.find_or_create(self, *args)
|
160
|
+
machine.instance_eval(&block) if block
|
161
|
+
machine
|
117
162
|
end
|
118
163
|
end
|
119
164
|
end
|
120
165
|
|
121
|
-
|
122
|
-
|
166
|
+
Class.class_eval do
|
167
|
+
include StateMachine::MacroMethods
|
123
168
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Provides a set of helper methods for making assertions about content of
|
3
|
+
# various objects
|
4
|
+
module Assertions
|
5
|
+
# Validates that all keys in the given hash *only* includes the specified
|
6
|
+
# valid keys. If any invalid keys are found, an ArgumentError will be
|
7
|
+
# raised.
|
8
|
+
#
|
9
|
+
# == Examples
|
10
|
+
#
|
11
|
+
# options = {:name => 'John Smith', :age => 30}
|
12
|
+
#
|
13
|
+
# assert_valid_keys(options, :name) # => ArgumentError: Invalid key(s): age
|
14
|
+
# assert_valid_keys(options, 'name', 'age') # => ArgumentError: Invalid key(s): age, name
|
15
|
+
# assert_valid_keys(options, :name, :age) # => nil
|
16
|
+
def assert_valid_keys(hash, *valid_keys)
|
17
|
+
invalid_keys = hash.keys - valid_keys
|
18
|
+
raise ArgumentError, "Invalid key(s): #{invalid_keys.join(", ")}" unless invalid_keys.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'state_machine/guard'
|
2
|
+
require 'state_machine/eval_helpers'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# Callbacks represent hooks into objects that allow you to trigger logic
|
6
|
+
# before or after a specific transition occurs.
|
7
|
+
class Callback
|
8
|
+
include EvalHelpers
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Whether to automatically bind the callback to the object being
|
12
|
+
# transitioned. This only applies to callbacks that are defined as
|
13
|
+
# lambda blocks (or Procs). Some libraries, such as Extlib, handle
|
14
|
+
# callbacks by executing them bound to the object involved, while other
|
15
|
+
# libraries, such as ActiveSupport, pass the object as an argument to
|
16
|
+
# the callback. This can be configured on an application-wide basis by
|
17
|
+
# setting this configuration to +true+ or +false+. The default value
|
18
|
+
# is +false+.
|
19
|
+
#
|
20
|
+
# *Note* that the DataMapper and Sequel integrations automatically
|
21
|
+
# configure this value on a per-callback basis, so it does not have to
|
22
|
+
# be enabled application-wide.
|
23
|
+
#
|
24
|
+
# == Examples
|
25
|
+
#
|
26
|
+
# When not bound to the object:
|
27
|
+
#
|
28
|
+
# class Vehicle
|
29
|
+
# state_machine do
|
30
|
+
# before_transition do |vehicle|
|
31
|
+
# vehicle.set_alarm
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def set_alarm
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# When bound to the object application-wide:
|
41
|
+
#
|
42
|
+
# StateMachine::Callback.bind_to_object = true
|
43
|
+
#
|
44
|
+
# class Vehicle
|
45
|
+
# state_machine do
|
46
|
+
# before_transition do
|
47
|
+
# self.set_alarm
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def set_alarm
|
52
|
+
# ...
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
attr_accessor :bind_to_object
|
56
|
+
end
|
57
|
+
|
58
|
+
# An optional block for determining whether to cancel the callback chain
|
59
|
+
# based on the return value of the callback. By default, the callback
|
60
|
+
# chain never cancels based on the return value (i.e. there is no implicit
|
61
|
+
# terminator). Certain integrations, such as ActiveRecord, change this
|
62
|
+
# default value.
|
63
|
+
#
|
64
|
+
# == Examples
|
65
|
+
#
|
66
|
+
# Canceling the callback chain without a terminator:
|
67
|
+
#
|
68
|
+
# class Vehicle
|
69
|
+
# state_machine do
|
70
|
+
# before_transition do |vehicle|
|
71
|
+
# throw :halt
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# Canceling the callback chain with a terminator value of +false+:
|
77
|
+
#
|
78
|
+
# class Vehicle
|
79
|
+
# state_machine do
|
80
|
+
# before_transition do |vehicle|
|
81
|
+
# false
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
attr_reader :terminator
|
86
|
+
|
87
|
+
# The guard that determines whether or not this callback can be invoked
|
88
|
+
# based on the context of the transition. The event, from state, and
|
89
|
+
# to state must all match in order for the guard to pass.
|
90
|
+
#
|
91
|
+
# See StateMachine::Guard for more information.
|
92
|
+
attr_reader :guard
|
93
|
+
|
94
|
+
# Creates a new callback that can get called based on the configured
|
95
|
+
# options.
|
96
|
+
#
|
97
|
+
# In addition to the possible configuration options for guards, the
|
98
|
+
# following options can be configured:
|
99
|
+
# * +bind_to_object+ - Whether to bind the callback to the object involved. If set to false, the object will be passed as a parameter instead. Default is integration-specific or set to the application default.
|
100
|
+
# * +terminator+ - A block/proc that determines what callback results should cause the callback chain to halt (if not using the default <tt>throw :halt</tt> technique).
|
101
|
+
#
|
102
|
+
# More information about how those options affect the behavior of the
|
103
|
+
# callback can be found in their attr_accessor definitions.
|
104
|
+
def initialize(options = {}, &block) #:nodoc:
|
105
|
+
if options.is_a?(Hash)
|
106
|
+
@method = options.delete(:do) || block
|
107
|
+
else
|
108
|
+
# Only the callback was configured
|
109
|
+
@method = options
|
110
|
+
options = {}
|
111
|
+
end
|
112
|
+
|
113
|
+
# The actual method to invoke must be defined
|
114
|
+
raise ArgumentError, ':do callback must be specified' unless @method
|
115
|
+
|
116
|
+
# Proxy the method so that it's bound to the object
|
117
|
+
@method = bound_method(@method) if @method.is_a?(Proc) && (!options.include?(:bind_to_object) && self.class.bind_to_object || options.delete(:bind_to_object))
|
118
|
+
@terminator = options.delete(:terminator)
|
119
|
+
|
120
|
+
@guard = Guard.new(options)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Gets a list of the states known to this callback by looking at the
|
124
|
+
# guard's requirements
|
125
|
+
def known_states
|
126
|
+
guard.known_states
|
127
|
+
end
|
128
|
+
|
129
|
+
# Runs the callback as long as the transition context matches the guard
|
130
|
+
# requirements configured for this callback.
|
131
|
+
def call(object, context = {}, *args)
|
132
|
+
# Only evaluate the method if the guard passes
|
133
|
+
if @guard.matches?(object, context)
|
134
|
+
result = evaluate_method(object, @method, *args)
|
135
|
+
|
136
|
+
# If a terminator has been configured and it matches the result from
|
137
|
+
# the evaluated method, then the callback chain should be halted
|
138
|
+
if @terminator && @terminator.call(result)
|
139
|
+
throw :halt
|
140
|
+
else
|
141
|
+
result
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
# Generates an method that can be bound to the object being transitioned
|
148
|
+
# when the callback is invoked
|
149
|
+
def bound_method(block)
|
150
|
+
# Generate a thread-safe unbound method that can be used on any object
|
151
|
+
unbound_method = Object.class_eval do
|
152
|
+
time = Time.now
|
153
|
+
method_name = "__bind_#{time.to_i}_#{time.usec}"
|
154
|
+
define_method(method_name, &block)
|
155
|
+
method = instance_method(method_name)
|
156
|
+
remove_method(method_name)
|
157
|
+
method
|
158
|
+
end
|
159
|
+
arity = unbound_method.arity
|
160
|
+
|
161
|
+
# Proxy calls to the method so that the method can be bound *and*
|
162
|
+
# the arguments are adjusted
|
163
|
+
lambda do |object, *args|
|
164
|
+
unbound_method.bind(object).call(*(arity == 0 ? [] : args))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|