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
@@ -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'