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
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
|