state_machine 1.1.2 → 1.2.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 (240) hide show
  1. data/.gitignore +7 -11
  2. data/.travis.yml +49 -7
  3. data/Appraisals +255 -87
  4. data/CHANGELOG.md +30 -0
  5. data/README.md +142 -21
  6. data/Rakefile +1 -11
  7. data/examples/Gemfile +5 -0
  8. data/examples/Gemfile.lock +14 -0
  9. data/examples/auto_shop.rb +2 -0
  10. data/examples/car.rb +2 -0
  11. data/examples/doc/AutoShop.html +2856 -0
  12. data/examples/doc/AutoShop_state.png +0 -0
  13. data/examples/doc/Car.html +919 -0
  14. data/examples/doc/Car_state.png +0 -0
  15. data/examples/doc/TrafficLight.html +2230 -0
  16. data/examples/doc/TrafficLight_state.png +0 -0
  17. data/examples/doc/Vehicle.html +7921 -0
  18. data/examples/doc/Vehicle_state.png +0 -0
  19. data/examples/doc/_index.html +136 -0
  20. data/examples/doc/class_list.html +47 -0
  21. data/examples/doc/css/common.css +1 -0
  22. data/examples/doc/css/full_list.css +55 -0
  23. data/examples/doc/css/style.css +322 -0
  24. data/examples/doc/file_list.html +46 -0
  25. data/examples/doc/frames.html +13 -0
  26. data/examples/doc/index.html +136 -0
  27. data/examples/doc/js/app.js +205 -0
  28. data/examples/doc/js/full_list.js +173 -0
  29. data/examples/doc/js/jquery.js +16 -0
  30. data/examples/doc/method_list.html +734 -0
  31. data/examples/doc/top-level-namespace.html +105 -0
  32. data/examples/rails-rest/migration.rb +1 -5
  33. data/examples/rails-rest/view__form.html.erb +34 -0
  34. data/examples/rails-rest/view_edit.html.erb +2 -21
  35. data/examples/rails-rest/view_index.html.erb +6 -4
  36. data/examples/rails-rest/view_new.html.erb +2 -11
  37. data/examples/rails-rest/view_show.html.erb +5 -3
  38. data/examples/traffic_light.rb +2 -0
  39. data/examples/vehicle.rb +2 -0
  40. data/gemfiles/active_model-3.0.0.gemfile.lock +9 -6
  41. data/gemfiles/active_model-3.0.5.gemfile.lock +10 -7
  42. data/gemfiles/active_model-3.1.1.gemfile.lock +12 -10
  43. data/gemfiles/{active_model-3.2.0.gemfile → active_model-3.2.1.gemfile} +1 -1
  44. data/gemfiles/{graphviz-0.9.0.gemfile → active_model-3.2.12.gemfile} +1 -1
  45. data/gemfiles/active_model-3.2.12.gemfile.lock +36 -0
  46. data/gemfiles/{active_record-3.2.0.gemfile → active_model-3.2.13.rc1.gemfile} +1 -2
  47. data/gemfiles/active_model-3.2.13.rc1.gemfile.lock +36 -0
  48. data/gemfiles/active_model-4.0.0.gemfile +9 -0
  49. data/gemfiles/active_model-4.0.0.gemfile.lock +78 -0
  50. data/gemfiles/active_record-2.0.0.gemfile +2 -1
  51. data/gemfiles/active_record-2.0.0.gemfile.lock +15 -6
  52. data/gemfiles/active_record-2.0.5.gemfile +2 -1
  53. data/gemfiles/active_record-2.0.5.gemfile.lock +15 -6
  54. data/gemfiles/active_record-2.1.0.gemfile +2 -1
  55. data/gemfiles/active_record-2.1.0.gemfile.lock +15 -6
  56. data/gemfiles/active_record-2.1.2.gemfile +2 -1
  57. data/gemfiles/active_record-2.1.2.gemfile.lock +15 -6
  58. data/gemfiles/active_record-2.2.3.gemfile +2 -1
  59. data/gemfiles/active_record-2.2.3.gemfile.lock +15 -6
  60. data/gemfiles/active_record-2.3.12.gemfile +2 -1
  61. data/gemfiles/active_record-2.3.12.gemfile.lock +15 -6
  62. data/gemfiles/active_record-2.3.5.gemfile +9 -0
  63. data/gemfiles/active_record-2.3.5.gemfile.lock +39 -0
  64. data/gemfiles/active_record-3.0.0.gemfile +2 -1
  65. data/gemfiles/active_record-3.0.0.gemfile.lock +18 -11
  66. data/gemfiles/active_record-3.0.5.gemfile +2 -1
  67. data/gemfiles/active_record-3.0.5.gemfile.lock +19 -12
  68. data/gemfiles/active_record-3.1.1.gemfile +2 -1
  69. data/gemfiles/active_record-3.1.1.gemfile.lock +22 -16
  70. data/gemfiles/active_record-3.2.12.gemfile +9 -0
  71. data/gemfiles/active_record-3.2.12.gemfile.lock +51 -0
  72. data/gemfiles/active_record-3.2.13.rc1.gemfile +9 -0
  73. data/gemfiles/active_record-3.2.13.rc1.gemfile.lock +51 -0
  74. data/gemfiles/active_record-4.0.0.gemfile +11 -0
  75. data/gemfiles/active_record-4.0.0.gemfile.lock +83 -0
  76. data/gemfiles/data_mapper-0.10.2.gemfile +1 -0
  77. data/gemfiles/data_mapper-0.10.2.gemfile.lock +13 -9
  78. data/gemfiles/data_mapper-0.9.11.gemfile +1 -0
  79. data/gemfiles/data_mapper-0.9.11.gemfile.lock +31 -7
  80. data/gemfiles/data_mapper-0.9.4.gemfile.lock +25 -14
  81. data/gemfiles/data_mapper-0.9.7.gemfile +1 -0
  82. data/gemfiles/data_mapper-0.9.7.gemfile.lock +27 -15
  83. data/gemfiles/data_mapper-1.0.0.gemfile.lock +20 -17
  84. data/gemfiles/data_mapper-1.0.1.gemfile.lock +20 -17
  85. data/gemfiles/data_mapper-1.0.2.gemfile.lock +20 -17
  86. data/gemfiles/data_mapper-1.1.0.gemfile.lock +19 -16
  87. data/gemfiles/data_mapper-1.2.0.gemfile.lock +19 -16
  88. data/gemfiles/default.gemfile.lock +8 -5
  89. data/gemfiles/graphviz-0.9.17.gemfile +7 -0
  90. data/gemfiles/graphviz-0.9.17.gemfile.lock +29 -0
  91. data/gemfiles/graphviz-0.9.21.gemfile.lock +7 -4
  92. data/gemfiles/graphviz-1.0.0.gemfile.lock +7 -4
  93. data/gemfiles/graphviz-1.0.3.gemfile +7 -0
  94. data/gemfiles/graphviz-1.0.3.gemfile.lock +29 -0
  95. data/gemfiles/graphviz-1.0.8.gemfile +7 -0
  96. data/gemfiles/graphviz-1.0.8.gemfile.lock +29 -0
  97. data/gemfiles/mongo_mapper-0.10.0.gemfile +1 -0
  98. data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +14 -11
  99. data/gemfiles/mongo_mapper-0.11.1.gemfile +7 -0
  100. data/gemfiles/mongo_mapper-0.11.1.gemfile.lock +44 -0
  101. data/gemfiles/mongo_mapper-0.11.2.gemfile +9 -0
  102. data/gemfiles/mongo_mapper-0.11.2.gemfile.lock +48 -0
  103. data/gemfiles/mongo_mapper-0.12.0.gemfile +9 -0
  104. data/gemfiles/mongo_mapper-0.12.0.gemfile.lock +48 -0
  105. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +7 -4
  106. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +7 -4
  107. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +7 -4
  108. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +7 -4
  109. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +7 -4
  110. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +7 -4
  111. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +7 -4
  112. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +7 -4
  113. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +7 -4
  114. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +7 -4
  115. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +7 -4
  116. data/gemfiles/mongoid-2.0.0.gemfile +2 -0
  117. data/gemfiles/mongoid-2.0.0.gemfile.lock +22 -18
  118. data/gemfiles/mongoid-2.1.4.gemfile +2 -0
  119. data/gemfiles/mongoid-2.1.4.gemfile.lock +21 -17
  120. data/gemfiles/mongoid-2.2.4.gemfile +2 -0
  121. data/gemfiles/mongoid-2.2.4.gemfile.lock +21 -17
  122. data/gemfiles/mongoid-2.3.3.gemfile +2 -0
  123. data/gemfiles/mongoid-2.3.3.gemfile.lock +21 -17
  124. data/gemfiles/mongoid-2.4.0.gemfile +9 -0
  125. data/gemfiles/mongoid-2.4.0.gemfile.lock +47 -0
  126. data/gemfiles/mongoid-2.4.10.gemfile +9 -0
  127. data/gemfiles/mongoid-2.4.10.gemfile.lock +47 -0
  128. data/gemfiles/mongoid-2.5.2.gemfile +9 -0
  129. data/gemfiles/mongoid-2.5.2.gemfile.lock +47 -0
  130. data/gemfiles/mongoid-2.6.0.gemfile +9 -0
  131. data/gemfiles/mongoid-2.6.0.gemfile.lock +47 -0
  132. data/gemfiles/mongoid-3.0.0.gemfile +8 -0
  133. data/gemfiles/mongoid-3.0.0.gemfile.lock +45 -0
  134. data/gemfiles/mongoid-3.0.22.gemfile +8 -0
  135. data/gemfiles/mongoid-3.0.22.gemfile.lock +45 -0
  136. data/gemfiles/mongoid-3.1.0.gemfile +8 -0
  137. data/gemfiles/mongoid-3.1.0.gemfile.lock +45 -0
  138. data/gemfiles/sequel-2.11.0.gemfile +2 -1
  139. data/gemfiles/sequel-2.11.0.gemfile.lock +11 -6
  140. data/gemfiles/sequel-2.12.0.gemfile +2 -1
  141. data/gemfiles/sequel-2.12.0.gemfile.lock +11 -6
  142. data/gemfiles/sequel-2.8.0.gemfile +2 -1
  143. data/gemfiles/sequel-2.8.0.gemfile.lock +11 -6
  144. data/gemfiles/sequel-3.0.0.gemfile +2 -1
  145. data/gemfiles/sequel-3.0.0.gemfile.lock +11 -6
  146. data/gemfiles/sequel-3.10.0.gemfile +9 -0
  147. data/gemfiles/sequel-3.10.0.gemfile.lock +33 -0
  148. data/gemfiles/sequel-3.13.0.gemfile +2 -1
  149. data/gemfiles/sequel-3.13.0.gemfile.lock +11 -6
  150. data/gemfiles/sequel-3.14.0.gemfile +2 -1
  151. data/gemfiles/sequel-3.14.0.gemfile.lock +11 -6
  152. data/gemfiles/sequel-3.23.0.gemfile +2 -1
  153. data/gemfiles/sequel-3.23.0.gemfile.lock +11 -6
  154. data/gemfiles/sequel-3.24.0.gemfile +2 -1
  155. data/gemfiles/sequel-3.24.0.gemfile.lock +11 -6
  156. data/gemfiles/sequel-3.29.0.gemfile +2 -1
  157. data/gemfiles/sequel-3.29.0.gemfile.lock +11 -6
  158. data/gemfiles/sequel-3.34.0.gemfile +9 -0
  159. data/gemfiles/sequel-3.34.0.gemfile.lock +33 -0
  160. data/gemfiles/sequel-3.35.0.gemfile +9 -0
  161. data/gemfiles/sequel-3.35.0.gemfile.lock +33 -0
  162. data/gemfiles/sequel-3.4.0.gemfile +9 -0
  163. data/gemfiles/sequel-3.4.0.gemfile.lock +33 -0
  164. data/gemfiles/sequel-3.44.0.gemfile +9 -0
  165. data/gemfiles/sequel-3.44.0.gemfile.lock +33 -0
  166. data/lib/state_machine.rb +6 -0
  167. data/lib/state_machine/branch.rb +9 -8
  168. data/lib/state_machine/callback.rb +2 -2
  169. data/lib/state_machine/core.rb +10 -0
  170. data/lib/state_machine/core_ext.rb +1 -0
  171. data/lib/state_machine/eval_helpers.rb +5 -3
  172. data/lib/state_machine/event.rb +17 -6
  173. data/lib/state_machine/graph.rb +92 -0
  174. data/lib/state_machine/integrations.rb +13 -1
  175. data/lib/state_machine/integrations/active_model.rb +14 -20
  176. data/lib/state_machine/integrations/active_model/observer.rb +3 -3
  177. data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
  178. data/lib/state_machine/integrations/active_record.rb +52 -25
  179. data/lib/state_machine/integrations/active_record/locale.rb +1 -1
  180. data/lib/state_machine/integrations/active_record/versions.rb +1 -17
  181. data/lib/state_machine/integrations/base.rb +15 -6
  182. data/lib/state_machine/integrations/data_mapper.rb +98 -35
  183. data/lib/state_machine/integrations/data_mapper/versions.rb +46 -8
  184. data/lib/state_machine/integrations/mongo_mapper.rb +39 -12
  185. data/lib/state_machine/integrations/mongo_mapper/locale.rb +1 -1
  186. data/lib/state_machine/integrations/mongo_mapper/versions.rb +3 -20
  187. data/lib/state_machine/integrations/mongoid.rb +52 -14
  188. data/lib/state_machine/integrations/mongoid/locale.rb +1 -1
  189. data/lib/state_machine/integrations/mongoid/versions.rb +52 -26
  190. data/lib/state_machine/integrations/sequel.rb +82 -33
  191. data/lib/state_machine/integrations/sequel/versions.rb +19 -44
  192. data/lib/state_machine/machine.rb +99 -59
  193. data/lib/state_machine/machine_collection.rb +1 -2
  194. data/lib/state_machine/macro_methods.rb +29 -0
  195. data/lib/state_machine/node_collection.rb +1 -1
  196. data/lib/state_machine/state.rb +18 -10
  197. data/lib/state_machine/state_context.rb +2 -2
  198. data/lib/state_machine/transition.rb +8 -1
  199. data/lib/state_machine/transition_collection.rb +2 -1
  200. data/lib/state_machine/version.rb +1 -1
  201. data/lib/state_machine/yard.rb +8 -0
  202. data/lib/state_machine/yard/handlers.rb +12 -0
  203. data/lib/state_machine/yard/handlers/base.rb +32 -0
  204. data/lib/state_machine/yard/handlers/event.rb +25 -0
  205. data/lib/state_machine/yard/handlers/machine.rb +344 -0
  206. data/lib/state_machine/yard/handlers/state.rb +25 -0
  207. data/lib/state_machine/yard/handlers/transition.rb +47 -0
  208. data/lib/state_machine/yard/templates.rb +3 -0
  209. data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
  210. data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  211. data/lib/tasks/state_machine.rb +2 -1
  212. data/lib/yard-state_machine.rb +2 -0
  213. data/state_machine.gemspec +4 -3
  214. data/test/files/switch.rb +4 -0
  215. data/test/test_helper.rb +5 -0
  216. data/test/unit/branch_test.rb +117 -36
  217. data/test/unit/callback_test.rb +5 -2
  218. data/test/unit/eval_helpers_test.rb +49 -1
  219. data/test/unit/event_collection_test.rb +3 -1
  220. data/test/unit/event_test.rb +182 -12
  221. data/test/unit/graph_test.rb +98 -0
  222. data/test/unit/integrations/active_model_test.rb +82 -12
  223. data/test/unit/integrations/active_record_test.rb +393 -37
  224. data/test/unit/integrations/base_test.rb +7 -2
  225. data/test/unit/integrations/data_mapper_test.rb +326 -72
  226. data/test/unit/integrations/mongo_mapper_test.rb +338 -44
  227. data/test/unit/integrations/mongoid_test.rb +606 -98
  228. data/test/unit/integrations/sequel_test.rb +429 -102
  229. data/test/unit/integrations_test.rb +28 -6
  230. data/test/unit/machine_collection_test.rb +6 -2
  231. data/test/unit/machine_test.rb +134 -82
  232. data/test/unit/node_collection_test.rb +2 -2
  233. data/test/unit/path_test.rb +1 -1
  234. data/test/unit/state_test.rb +65 -21
  235. data/test/unit/transition_collection_test.rb +43 -23
  236. data/test/unit/transition_test.rb +8 -2
  237. metadata +303 -221
  238. data/gemfiles/active_model-3.2.0.gemfile.lock +0 -32
  239. data/gemfiles/active_record-3.2.0.gemfile.lock +0 -43
  240. data/gemfiles/graphviz-0.9.0.gemfile.lock +0 -26
