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