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
@@ -489,7 +489,7 @@ module StateMachine
489
489
  @default_messages = {
490
490
  :invalid => 'is invalid',
491
491
  :invalid_event => 'cannot transition when %s',
492
- :invalid_transition => 'cannot transition via "%s"'
492
+ :invalid_transition => 'cannot transition via "%1$s"'
493
493
  }
494
494
 
495
495
  # Whether to ignore any conflicts that are detected for helper methods that
@@ -498,7 +498,7 @@ module StateMachine
498
498
  @ignore_method_conflicts = false
499
499
 
500
500
  # The class that the machine is defined in
501
- attr_accessor :owner_class
501
+ attr_reader :owner_class
502
502
 
503
503
  # The name of the machine, used for scoping methods generated for the
504
504
  # machine as a whole (not states or events)
@@ -542,7 +542,7 @@ module StateMachine
542
542
 
543
543
  # Find an integration that matches this machine's owner class
544
544
  if options.include?(:integration)
545
- @integration = StateMachine::Integrations.find_by_name(options[:integration]) if options[:integration]
545
+ @integration = options[:integration] && StateMachine::Integrations.find_by_name(options[:integration])
546
546
  else
547
547
  @integration = StateMachine::Integrations.match(owner_class)
548
548
  end
@@ -566,6 +566,7 @@ module StateMachine
566
566
  @action = options[:action]
567
567
  @use_transactions = options[:use_transactions]
568
568
  @initialize_state = options[:initialize]
569
+ @action_hook_defined = false
569
570
  self.owner_class = owner_class
570
571
  self.initial_state = options[:initial] unless sibling_machines.any?
571
572
 
@@ -631,8 +632,19 @@ module StateMachine
631
632
 
632
633
  # Update all states to reflect the new initial state
633
634
  states.each {|state| state.initial = (state.name == @initial_state)}
635
+
636
+ # Output a warning if there are conflicting initial states for the machine's
637
+ # attribute
638
+ initial_state = states.detect {|state| state.initial}
639
+ if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
640
+ warn(
641
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
642
+ "a different default for \"#{attribute}\". Use only one or the other for "\
643
+ "defining defaults to avoid unexpected behaviors."
644
+ )
645
+ end
634
646
  end
635
-
647
+
636
648
  # Gets the initial state of the machine for the given object. If a dynamic
637
649
  # initial state was configured for this machine, then the object will be
638
650
  # passed into the lambda block to help determine the actual state.
@@ -673,7 +685,7 @@ module StateMachine
673
685
 
674
686
  # Whether a dynamic initial state is being used in the machine
675
687
  def dynamic_initial_state?
676
- @initial_state.is_a?(Proc)
688
+ instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
677
689
  end
678
690
 
679
691
  # Initializes the state on the given object. Initial values are only set if
@@ -749,8 +761,8 @@ module StateMachine
749
761
  else
750
762
  name = self.name
751
763
  helper_module.class_eval do
752
- define_method(method) do |*args|
753
- block.call((scope == :instance ? self.class : self).state_machine(name), self, *args)
764
+ define_method(method) do |*block_args|
765
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args)
754
766
  end
755
767
  end
756
768
  end
@@ -1074,7 +1086,11 @@ module StateMachine
1074
1086
  # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1075
1087
  def read(object, attribute, ivar = false)
1076
1088
  attribute = self.attribute(attribute)
1077
- ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute)
1089
+ if ivar
1090
+ object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1091
+ else
1092
+ object.send(attribute)
1093
+ end
1078
1094
  end
1079
1095
 
1080
1096
  # Sets a new value in the given object's attribute.
@@ -1445,6 +1461,7 @@ module StateMachine
1445
1461
  raise ArgumentError, 'Must specify :on event' unless options[:on]
1446
1462
 
1447
1463
  branches = []
1464
+ options = options.dup
1448
1465
  event(*Array(options.delete(:on))) { branches << transition(options) }
1449
1466
 
1450
1467
  branches.length == 1 ? branches.first : branches
@@ -1569,7 +1586,7 @@ module StateMachine
1569
1586
  # before_transition :parked => :idling, :if => :moving?, :do => ...
1570
1587
  # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1571
1588
  #
1572
- # === Accessing the transition
1589
+ # == Accessing the transition
1573
1590
  #
1574
1591
  # In addition to passing the object being transitioned, the actual
1575
1592
  # transition describing the context (e.g. event, from, to) can be accessed
@@ -1597,6 +1614,40 @@ module StateMachine
1597
1614
  # See StateMachine::Transition for more information about the
1598
1615
  # attributes available on the transition.
1599
1616
  #