@@ -1 +1,2 @@
1
+ # Loads all of the extensions to be made to Ruby core classes
1
2
  require 'state_machine/core_ext/class/state_machine'
@@ -53,7 +53,9 @@ module StateMachine
53
53
  def evaluate_method(object, method, *args, &block)
54
54
  case method
55
55
  when Symbol
56
- object.method(method).arity == 0 ? object.send(method, &block) : object.send(method, *args, &block)
56
+ klass = (class << object; self; end)
57
+ args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
58
+ object.send(method, *args, &block)
57
59
  when Proc, Method
58
60
  args.unshift(object)
59
61
  arity = method.arity
@@ -74,12 +76,12 @@ module StateMachine
74
76
  args = args[0, arity] if [0, 1].include?(arity)
75
77
  end
76
78
 
77
- method.call(*args, &block)
79
+ method.is_a?(Proc) ? method.call(*args) : method.call(*args, &block)
78
80
  when String
79
81
  eval(method, object.instance_eval {binding}, &block)
80
82
  else
81
83
  raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
82
- end
84
+ end
83
85
  end
84
86
  end
85
87
  end
@@ -58,7 +58,7 @@ module StateMachine
58
58
  reset
59
59
 
60
60
  # Output a warning if another event has a conflicting qualified name
61
- if conflict = machine.owner_class.state_machines.detect {|name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name]}
61
+ if conflict = machine.owner_class.state_machines.detect {|other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name]}
62
62
  name, other_machine = conflict
