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.
Files changed (48) hide show
  1. data/CHANGELOG.rdoc +17 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +54 -84
  4. data/Rakefile +1 -1
  5. data/examples/Car_state.png +0 -0
  6. data/examples/Vehicle_state.png +0 -0
  7. data/examples/auto_shop.rb +11 -0
  8. data/examples/car.rb +19 -0
  9. data/examples/traffic_light.rb +9 -0
  10. data/examples/vehicle.rb +35 -0
  11. data/lib/state_machine.rb +65 -52
  12. data/lib/state_machine/assertions.rb +1 -1
  13. data/lib/state_machine/callback.rb +13 -9
  14. data/lib/state_machine/eval_helpers.rb +4 -3
  15. data/lib/state_machine/event.rb +51 -33
  16. data/lib/state_machine/extensions.rb +2 -2
  17. data/lib/state_machine/guard.rb +47 -41
  18. data/lib/state_machine/integrations.rb +67 -0
  19. data/lib/state_machine/integrations/active_record.rb +62 -36
  20. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  21. data/lib/state_machine/integrations/data_mapper.rb +23 -37
  22. data/lib/state_machine/integrations/data_mapper/observer.rb +23 -9
  23. data/lib/state_machine/integrations/sequel.rb +23 -24
  24. data/lib/state_machine/machine.rb +380 -277
  25. data/lib/state_machine/node_collection.rb +142 -0
  26. data/lib/state_machine/state.rb +114 -69
  27. data/lib/state_machine/state_collection.rb +38 -0
  28. data/lib/state_machine/transition.rb +36 -17
  29. data/test/active_record.log +2940 -85664
  30. data/test/functional/state_machine_test.rb +49 -53
  31. data/test/sequel.log +747 -11990
  32. data/test/unit/assertions_test.rb +2 -1
  33. data/test/unit/callback_test.rb +14 -12
  34. data/test/unit/eval_helpers_test.rb +25 -6
  35. data/test/unit/event_test.rb +144 -124
  36. data/test/unit/guard_test.rb +118 -140
  37. data/test/unit/integrations/active_record_test.rb +102 -68
  38. data/test/unit/integrations/data_mapper_test.rb +48 -37
  39. data/test/unit/integrations/sequel_test.rb +34 -25
  40. data/test/unit/integrations_test.rb +42 -0
  41. data/test/unit/machine_test.rb +460 -531
  42. data/test/unit/node_collection_test.rb +208 -0
  43. data/test/unit/state_collection_test.rb +167 -0
  44. data/test/unit/state_machine_test.rb +1 -1
  45. data/test/unit/state_test.rb +223 -200
  46. data/test/unit/transition_test.rb +81 -46
  47. metadata +17 -3
  48. 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(", ")}" unless invalid_keys.empty?
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
- # Whether to automatically bind the callback to the object being
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 libraries, such as Extlib, handle
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
- # libraries, such as ActiveSupport, pass the object as an argument to
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 application-wide:
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
- # * +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).
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 attr_accessor definitions.
104
- def initialize(options = {}, &block) #:nodoc:
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 an method that can be bound to the object being transitioned
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 string to be evaluated, or a block to be invoked'
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
@@ -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 different events.
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
- # * +to+ - The state that's being transitioned to. If not specified, then the transition will not change the state.
48
- # * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state.
49
- # * +except_from+ - A state or array of states that *cannot* be transitioned from.
50
- # * +if+ - Specifies a method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
51
- # * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
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 => 'parked'
72
- # transition :from => %w(first_gear reverse)
73
- # transition :except_from => 'parked'
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 => 'parked'
76
- # transition :to => lambda {Time.now}
77
- # transition :to => 'parked', :from => 'first_gear'
78
- # transition :to => 'parked', :from => %w(first_gear reverse)
79
- # transition :to => 'parked', :from => 'first_gear', :if => :moving?
80
- # transition :to => 'parked', :from => 'first_gear', :unless => :stopped?
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, :to, :from, :except_from, :if, :unless)
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 = object.send(machine.attribute)
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.states_order
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 to be fired now
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 to transition
174
+ # Fires the event, raising an exception if it fails
157
175
  define_method("#{qualified_name}!") do |*args|
158
- send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{attribute} via :#{name} from #{send(attribute).inspect}")
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 # => {"state" => #<StateMachine::Machine:0xb6f6e4a4 ...>
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
@@ -10,32 +10,34 @@ module StateMachine
10
10
  include Assertions
11
11
  include EvalHelpers
12
12
 
13
- # The transition/conditional options that must be met in order for the
14
- # guard to match
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 state
18
- # values from the following requirements:
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, :to, :from, :on, :except_to, :except_from, :except_on, :if, :unless)
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
- [:to, :from, :on, :except_to, :except_from, :except_on].each do |option|
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 [:to, :from, :except_to, :except_from].include?(option)
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
- # conditionals pass on the given object.
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 => 'ignite', :from => [nil, 'parked'], :to => 'idling')
60
+ # guard = StateMachine::Guard.new(:on => :ignite, :from => [nil, :parked], :to => :idling)
59
61
  #
60
62
  # # Successful
61
- # guard.matches?(object, :on => 'ignite') # => true
62
- # guard.matches?(object, :from => nil) # => true
63
- # guard.matches?(object, :from => 'parked') # => true
64
- # guard.matches?(object, :to => 'idling') # => true
65
- # guard.matches?(object, :from => 'parked', :to => 'idling') # => true
66
- # guard.matches?(object, :on => 'ignite', :from => 'parked', :to => 'idling') # => true
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 => 'park') # => false
70
- # guard.matches?(object, :from => 'idling') # => false
71
- # guard.matches?(object, :to => 'first_gear') # => false
72
- # guard.matches?(object, :from => 'parked', :to => 'first_gear') # => false
73
- # guard.matches?(object, :on => 'park', :from => 'parked', :to => 'idling') # => false
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 "parked", then the following edges will be created:
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, event_name, valid_states)
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
- if to_state = requirements[:to]
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 = State.id_for(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
- (!query || query.empty?) || [:from, :to, :on].all? do |option|
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
- # find_match(nil, %w(parked idling), nil) # => false
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('parked', nil, nil) # => true
143
- # find_match('parked', %w(parked idling), nil) # => true
144
- # find_match('first_gear', %w(parked idling, nil) # => false
145
- # find_match('parked', nil, %w(parked idling)) # => false
146
- # find_match('first_gear', nil, %w(parked idling)) # => true
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