1617
+ # == Usage with delegates
1618
+ #
1619
+ # As noted above, state_machine uses the callback method's argument list
1620
+ # arity to determine whether to include the transition in the method call.
1621
+ # If you're using delegates, such as those defined in ActiveSupport or
1622
+ # Forwardable, the actual arity of the delegated method gets masked. This
1623
+ # means that callbacks which reference delegates will always get passed the
1624
+ # transition as an argument. For example:
1625
+ #
1626
+ # class Vehicle
1627
+ # extend Forwardable
1628
+ # delegate :refresh => :dashboard
1629
+ #
1630
+ # state_machine do
1631
+ # before_transition :refresh
1632
+ # ...
1633
+ # end
1634
+ #
1635
+ # def dashboard
1636
+ # @dashboard ||= Dashboard.new
1637
+ # end
1638
+ # end
1639
+ #
1640
+ # class Dashboard
1641
+ # def refresh(transition)
1642
+ # # ...
1643
+ # end
1644
+ # end
1645
+ #
1646
+ # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
1647
+ # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
1648
+ # raised. The only way around this is to avoid the use of delegates and
1649
+ # manually define the delegate method so that the correct arity is used.
1650
+ #
1600
1651
  # == Examples
1601
1652
  #
1602
1653
  # Below is an example of a class with one state machine and various types
@@ -1830,7 +1881,15 @@ module StateMachine
1830
1881
  # Generates the message to use when invalidating the given object after
1831
1882
  # failing to transition on a specific event
1832
1883
  def generate_message(name, values = [])
1833
- (@messages[name] || self.class.default_messages[name]) % values.map {|value| value.last}
1884
+ message = (@messages[name] || self.class.default_messages[name])
1885
+
1886
+ # Check whether there are actually any values to interpolate to avoid
1887
+ # any warnings
1888
+ if message.scan(/%./).any? {|match| match != '%%'}
1889
+ message % values.map {|value| value.last}
1890
+ else
1891
+ message
1892
+ end
1834
1893
  end
1835
1894
 
1836
1895
  # Runs a transaction, rolling back any changes if the yielded block fails.
@@ -1863,53 +1922,22 @@ module StateMachine
1863
1922
  # Default is "Arial".
1864
1923
  # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
1865
1924
  # "landscape"). Default is "portrait".
1866
- # * <tt>:output</tt> - Whether to generate the output of the graph
1867
- def draw(options = {})
1868
- options = {
1869
- :name => "#{owner_class.name}_#{name}",
1870
- :path => '.',
1871
- :format => 'png',
1872
- :font => 'Arial',
1873
- :orientation => 'portrait'
1874
- }.merge(options)
1875
- assert_valid_keys(options, :name, :path, :format, :font, :orientation)
1925
+ # * <tt>:human_names</tt> - Whether to use human state / event names for
1926
+ # node labels on the graph instead of the internal name. Default is false.
1927
+ def draw(graph_options = {})
1928
+ name = graph_options.delete(:name) || "#{owner_class.name}_#{self.name}"
1929
+ draw_options = {:human_name => false}
1930
+ draw_options[:human_name] = graph_options.delete(:human_names) if graph_options.include?(:human_names)
1876
1931
 
1877
- begin
1878
- # Load the graphviz library
1879
- require 'rubygems'
1880
- gem 'ruby-graphviz', '>=0.9.0'
1881
- require 'graphviz'
1882
-
1883
- graph = GraphViz.new('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB')
1884
-
1885
- # Add nodes
1886
- states.by_priority.each do |state|
1887
- node = state.draw(graph)
1888
- node.fontname = options[:font]
1889
- end
1890
-
1891
- # Add edges
1892
- events.each do |event|
1893
- edges = event.draw(graph)
1894
- edges.each {|edge| edge.fontname = options[:font]}
1895
- end
1896
-
1897
- # Generate the graph
1898
- graphvizVersion = Constants::RGV_VERSION.split('.')
1899
- file = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
1900
-
1901
- if graphvizVersion[0] == '0' && graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
1902
- outputOptions = {:output => options[:format], :file => file}
1903
- else
1904
- outputOptions = {options[:format] => file}
1905
- end
1906
-
1907
- graph.output(outputOptions)
1908
- graph
1909
- rescue LoadError => ex
1910
- $stderr.puts "Cannot draw the machine (#{ex.message}). `gem install ruby-graphviz` >= v0.9.0 and try again."
1911
- false
1912
- end
1932
+ graph = Graph.new(name, graph_options)
1933
+
1934
+ # Add nodes / edges
1935
+ states.by_priority.each {|state| state.draw(graph, draw_options)}
1936
+ events.each {|event| event.draw(graph, draw_options)}
1937
+
1938
+ # Output result
1939
+ graph.output
1940
+ graph
1913
1941
  end
1914
1942
 