63
63
  warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
64
64
  else
@@ -107,7 +107,7 @@ module StateMachine
107
107
 
108
108
  # Only a certain subset of explicit options are allowed for transition
109
109
  # requirements
110
- assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
110
+ assert_valid_keys(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
111
111
 
112
112
  branches << branch = Branch.new(options.merge(:on => name))
113
113
  @known_states |= branch.known_states
@@ -144,7 +144,12 @@ module StateMachine
144
144
  if match = branch.match(object, requirements)
145
145
  # Branch allows for the transition to occur
146
146
  from = requirements[:from]
147
- to = match[:to].values.empty? ? from : match[:to].values.first
147
+ to = if match[:to].is_a?(LoopbackMatcher)
148
+ from
149
+ else
150
+ values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.keys(:name)
151
+ match[:to].filter(values).first
152
+ end
148
153
 
149
154
  return Transition.new(object, machine, name, from, to, !custom_from_state)
150
155
  end
@@ -193,10 +198,16 @@ module StateMachine
193
198
  # create 1 or more edges on the graph for each branch (i.e. transition)
194
199
  # configured.
195
200
  #
196
- # A collection of the generated edges will be returned.
197
- def draw(graph)
201
+ # Configuration options:
202
+ # * <tt>:human_name</tt> - Whether to use the event's human name for the
203
+ # node's label that gets drawn on the graph
204
+ def draw(graph, options = {})
198
205
  valid_states = machine.states.by_priority.map {|state| state.name}
199
- branches.collect {|branch| branch.draw(graph, name, valid_states)}.flatten
206
+ branches.each do |branch|
207
+ branch.draw(graph, options[:human_name] ? human_name : name, valid_states)
208
+ end
209
+
210
+ true
200
211
  end
201
212
 
202
213
  # Generates a nicely formatted description of this event's contents.
@@ -0,0 +1,92 @@
1
+ begin
2
+ require 'rubygems'
3
+ gem 'ruby-graphviz', '>=0.9.17'
4
+ require 'graphviz'
5
+ rescue LoadError => ex
6
+ $stderr.puts "Cannot draw the machine (#{ex.message}). `gem install ruby-graphviz` >= v0.9.17 and try again."
7
+ raise
8
+ end
9
+
10
+ require 'state_machine/assertions'
11
+
12
+ module StateMachine
13
+ # Provides a set of higher-order features on top of the raw GraphViz graphs
14
+ class Graph < GraphViz
15
+ include Assertions
16
+
17
+ # The name of the font to draw state names in
18
+ attr_reader :font
19
+
20
+ # The graph's full filename
21
+ attr_reader :file_path
22
+
23
+ # The image format to generate the graph in
24
+ attr_reader :file_format
25
+
26
+ # Creates a new graph with the given name.
27
+ #
28
+ # Configuration options:
29
+ # * <tt>:path</tt> - The path to write the graph file to. Default is the
30
+ # current directory (".").
31
+ # * <tt>:format</tt> - The image format to generate the graph in.
32
+ # Default is "png'.
33
+ # * <tt>:font</tt> - The name of the font to draw state names in.
34
+ # Default is "Arial".
35
+ # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
36
+ # "landscape"). Default is "portrait".
37
+ def initialize(name, options = {})
38
+ options = {:path => '.', :format => 'png', :font => 'Arial', :orientation => 'portrait'}.merge(options)
39
+ assert_valid_keys(options, :path, :format, :font, :orientation)
40
+
41
+ @font = options[:font]
42
+ @file_path = File.join(options[:path], "#{name}.#{options[:format]}")
43
+ @file_format = options[:format]
44
+
45
+ super('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB')
46
+ end
47
+
48
+ # Generates the actual image file based on the nodes / edges added to the
49
+ # graph. The path to the file is based on the configuration options for
50
+ # this graph.
51
+ def output
52
+ super(@file_format => @file_path)
53
+ end
54
+
55
+ # Adds a new node to the graph. The font for the node will be automatically
56
+ # set based on the graph configuration. The generated node will be returned.
57
+ #
58
+ # For example,
59
+ #
60
+ # graph = StateMachine::Graph.new('test')
61
+ # graph.add_nodes('parked', :label => 'Parked', :width => '1', :height => '1', :shape => 'ellipse')
62
+ def add_nodes(*args)
63
+ node = v0_api? ? add_node(*args) : super
64
+ node.fontname = @font
65
+ node
66
+ end
67
+
68
+ # Adds a new edge to the graph. The font for the edge will be automatically
69
+ # set based on the graph configuration. The generated edge will be returned.
70
+ #
71
+ # For example,
72
+ #
73
+ # graph = StateMachine::Graph.new('test')
74
+ # graph.add_edges('parked', 'idling', :label => 'ignite')
75
+ def add_edges(*args)
76
+ edge = v0_api? ? add_edge(*args) : super
77
+ edge.fontname = @font
78
+ edge
79
+ end
80
+
81
+ private
82
+ # Determines whether the old v0 api is in use
83
+ def v0_api?
84
+ version[0] == '0' || version[0] == '1' && version[1] == '0' && version[2] <= '2'
85
+ end
86
+
87
+ # The ruby-graphviz version data
88
+ def version
89
+ Constants::RGV_VERSION.split('.')
90
+ end
91
+ end
92
+ end
@@ -73,7 +73,19 @@ module StateMachine
73
73
  # StateMachine::Integrations.match(MongoMapperVehicle) # => StateMachine::Integrations::MongoMapper
74
74
  # StateMachine::Integrations.match(SequelVehicle) # => StateMachine::Integrations::Sequel
75
75
  def self.match(klass)
76
- all.detect {|integration| integration.available? && integration.matches?(klass)}
76
+ all.detect {|integration| integration.matches?(klass)}
77
+ end
78
+
79
+ # Attempts to find an integration that matches the given list of ancestors.
80
+ # This will look through all of the built-in integrations under the StateMachine::Integrations
81
+ # namespace and find one that successfully matches one of the ancestors.
82
+ #
83
+ # == Examples
84
+ #
85
+ # StateMachine::Integrations.match([]) # => nil
86
+ # StateMachine::Integrations.match(['ActiveRecord::Base') # => StateMachine::Integrations::ActiveModel
87
+ def self.match_ancestors(ancestors)
88
+ all.detect {|integration| integration.matches_ancestors?(ancestors)}
77
89
  end
78
90
 
79
91
  # Finds an integration with the given name. If the integration cannot be
@@ -366,26 +366,19 @@ module StateMachine
366
366
 
367
367
  @defaults = {}
368
368
 
369
- # Whether this integration is available. Only true if ActiveModel is
370
- # defined.
371
- def self.available?
372
- defined?(::ActiveModel)
373
- end
374
-
375
- # Should this integration be used for state machines in the given class?
376
369
  # Classes that include ActiveModel::Observing or ActiveModel::Validations
377
370
  # will automatically use the ActiveModel integration.
378
- def self.matches?(klass)
379
- %w(Observing Validations).any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
371
+ def self.matching_ancestors
372
+ %w(ActiveModel ActiveModel::Observing ActiveModel::Validations)
380
373
  end
381
374
 
382
375
  # Adds a validation error to the given object
383
376
  def invalidate(object, attribute, message, values = [])
384
377
  if supports_validations?
385
378
  attribute = self.attribute(attribute)
386
- options = values.inject({}) do |options, (key, value)|
387
- options[key] = value
388
- options
379
+ options = values.inject({}) do |h, (key, value)|
380
+ h[key] = value
381
+ h
389
382
  end
390
383
 
391
384
  default_options = default_error_message_options(object, attribute, message)
@@ -454,8 +447,8 @@ module StateMachine
454
447
  value = value ? value.to_s : 'nil'
455
448
 
456
449
  # Generate all possible translation keys
457
- translations = ancestors.map {|ancestor| :"#{ancestor.model_name.underscore}.#{name}.#{group}.#{value}"}
458
- translations.concat(ancestors.map {|ancestor| :"#{ancestor.model_name.underscore}.#{group}.#{value}"})
450
+ translations = ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}"}
451
+ translations.concat(ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}"})
459
452
  translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
