state_machine 0.4.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +17 -0
- data/LICENSE +1 -1
- data/README.rdoc +54 -84
- data/Rakefile +1 -1
- data/examples/Car_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +35 -0
- data/lib/state_machine.rb +65 -52
- data/lib/state_machine/assertions.rb +1 -1
- data/lib/state_machine/callback.rb +13 -9
- data/lib/state_machine/eval_helpers.rb +4 -3
- data/lib/state_machine/event.rb +51 -33
- data/lib/state_machine/extensions.rb +2 -2
- data/lib/state_machine/guard.rb +47 -41
- data/lib/state_machine/integrations.rb +67 -0
- data/lib/state_machine/integrations/active_record.rb +62 -36
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +23 -37
- data/lib/state_machine/integrations/data_mapper/observer.rb +23 -9
- data/lib/state_machine/integrations/sequel.rb +23 -24
- data/lib/state_machine/machine.rb +380 -277
- data/lib/state_machine/node_collection.rb +142 -0
- data/lib/state_machine/state.rb +114 -69
- data/lib/state_machine/state_collection.rb +38 -0
- data/lib/state_machine/transition.rb +36 -17
- data/test/active_record.log +2940 -85664
- data/test/functional/state_machine_test.rb +49 -53
- data/test/sequel.log +747 -11990
- data/test/unit/assertions_test.rb +2 -1
- data/test/unit/callback_test.rb +14 -12
- data/test/unit/eval_helpers_test.rb +25 -6
- data/test/unit/event_test.rb +144 -124
- data/test/unit/guard_test.rb +118 -140
- data/test/unit/integrations/active_record_test.rb +102 -68
- data/test/unit/integrations/data_mapper_test.rb +48 -37
- data/test/unit/integrations/sequel_test.rb +34 -25
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/machine_test.rb +460 -531
- data/test/unit/node_collection_test.rb +208 -0
- data/test/unit/state_collection_test.rb +167 -0
- data/test/unit/state_machine_test.rb +1 -1
- data/test/unit/state_test.rb +223 -200
- data/test/unit/transition_test.rb +81 -46
- metadata +17 -3
- data/test/data_mapper.log +0 -30860
@@ -15,7 +15,7 @@ module StateMachine
|
|
15
15
|
# assert_valid_keys(options, :name, :age) # => nil
|
16
16
|
def assert_valid_keys(hash, *valid_keys)
|
17
17
|
invalid_keys = hash.keys - valid_keys
|
18
|
-
raise ArgumentError, "Invalid key(s): #{invalid_keys.join(
|
18
|
+
raise ArgumentError, "Invalid key(s): #{invalid_keys.join(', ')}" unless invalid_keys.empty?
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -8,11 +8,11 @@ module StateMachine
|
|
8
8
|
include EvalHelpers
|
9
9
|
|
10
10
|
class << self
|
11
|
-
#
|
11
|
+
# Determines whether to automatically bind the callback to the object being
|
12
12
|
# transitioned. This only applies to callbacks that are defined as
|
13
|
-
# lambda blocks (or Procs). Some
|
13
|
+
# lambda blocks (or Procs). Some integrations, such as DataMapper, handle
|
14
14
|
# callbacks by executing them bound to the object involved, while other
|
15
|
-
#
|
15
|
+
# integrations, such as ActiveRecord, pass the object as an argument to
|
16
16
|
# the callback. This can be configured on an application-wide basis by
|
17
17
|
# setting this configuration to +true+ or +false+. The default value
|
18
18
|
# is +false+.
|
@@ -37,7 +37,7 @@ module StateMachine
|
|
37
37
|
# end
|
38
38
|
# end
|
39
39
|
#
|
40
|
-
# When bound to the object
|
40
|
+
# When bound to the object:
|
41
41
|
#
|
42
42
|
# StateMachine::Callback.bind_to_object = true
|
43
43
|
#
|
@@ -96,12 +96,16 @@ module StateMachine
|
|
96
96
|
#
|
97
97
|
# In addition to the possible configuration options for guards, the
|
98
98
|
# following options can be configured:
|
99
|
-
# *
|
100
|
-
#
|
99
|
+
# * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
|
100
|
+
# If set to false, the object will be passed as a parameter instead.
|
101
|
+
# Default is integration-specific or set to the application default.
|
102
|
+
# * <tt>:terminator</tt> - A block/proc that determines what callback results
|
103
|
+
# should cause the callback chain to halt (if not using the default
|
104
|
+
# <tt>throw :halt</tt> technique).
|
101
105
|
#
|
102
106
|
# More information about how those options affect the behavior of the
|
103
|
-
# callback can be found in their
|
104
|
-
def initialize(options = {}, &block)
|
107
|
+
# callback can be found in their attribute definitions.
|
108
|
+
def initialize(options = {}, &block)
|
105
109
|
if options.is_a?(Hash)
|
106
110
|
@method = options.delete(:do) || block
|
107
111
|
else
|
@@ -146,7 +150,7 @@ module StateMachine
|
|
146
150
|
end
|
147
151
|
|
148
152
|
private
|
149
|
-
# Generates
|
153
|
+
# Generates a method that can be bound to the object being transitioned
|
150
154
|
# when the callback is invoked
|
151
155
|
def bound_method(block)
|
152
156
|
# Generate a thread-safe unbound method that can be used on any object
|
@@ -55,12 +55,13 @@ module StateMachine
|
|
55
55
|
when Symbol
|
56
56
|
method = object.method(method)
|
57
57
|
method.arity == 0 ? method.call : method.call(*args)
|
58
|
+
when Proc, Method
|
59
|
+
args.unshift(object)
|
60
|
+
[0, 1].include?(method.arity) ? method.call(*args.slice(0, method.arity)) : method.call(*args)
|
58
61
|
when String
|
59
62
|
eval(method, object.instance_eval {binding})
|
60
|
-
when Proc, Method
|
61
|
-
method.arity == 1 ? method.call(object) : method.call(object, *args)
|
62
63
|
else
|
63
|
-
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a
|
64
|
+
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
64
65
|
end
|
65
66
|
end
|
66
67
|
end
|
data/lib/state_machine/event.rb
CHANGED
@@ -34,7 +34,7 @@ module StateMachine
|
|
34
34
|
end
|
35
35
|
|
36
36
|
# Creates a copy of this event in addition to the list of associated
|
37
|
-
# guards to prevent conflicts across
|
37
|
+
# guards to prevent conflicts across events within a class hierarchy.
|
38
38
|
def initialize_copy(orig) #:nodoc:
|
39
39
|
super
|
40
40
|
@guards = @guards.dup
|
@@ -44,11 +44,18 @@ module StateMachine
|
|
44
44
|
# Creates a new transition that will be evaluated when the event is fired.
|
45
45
|
#
|
46
46
|
# Configuration options:
|
47
|
-
# *
|
48
|
-
#
|
49
|
-
# *
|
50
|
-
#
|
51
|
-
# *
|
47
|
+
# * <tt>:from</tt> - A state or array of states that can be transitioned from.
|
48
|
+
# If not specified, then the transition can occur for *any* state.
|
49
|
+
# * <tt>:to</tt> - The state that's being transitioned to. If not specified,
|
50
|
+
# then the transition will simply loop back (i.e. the state will not change).
|
51
|
+
# * <tt>:except_from</tt> - A state or array of states that *cannot* be
|
52
|
+
# transitioned from.
|
53
|
+
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
54
|
+
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
|
55
|
+
# The condition should return or evaluate to true or false.
|
56
|
+
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
57
|
+
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
|
58
|
+
# The condition should return or evaluate to true or false.
|
52
59
|
#
|
53
60
|
# == Order of operations
|
54
61
|
#
|
@@ -56,31 +63,20 @@ module StateMachine
|
|
56
63
|
# result, if more than one transition applies to a given object, then the
|
57
64
|
# first transition that matches will be performed.
|
58
65
|
#
|
59
|
-
# == Dynamic states
|
60
|
-
#
|
61
|
-
# There is limited support for using dynamically generated values for the
|
62
|
-
# +to+ state in transitions. This is especially useful for times where
|
63
|
-
# the machine attribute represents a Time object. In order to have a
|
64
|
-
# a transition be made to the current time, a lambda block can be passed
|
65
|
-
# in representing the state, such as:
|
66
|
-
#
|
67
|
-
# transition :to => lambda {Time.now}
|
68
|
-
#
|
69
66
|
# == Examples
|
70
67
|
#
|
71
|
-
# transition :from => nil, :to =>
|
72
|
-
# transition :from =>
|
73
|
-
# transition :except_from =>
|
68
|
+
# transition :from => nil, :to => :parked
|
69
|
+
# transition :from => [:first_gear, :reverse]
|
70
|
+
# transition :except_from => :parked
|
74
71
|
# transition :to => nil
|
75
|
-
# transition :to =>
|
76
|
-
# transition :to =>
|
77
|
-
# transition :to =>
|
78
|
-
# transition :to =>
|
79
|
-
# transition :to =>
|
80
|
-
# transition :to =>
|
81
|
-
# transition :to => 'parked', :except_from => 'parked'
|
72
|
+
# transition :to => :parked
|
73
|
+
# transition :to => :parked, :from => :first_gear
|
74
|
+
# transition :to => :parked, :from => [:first_gear, :reverse]
|
75
|
+
# transition :to => :parked, :from => :first_gear, :if => :moving?
|
76
|
+
# transition :to => :parked, :from => :first_gear, :unless => :stopped?
|
77
|
+
# transition :to => :parked, :except_from => :parked
|
82
78
|
def transition(options)
|
83
|
-
assert_valid_keys(options, :
|
79
|
+
assert_valid_keys(options, :from, :to, :except_from, :if, :unless)
|
84
80
|
|
85
81
|
guards << guard = Guard.new(options)
|
86
82
|
@known_states |= guard.known_states
|
@@ -98,12 +94,11 @@ module StateMachine
|
|
98
94
|
# Finds and builds the next transition that can be performed on the given
|
99
95
|
# object. If no transitions can be made, then this will return nil.
|
100
96
|
def next_transition(object)
|
101
|
-
from =
|
97
|
+
from = machine.state_for(object).name
|
102
98
|
|
103
99
|
if guard = guards.find {|guard| guard.matches?(object, :from => from)}
|
104
100
|
# Guard allows for the transition to occur
|
105
101
|
to = guard.requirements[:to] ? guard.requirements[:to].first : from
|
106
|
-
to = to.call if to.is_a?(Proc)
|
107
102
|
Transition.new(object, machine, name, from, to)
|
108
103
|
end
|
109
104
|
end
|
@@ -111,6 +106,9 @@ module StateMachine
|
|
111
106
|
# Attempts to perform the next available transition on the given object.
|
112
107
|
# If no transitions can be made, then this will return false, otherwise
|
113
108
|
# true.
|
109
|
+
#
|
110
|
+
# Any additional arguments are passed to the StateMachine::Transition#perform
|
111
|
+
# instance method.
|
114
112
|
def fire(object, *args)
|
115
113
|
if transition = next_transition(object)
|
116
114
|
transition.perform(*args)
|
@@ -119,16 +117,35 @@ module StateMachine
|
|
119
117
|
end
|
120
118
|
end
|
121
119
|
|
120
|
+
# Attempts to perform the next available transition on the given object.
|
121
|
+
# If no transitions can be made, then a StateMachine::InvalidTransition
|
122
|
+
# exception will be raised, otherwise true will be returned.
|
123
|
+
def fire!(object, *args)
|
124
|
+
fire(object, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{machine.attribute} via :#{name} from #{machine.state_for(object).name.inspect}")
|
125
|
+
end
|
126
|
+
|
122
127
|
# Draws a representation of this event on the given graph. This will
|
123
128
|
# create 1 or more edges on the graph for each guard (i.e. transition)
|
124
129
|
# configured.
|
125
130
|
#
|
126
131
|
# A collection of the generated edges will be returned.
|
127
132
|
def draw(graph)
|
128
|
-
valid_states = machine.
|
133
|
+
valid_states = machine.states.by_priority.map {|state| state.name}
|
129
134
|
guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
|
130
135
|
end
|
131
136
|
|
137
|
+
# Generates a nicely formatted description of this events's contents.
|
138
|
+
#
|
139
|
+
# For example,
|
140
|
+
#
|
141
|
+
# event = StateMachine::Event.new(machine, :park)
|
142
|
+
# event.transition :to => :parked, :from => :idling
|
143
|
+
# event # => #<StateMachine::Event name=:park transitions=[{:to => [:parked], :from => [:idling]}]>
|
144
|
+
def inspect
|
145
|
+
attributes = [[:name, name], [:transitions, guards.map {|guard| guard.requirements}]]
|
146
|
+
"#<#{self.class} #{attributes.map {|name, value| "#{name}=#{value.inspect}"} * ' '}>"
|
147
|
+
end
|
148
|
+
|
132
149
|
protected
|
133
150
|
# Add the various instance methods that can transition the object using
|
134
151
|
# the current event
|
@@ -143,7 +160,8 @@ module StateMachine
|
|
143
160
|
self.class.state_machines[attribute].event(name).can_fire?(self)
|
144
161
|
end
|
145
162
|
|
146
|
-
# Gets the next transition that would be performed if the event were
|
163
|
+
# Gets the next transition that would be performed if the event were
|
164
|
+
# fired now
|
147
165
|
define_method("next_#{qualified_name}_transition") do
|
148
166
|
self.class.state_machines[attribute].event(name).next_transition(self)
|
149
167
|
end
|
@@ -153,9 +171,9 @@ module StateMachine
|
|
153
171
|
self.class.state_machines[attribute].event(name).fire(self, *args)
|
154
172
|
end
|
155
173
|
|
156
|
-
# Fires the event, raising an exception if it fails
|
174
|
+
# Fires the event, raising an exception if it fails
|
157
175
|
define_method("#{qualified_name}!") do |*args|
|
158
|
-
|
176
|
+
self.class.state_machines[attribute].event(name).fire!(self, *args)
|
159
177
|
end
|
160
178
|
end
|
161
179
|
end
|
@@ -13,7 +13,7 @@ module StateMachine
|
|
13
13
|
#
|
14
14
|
# The hash of state machines maps +attribute+ => +machine+, e.g.
|
15
15
|
#
|
16
|
-
# Vehicle.state_machines # => {
|
16
|
+
# Vehicle.state_machines # => {:state => #<StateMachine::Machine:0xb6f6e4a4 ...>
|
17
17
|
def state_machines
|
18
18
|
@state_machines ||= superclass.state_machines.dup
|
19
19
|
end
|
@@ -35,7 +35,7 @@ module StateMachine
|
|
35
35
|
# Set the initial value of the machine's attribute unless it already
|
36
36
|
# exists (which must mean the defaults are being skipped)
|
37
37
|
value = send(attribute)
|
38
|
-
send("#{attribute}=", machine.initial_state(self)) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
38
|
+
send("#{attribute}=", machine.initial_state(self).value) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
data/lib/state_machine/guard.rb
CHANGED
@@ -10,32 +10,34 @@ module StateMachine
|
|
10
10
|
include Assertions
|
11
11
|
include EvalHelpers
|
12
12
|
|
13
|
-
# The transition/
|
14
|
-
#
|
13
|
+
# The transition/condition options that must be met in order for the guard
|
14
|
+
# to match
|
15
15
|
attr_reader :requirements
|
16
16
|
|
17
|
-
# A list of all of the states known to this guard. This will pull
|
18
|
-
#
|
19
|
-
# * +to+
|
17
|
+
# A list of all of the states known to this guard. This will pull states
|
18
|
+
# from the following requirements (in the same order):
|
20
19
|
# * +from+
|
21
|
-
# * +except_to+
|
22
20
|
# * +except_from+
|
21
|
+
# * +to+
|
22
|
+
# * +except_to+
|
23
23
|
attr_reader :known_states
|
24
24
|
|
25
25
|
# Creates a new guard with the given requirements
|
26
26
|
def initialize(requirements = {}) #:nodoc:
|
27
|
-
assert_valid_keys(requirements, :
|
27
|
+
assert_valid_keys(requirements, :from, :to, :on, :except_from, :except_to, :except_on, :if, :unless)
|
28
28
|
|
29
29
|
@requirements = requirements
|
30
30
|
@known_states = []
|
31
31
|
|
32
|
-
# Normalize the requirements and track known states
|
33
|
-
|
32
|
+
# Normalize the requirements and track known states. The order that
|
33
|
+
# requirements are iterated is based on the priority in which tracked
|
34
|
+
# states should be added (from followed by to states).
|
35
|
+
[:from, :except_from, :to, :except_to, :on, :except_on].each do |option|
|
34
36
|
if @requirements.include?(option)
|
35
37
|
values = @requirements[option]
|
36
38
|
|
37
39
|
@requirements[option] = values = [values] unless values.is_a?(Array)
|
38
|
-
@known_states |= values if [:
|
40
|
+
@known_states |= values if [:from, :to, :except_from, :except_to].include?(option)
|
39
41
|
end
|
40
42
|
end
|
41
43
|
end
|
@@ -43,34 +45,34 @@ module StateMachine
|
|
43
45
|
# Determines whether the given object / query matches the requirements
|
44
46
|
# configured for this guard. In addition to matching the event, from state,
|
45
47
|
# and to state, this will also check whether the configured :if/:unless
|
46
|
-
#
|
48
|
+
# conditions pass on the given object.
|
47
49
|
#
|
48
50
|
# Query options:
|
49
|
-
# * +to+ - One or more states being transitioned to. If none are specified, then this will always match.
|
50
51
|
# * +from+ - One or more states being transitioned from. If none are specified, then this will always match.
|
52
|
+
# * +to+ - One or more states being transitioned to. If none are specified, then this will always match.
|
51
53
|
# * +on+ - One or more events that fired the transition. If none are specified, then this will always match.
|
52
|
-
# * +except_to+ - One more states *not* being transitioned to
|
53
54
|
# * +except_from+ - One or more states *not* being transitioned from
|
55
|
+
# * +except_to+ - One more states *not* being transitioned to
|
54
56
|
# * +except_on+ - One or more events that *did not* fire the transition.
|
55
57
|
#
|
56
58
|
# == Examples
|
57
59
|
#
|
58
|
-
# guard = StateMachine::Guard.new(:on =>
|
60
|
+
# guard = StateMachine::Guard.new(:on => :ignite, :from => [nil, :parked], :to => :idling)
|
59
61
|
#
|
60
62
|
# # Successful
|
61
|
-
# guard.matches?(object, :on =>
|
62
|
-
# guard.matches?(object, :from => nil)
|
63
|
-
# guard.matches?(object, :from =>
|
64
|
-
# guard.matches?(object, :to =>
|
65
|
-
# guard.matches?(object, :from =>
|
66
|
-
# guard.matches?(object, :on =>
|
63
|
+
# guard.matches?(object, :on => :ignite) # => true
|
64
|
+
# guard.matches?(object, :from => nil) # => true
|
65
|
+
# guard.matches?(object, :from => :parked) # => true
|
66
|
+
# guard.matches?(object, :to => :idling) # => true
|
67
|
+
# guard.matches?(object, :from => :parked, :to => :idling) # => true
|
68
|
+
# guard.matches?(object, :on => :ignite, :from => :parked, :to => :idling) # => true
|
67
69
|
#
|
68
70
|
# # Unsuccessful
|
69
|
-
# guard.matches?(object, :on =>
|
70
|
-
# guard.matches?(object, :from =>
|
71
|
-
# guard.matches?(object, :to =>
|
72
|
-
# guard.matches?(object, :from =>
|
73
|
-
# guard.matches?(object, :on =>
|
71
|
+
# guard.matches?(object, :on => :park) # => false
|
72
|
+
# guard.matches?(object, :from => :idling) # => false
|
73
|
+
# guard.matches?(object, :to => :first_gear) # => false
|
74
|
+
# guard.matches?(object, :from => :parked, :to => :first_gear) # => false
|
75
|
+
# guard.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false
|
74
76
|
def matches?(object, query = {})
|
75
77
|
matches_query?(object, query) && matches_conditions?(object)
|
76
78
|
end
|
@@ -81,39 +83,36 @@ module StateMachine
|
|
81
83
|
# state.
|
82
84
|
#
|
83
85
|
# For example, if the following from states are configured:
|
84
|
-
# * +first_gear+
|
85
86
|
# * +idling+
|
87
|
+
# * +first_gear+
|
86
88
|
# * +backing_up+
|
87
89
|
#
|
88
|
-
# ...and the to state is
|
89
|
-
# * +first_gear+ -> +parked+
|
90
|
+
# ...and the to state is +parked+, then the following edges will be created:
|
90
91
|
# * +idling+ -> +parked+
|
92
|
+
# * +first_gear+ -> +parked+
|
91
93
|
# * +backing_up+ -> +parked+
|
92
94
|
#
|
93
95
|
# Each edge will be labeled with the name of the event that would cause the
|
94
96
|
# transition.
|
95
97
|
#
|
96
98
|
# The collection of edges generated on the graph will be returned.
|
97
|
-
def draw(graph,
|
99
|
+
def draw(graph, event, valid_states)
|
98
100
|
# From states: :from, everything but :except states, or all states
|
99
101
|
from_states = requirements[:from] || requirements[:except_from] && (valid_states - requirements[:except_from]) || valid_states
|
100
102
|
|
101
103
|
# To state can be optional, otherwise it's a loopback
|
102
|
-
|
103
|
-
to_state = State.id_for(to_state.first)
|
104
|
-
end
|
104
|
+
to_state = requirements[:to] && requirements[:to].first
|
105
105
|
|
106
106
|
# Generate an edge between each from and to state
|
107
107
|
from_states.collect do |from_state|
|
108
|
-
from_state
|
109
|
-
graph.add_edge(from_state, to_state || from_state, :label => event_name)
|
108
|
+
graph.add_edge(from_state.to_s, (to_state || from_state).to_s, :label => event.to_s)
|
110
109
|
end
|
111
110
|
end
|
112
111
|
|
113
112
|
protected
|
114
113
|
# Verify that the from state, to state, and event match the query
|
115
114
|
def matches_query?(object, query)
|
116
|
-
|
115
|
+
!query || query.empty? || [:from, :to, :on].all? do |option|
|
117
116
|
!query.include?(option) || find_match(query[option], requirements[option], requirements[:"except_#{option}"])
|
118
117
|
end
|
119
118
|
end
|
@@ -137,13 +136,20 @@ module StateMachine
|
|
137
136
|
#
|
138
137
|
# == Examples
|
139
138
|
#
|
140
|
-
#
|
139
|
+
# # No list
|
140
|
+
# find_match(:parked, nil, nil) # => true
|
141
|
+
#
|
142
|
+
# # Whitelist
|
143
|
+
# find_match(nil, [:parked, :idling], nil) # => false
|
141
144
|
# find_match(nil, [nil], nil) # => true
|
142
|
-
# find_match(
|
143
|
-
# find_match(
|
144
|
-
#
|
145
|
-
#
|
146
|
-
# find_match(
|
145
|
+
# find_match(:parked, [:parked, :idling], nil) # => true
|
146
|
+
# find_match(:first_gear, [:parked, :idling], nil) # => false
|
147
|
+
#
|
148
|
+
# # Blacklist
|
149
|
+
# find_match(nil, nil, [:parked, :idling]) # => true
|
150
|
+
# find_match(nil, nil, [nil]) # => false
|
151
|
+
# find_match(:parked, nil, [:parked, idling]) # => false
|
152
|
+
# find_match(:first_gear, nil, [:parked, :idling]) # => true
|
147
153
|
def find_match(value, whitelist, blacklist)
|
148
154
|
if whitelist
|
149
155
|
whitelist.include?(value)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# Load each available integration
|
2
|
+
Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
|
3
|
+
require "state_machine/integrations/#{File.basename(path)}"
|
4
|
+
end
|
5
|
+
|
6
|
+
module StateMachine
|
7
|
+
# Integrations allow state machines to take advantage of features within the
|
8
|
+
# context of a particular library. This is currently most useful with
|
9
|
+
# database libraries. For example, the various database integrations allow
|
10
|
+
# state machines to hook into features like:
|
11
|
+
# * Saving
|
12
|
+
# * Transactions
|
13
|
+
# * Observers
|
14
|
+
# * Scopes
|
15
|
+
# * Callbacks
|
16
|
+
#
|
17
|
+
# This type of integration allows the user to work with state machines in a
|
18
|
+
# fashion similar to other object models in their application.
|
19
|
+
#
|
20
|
+
# The integration interface is loosely defined by various unimplemented
|
21
|
+
# methods in the StateMachine::Machine class. See that class or the various
|
22
|
+
# built-in integrations for more information about how to define additional
|
23
|
+
# integrations.
|
24
|
+
module Integrations
|
25
|
+
# Attempts to find an integration that matches the given class. This will
|
26
|
+
# look through all of the built-in integrations under the StateMachine::Integrations
|
27
|
+
# namespace and find one that successfully matches the class.
|
28
|
+
#
|
29
|
+
# == Examples
|
30
|
+
#
|
31
|
+
# class Vehicle
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# class ARVehicle < ActiveRecord::Base
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# class DMVehicle
|
38
|
+
# include DataMapper::Resource
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# class SequelVehicle < Sequel::Model
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# StateMachine::Integrations.match(Vehicle) # => nil
|
45
|
+
# StateMachine::Integrations.match(ARVehicle) # => StateMachine::Integrations::ActiveRecord
|
46
|
+
# StateMachine::Integrations.match(DMVehicle) # => StateMachine::Integrations::DataMapper
|
47
|
+
# StateMachine::Integrations.match(SequelVehicle) # => StateMachine::Integrations::Sequel
|
48
|
+
def self.match(klass)
|
49
|
+
if integration = constants.find {|name| const_get(name).matches?(klass)}
|
50
|
+
find(integration)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Finds an integration with the given name. If the integration cannot be
|
55
|
+
# found, then a NameError exception will be raised.
|
56
|
+
#
|
57
|
+
# == Examples
|
58
|
+
#
|
59
|
+
# StateMachine::Integrations.find(:active_record) # => StateMachine::Integrations::ActiveRecord
|
60
|
+
# StateMachine::Integrations.find(:data_mapper) # => StateMachine::Integrations::DataMapper
|
61
|
+
# StateMachine::Integrations.find(:sequel) # => StateMachine::Integrations::Sequel
|
62
|
+
# StateMachine::Integrations.find(:invalid) # => NameError: wrong constant name Invalid
|
63
|
+
def self.find(name)
|
64
|
+
const_get(name.to_s.gsub(/(?:^|_)(.)/) {$1.upcase})
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|