1915
1943
  # Determines whether an action hook was defined for firing attribute-based
@@ -2142,9 +2170,9 @@ module StateMachine
2142
2170
  def define_scopes(custom_plural = nil)
2143
2171
  plural = custom_plural || pluralize(name)
2144
2172
 
2145
- [name, plural].uniq.each do |name|
2146
- [:with, :without].each do |kind|
2147
- method = "#{kind}_#{name}"
2173
+ [:with, :without].each do |kind|
2174
+ [name, plural].map {|s| s.to_s}.uniq.each do |suffix|
2175
+ method = "#{kind}_#{suffix}"
2148
2176
 
2149
2177
  if scope = send("create_#{kind}_scope", method)
2150
2178
  # Converts state names to their corresponding values so that they
@@ -2194,6 +2222,18 @@ module StateMachine
2194
2222
  yield
2195
2223
  end
2196
2224
 
2225
+ # Gets the initial attribute value defined by the owner class (outside of
2226
+ # the machine's definition). By default, this is always nil.
2227
+ def owner_class_attribute_default
2228
+ nil
2229
+ end
2230
+
2231
+ # Checks whether the given state matches the attribute default specified
2232
+ # by the owner class
2233
+ def owner_class_attribute_default_matches?(state)
2234
+ state.matches?(owner_class_attribute_default)
2235
+ end
2236
+
2197
2237
  # Updates this machine based on the configuration of other machines in the
2198
2238
  # owner class that share the same target attribute.
2199
2239
  def add_sibling_machine_configs
@@ -25,7 +25,7 @@ module StateMachine
25
25
  # writing to the object. Default is to write directly to the object.
26
26
  def initialize_states(object, options = {})
27
27
  assert_valid_keys(options, :static, :dynamic, :to)
28
- options = {:static => :force, :dynamic => true}.merge(options)
28
+ options = {:static => true, :dynamic => true}.merge(options)
29
29
 
30
30
  each_value do |machine|
31
31
  machine.initialize_state(object, :force => options[:static] == :force, :to => options[:to]) unless machine.dynamic_initial_state?
@@ -55,7 +55,6 @@ module StateMachine
55
55
 
56
56
  # Get the transition that will be performed for the event
57
57
  unless transition = event.transition_for(object)
58
- machine = event.machine
59
58
  event.on_failure(object)
60
59
  end
61
60
 
@@ -285,6 +285,35 @@ module StateMachine
285
285
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
286
286
  # vehicle.state # => "parked"
287
287
  #
288
+ # You may also need to call the +initialize_state_machines+ helper manually
289
+ # in cases where you want to change how static / dynamic initial states get
290
+ # set. For example, the following example forces the initialization of
291
+ # static states regardless of their current value:
292
+ #
293
+ # class Vehicle
294
+ # state_machine :state, :initial => :parked do
295
+ # state nil, :idling
296
+ # ...
297
+ # end
298
+ #
299
+ # def initialize(attributes = {})
300
+ # @state = 'idling'
301
+ # initialize_state_machines(:static => :force) do
302
+ # ...
303
+ # end
304
+ # end
305
+ # end
306
+ #
307
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
308
+ # vehicle.state # => "parked"
309
+ #
310
+ # The above example is also noteworthy because it demonstrates how to avoid
311
+ # initialization issues when +nil+ is a valid state. Without passing in
312
+ # <tt>:static => :force</tt>, state_machine would never have initialized
313
+ # the state because +nil+ (the default attribute value) would have been
314
+ # interpreted as a valid current state. As a result, state_machine would
315
+ # have simply skipped initialization.
316
+ #
288
317
  # == States
289
318
  #
290
319
  # All of the valid states for the machine are automatically tracked based
@@ -44,7 +44,7 @@ module StateMachine
44
44
  contexts = @contexts
45
45
  @nodes = []
46
46
  @contexts = []
47
- @indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
47
+ @indices = @indices.inject({}) {|indices, (name, *)| indices[name] = {}; indices}
48
48
 
49
49
  # Add nodes *prior* to copying over the contexts so that they don't get
50
50
  # evaluated multiple times
@@ -74,11 +74,11 @@ module StateMachine
74
74
  @initial = options[:initial] == true
75
75
 
76
76
  if name