460
453
  I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
461
454
  end
@@ -482,6 +475,7 @@ module StateMachine
482
475
  # Loads extensions to ActiveModel's Observers
483
476
  def load_observer_extensions
484
477
  require 'state_machine/integrations/active_model/observer'
478
+ require 'state_machine/integrations/active_model/observer_update'
485
479
  end
486
480
 
487
481
  # Adds a set of default callbacks that utilize the Observer extensions
@@ -537,15 +531,15 @@ module StateMachine
537
531
 
538
532
  # Configures new states with the built-in humanize scheme
539
533
  def add_states(new_states)
540
- super.each do |state|
541
- state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
534
+ super.each do |new_state|
535
+ new_state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
542
536
  end
543
537
  end
544
538
 
545
539
  # Configures new event with the built-in humanize scheme
546
540
  def add_events(new_events)
547
- super.each do |event|
548
- event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
541
+ super.each do |new_event|
542
+ new_event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
549
543
  end
550
544
  end
551
545
 
@@ -575,14 +569,14 @@ module StateMachine
575
569
  ["_from_#{from}", nil].each do |from_segment|
576
570
  ["_to_#{to}", nil].each do |to_segment|
577
571
  object.class.changed if object.class.respond_to?(:changed)
578
- object.class.notify_observers('update_with_transition', [[event_segment, from_segment, to_segment].join, object, transition])
572
+ object.class.notify_observers('update_with_transition', ObserverUpdate.new([event_segment, from_segment, to_segment].join, object, transition))
579
573
  end
