state_machine 0.4.3 → 0.5.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 +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
|