77
- conflicting_machines = machine.owner_class.state_machines.select {|name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
77
+ conflicting_machines = machine.owner_class.state_machines.select {|other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
78
78
 
79
79
  # Output a warning if another machine has a conflicting qualified name
80
80
  # for a different attribute
81
- if conflict = conflicting_machines.detect {|name, other_machine| other_machine.attribute != machine.attribute}
81
+ if conflict = conflicting_machines.detect {|other_name, other_machine| other_machine.attribute != machine.attribute}
82
82
  name, other_machine = conflict
83
83
  warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
84
84
  elsif conflicting_machines.empty?
@@ -125,8 +125,13 @@ module StateMachine
125
125
  # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
126
126
  # State.new(machine, :parked, :value => 1).description # => "parked (1)"
127
127
  # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
128
- def description
129
- description = name ? name.to_s : name.inspect
128
+ #
129
+ # Configuration options:
130
+ # * <tt>:human_name</tt> - Whether to use this state's human name in the
131
+ # description or just the internal name
132
+ def description(options = {})
133
+ label = options[:human_name] ? human_name : name
134
+ description = label ? label.to_s : label.inspect
130
135
  description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
131
136
  description
132
137
  end
@@ -189,6 +194,7 @@ module StateMachine
189
194
 
190
195
  # Calls the method defined by the current state of the machine
191
196
  context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
197
+ remove_method :#{method}
192
198
  def #{method}(*args, &block)
193
199
  self.class.state_machine(#{machine_name.inspect}).states.fetch(#{name.inspect}).call(self, #{method.inspect}, lambda {super(*args, &block)}, *args, &block)
194
200
  end
@@ -226,19 +232,21 @@ module StateMachine
226
232
  # * +shape+ - The actual shape of the node. If the state is a final
227
233
  # state, then "doublecircle", otherwise "ellipse".
228
234
  #
229
- # The actual node generated on the graph will be returned.
230
- def draw(graph)
231
- node = graph.add_node(name ? name.to_s : 'nil',
232
- :label => description,
235
+ # Configuration options:
236
+ # * <tt>:human_name</tt> - Whether to use the state's human name for the
237
+ # node's label that gets drawn on the graph
238
+ def draw(graph, options = {})
239
+ node = graph.add_nodes(name ? name.to_s : 'nil',
240
+ :label => description(options),
233
241
  :width => '1',
234
242
  :height => '1',
235
243
  :shape => final? ? 'doublecircle' : 'ellipse'
236
244
  )
237
245
 
238
246
  # Add open arrow for initial state
239
- graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
247
+ graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
240
248
 
241
- node
249
+ true
242
250
  end
243
251
 
244
252
  # Generates a nicely formatted description of this state's contents.
@@ -116,10 +116,10 @@ module StateMachine
116
116
 
117
117
  # Replace the configuration condition with the one configured for this
118
118
  # proxy, merging together any existing conditions
119
- options[:if] = lambda do |*args|
119
+ options[:if] = lambda do |*condition_args|
120
120
  # Block may be executed within the context of the actual object, so
121
121
  # it'll either be the first argument or the executing context
122
- object = args.first || self
122
+ object = condition_args.first || self
123
123
 
124
124
  proxy.evaluate_method(object, proxy_condition) &&
125
125
  Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
@@ -84,12 +84,19 @@ module StateMachine
84
84
  # Whether the transition is only existing temporarily for the object
85
85
  attr_writer :transient
86
86
 
87
+ # Determines whether the curreny ruby implementation supports pausing and
88
+ # resuming transitions
89
+ def self.pause_supported?
90
+ !defined?(RUBY_ENGINE) || %w(ruby maglev).include?(RUBY_ENGINE)
91
+ end
92
+
87
93
  # Creates a new, specific transition
88
94
  def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
89
95
  @object = object
90
96
  @machine = machine
91
97
  @args = []
92
98
  @transient = false
99
+ @resume_block = nil
93
100
 
94
101
  @event = machine.events.fetch(event)
95
102
  @from_state = machine.states.fetch(from_name)
@@ -354,7 +361,7 @@ module StateMachine
354
361
  # around callbacks when the remainder of the callback will be executed at
355
362
  # a later point in time.
356
363
  def pause
357
- raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' if RUBY_PLATFORM == 'java'
364
+ raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' unless self.class.pause_supported?
358
365
 
359
366
  unless @resume_block
360
367
  require 'continuation' unless defined?(callcc)
@@ -73,9 +73,10 @@ module StateMachine
73
73
  end
74
74
  end
75
75
 
76
- private
76
+ protected
77
77
  attr_reader :results #:nodoc:
78
78
 
79
+ private
79
80
  # Is this a valid set of transitions? If the collection was creating with
80
81
  # any +false+ values for transitions, then the the collection will be
81
82
  # marked as invalid.
@@ -1,3 +1,3 @@
1
1
  module StateMachine
2
- VERSION = '1.1.2'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -0,0 +1,8 @@
1
+ module StateMachine
2
+ # YARD plugin for automated documentation
3
+ module YARD
4
+ end
5
+ end
6
+
7
+ require 'state_machine/yard/handlers'
8
+ require 'state_machine/yard/templates'