580
574
  end
581
575
  end
582
576
 
583
577
  # Generic updates
584
578
  object.class.changed if object.class.respond_to?(:changed)
585
- object.class.notify_observers('update_with_transition', ["#{type}_transition", object, transition])
579
+ object.class.notify_observers('update_with_transition', ObserverUpdate.new("#{type}_transition", object, transition))
586
580
 
587
581
  true
588
582
  end
@@ -19,9 +19,9 @@ module StateMachine
19
19
  # end
20
20
  # end
21
21
  module Observer
22
- def update_with_transition(args)
23
- observed_method, object, transition = args
24
- send(observed_method, object, transition) if respond_to?(observed_method)
22
+ def update_with_transition(observer_update)
23
+ method = observer_update.method
24
+ send(method, *observer_update.args) if respond_to?(method)
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,42 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveModel
4
+ # Represents the encapsulation of all of the details to be included in an
5
+ # update to state machine observers. This allows multiple arguments to
6
+ # get passed to an observer method (instead of just a single +object+)
7
+ # while still respecting the way in which ActiveModel checks for the
8
+ # object's list of observers.
9
+ class ObserverUpdate
10
+ # The method to invoke on the observer
11
+ attr_reader :method
12
+
13
+ # The object being transitioned
14
+ attr_reader :object
15
+
16
+ # The transition being run
17
+ attr_reader :transition
18
+
19
+ def initialize(method, object, transition) #:nodoc:
20
+ @method, @object, @transition = method, object, transition
21
+ end
22
+
23
+ # The arguments to pass into the method
24
+ def args
25
+ [object, transition]
26
+ end
27
+
28
+ # The class of the object being transitioned. Normally the object
29
+ # getting passed into observer methods is the actual instance of the
30
+ # ActiveModel class. ActiveModel uses that instance's class to check
31
+ # for enabled / disabled observers.
32
+ #
33
+ # Since state_machine is passing an ObserverUpdate instance into observer
34
+ # methods, +class+ needs to be overridden so that ActiveModel can still
35
+ # get access to the enabled / disabled observers.
36
+ def class
37
+ object.class
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -136,18 +136,6 @@ module StateMachine
136
136
  # end
