state_machine 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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