137
137
  # end
138
138
  #
139
- # If using the +save+ action for the machine, this option will be ignored as
140
- # the transaction will be created by ActiveRecord within +save+. To avoid
141
- # this, use a different action like so:
142
- #
143
- # class Vehicle < ActiveRecord::Base
144
- # state_machine :initial => :parked, :use_transactions => false, :action => :save_state do
145
- # ...
146
- # end
147
- #
148
- # alias_method :save_state, :save
149
- # end
150
- #
151
139
  # == Validations
152
140
  #
153
141
  # As mentioned in StateMachine::Machine#state, you can define behaviors,
@@ -290,6 +278,27 @@ module StateMachine
290
278
  # that allows new records to be saved without being affected by rollbacks
291
279
  # in the +Vehicle+ model's transaction.
292
280
  #
281
+ # === Callback Order
282
+ #
283
+ # Callbacks occur in the following order. Callbacks specific to state_machine
284
+ # are bolded. The remaining callbacks are part of ActiveRecord.
285
+ #
286
+ # * (-) save
287
+ # * (-) begin transaction (if enabled)
288
+ # * (1) *before_transition*
289
+ # * (-) valid
290
+ # * (2) before_validation
291
+ # * (-) validate
292
+ # * (3) after_validation
293
+ # * (4) before_save
294
+ # * (5) before_create
295
+ # * (-) create
296
+ # * (6) after_create
297
+ # * (7) after_save
298
+ # * (8) *after_transition*
299
+ # * (-) end transaction (if enabled)
300
+ # * (9) after_commit
301
+ #
293
302
  # == Observers
294
303
  #
295
304
  # In addition to support for ActiveRecord-like hooks, there is additional
@@ -407,17 +416,10 @@ module StateMachine
407
416
  # The default options to use for state machines using this integration
408
417
  @defaults = {:action => :save}
409
418
 
410
- # Whether this integration is available. Only true if ActiveRecord::Base
411
- # is defined.
412
- def self.available?
413
- defined?(::ActiveRecord::Base)
414
- end
415
-
416
- # Should this integration be used for state machines in the given class?
417
419
  # Classes that inherit from ActiveRecord::Base will automatically use
418
420
  # the ActiveRecord integration.
419
- def self.matches?(klass)
420
- klass <= ::ActiveRecord::Base
421
+ def self.matching_ancestors
422
+ %w(ActiveRecord::Base)
421
423
  end
422
424
 
423
425
  def self.extended(base) #:nodoc:
@@ -431,6 +433,13 @@ module StateMachine
431
433
  action == :save
432
434
  end
433
435
 
436
+ # Gets the db default for the machine's attribute
437
+ def owner_class_attribute_default
438
+ if owner_class.connected? && owner_class.table_exists? && column = owner_class.columns_hash[attribute.to_s]
439
+ column.default
440
+ end
441
+ end
442
+
434
443
  # Defines an initialization hook into the owner class for setting the
435
444
  # initial state of the machine *before* any attributes are set on the
436
445
  # object
@@ -448,7 +457,7 @@ module StateMachine
448
457
  def column_defaults(*) #:nodoc:
449
458
  result = super
450
459
  # No need to pass in an object, since the overrides will be forced
451
- self.state_machines.initialize_states(nil, :dynamic => false, :to => result)
460
+ self.state_machines.initialize_states(nil, :static => :force, :dynamic => false, :to => result)
452
461
  result
453
462
  end
454
463
  end_eval
@@ -469,7 +478,19 @@ module StateMachine
469
478
  # Uses around callbacks to run state events if using the :save hook
470
479
  def define_action_hook
471
480
  if action_hook == :save
472
- owner_class.set_callback(:save, :around, self, :prepend => true)
481
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
482
+ def save(*)
483
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
484
+ end
485
+
486
+ def save!(*)
487
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super } || raise(ActiveRecord::RecordInvalid.new(self))
488
+ end
489
+
490
+ def changed_for_autosave?
491
+ super || self.class.state_machines.any? {|name, machine| machine.action == :save && machine.read(self, :event)}
492
+ end
493
+ end_eval
473
494
  else
474
495
  super
475
496
  end
@@ -477,7 +498,9 @@ module StateMachine
477
498
 
478
499
  # Runs state events around the machine's :save action
479
500
  def around_save(object)
480
- object.class.state_machines.transitions(object, action).perform { yield }
501
+ transaction(object) do
502
+ object.class.state_machines.transitions(object, action).perform { yield }
503
+ end
481
504
  end
482
505
 
483
506
  # Creates a scope for finding records *with* a particular state or
@@ -502,7 +525,11 @@ module StateMachine
502
525
  # an ActiveRecord::Rollback exception if the yielded block fails
503
526
  # (i.e. returns false).
504
527
  def transaction(object)
505
- object.class.transaction {raise ::ActiveRecord::Rollback unless yield}
528
+ result = nil
529
+ object.class.transaction do
530
+ raise ::ActiveRecord::Rollback unless result = yield
531
+ end
532
+ result
506
533
  end
507
534
 
508
535
  # Defines a new named scope with the given name