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.
- data/.gitignore +7 -11
- data/.travis.yml +49 -7
- data/Appraisals +255 -87
- data/CHANGELOG.md +30 -0
- data/README.md +142 -21
- data/Rakefile +1 -11
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +14 -0
- data/examples/auto_shop.rb +2 -0
- data/examples/car.rb +2 -0
- data/examples/doc/AutoShop.html +2856 -0
- data/examples/doc/AutoShop_state.png +0 -0
- data/examples/doc/Car.html +919 -0
- data/examples/doc/Car_state.png +0 -0
- data/examples/doc/TrafficLight.html +2230 -0
- data/examples/doc/TrafficLight_state.png +0 -0
- data/examples/doc/Vehicle.html +7921 -0
- data/examples/doc/Vehicle_state.png +0 -0
- data/examples/doc/_index.html +136 -0
- data/examples/doc/class_list.html +47 -0
- data/examples/doc/css/common.css +1 -0
- data/examples/doc/css/full_list.css +55 -0
- data/examples/doc/css/style.css +322 -0
- data/examples/doc/file_list.html +46 -0
- data/examples/doc/frames.html +13 -0
- data/examples/doc/index.html +136 -0
- data/examples/doc/js/app.js +205 -0
- data/examples/doc/js/full_list.js +173 -0
- data/examples/doc/js/jquery.js +16 -0
- data/examples/doc/method_list.html +734 -0
- data/examples/doc/top-level-namespace.html +105 -0
- data/examples/rails-rest/migration.rb +1 -5
- data/examples/rails-rest/view__form.html.erb +34 -0
- data/examples/rails-rest/view_edit.html.erb +2 -21
- data/examples/rails-rest/view_index.html.erb +6 -4
- data/examples/rails-rest/view_new.html.erb +2 -11
- data/examples/rails-rest/view_show.html.erb +5 -3
- data/examples/traffic_light.rb +2 -0
- data/examples/vehicle.rb +2 -0
- data/gemfiles/active_model-3.0.0.gemfile.lock +9 -6
- data/gemfiles/active_model-3.0.5.gemfile.lock +10 -7
- data/gemfiles/active_model-3.1.1.gemfile.lock +12 -10
- data/gemfiles/{active_model-3.2.0.gemfile → active_model-3.2.1.gemfile} +1 -1
- data/gemfiles/{graphviz-0.9.0.gemfile → active_model-3.2.12.gemfile} +1 -1
- data/gemfiles/active_model-3.2.12.gemfile.lock +36 -0
- data/gemfiles/{active_record-3.2.0.gemfile → active_model-3.2.13.rc1.gemfile} +1 -2
- data/gemfiles/active_model-3.2.13.rc1.gemfile.lock +36 -0
- data/gemfiles/active_model-4.0.0.gemfile +9 -0
- data/gemfiles/active_model-4.0.0.gemfile.lock +78 -0
- data/gemfiles/active_record-2.0.0.gemfile +2 -1
- data/gemfiles/active_record-2.0.0.gemfile.lock +15 -6
- data/gemfiles/active_record-2.0.5.gemfile +2 -1
- data/gemfiles/active_record-2.0.5.gemfile.lock +15 -6
- data/gemfiles/active_record-2.1.0.gemfile +2 -1
- data/gemfiles/active_record-2.1.0.gemfile.lock +15 -6
- data/gemfiles/active_record-2.1.2.gemfile +2 -1
- data/gemfiles/active_record-2.1.2.gemfile.lock +15 -6
- data/gemfiles/active_record-2.2.3.gemfile +2 -1
- data/gemfiles/active_record-2.2.3.gemfile.lock +15 -6
- data/gemfiles/active_record-2.3.12.gemfile +2 -1
- data/gemfiles/active_record-2.3.12.gemfile.lock +15 -6
- data/gemfiles/active_record-2.3.5.gemfile +9 -0
- data/gemfiles/active_record-2.3.5.gemfile.lock +39 -0
- data/gemfiles/active_record-3.0.0.gemfile +2 -1
- data/gemfiles/active_record-3.0.0.gemfile.lock +18 -11
- data/gemfiles/active_record-3.0.5.gemfile +2 -1
- data/gemfiles/active_record-3.0.5.gemfile.lock +19 -12
- data/gemfiles/active_record-3.1.1.gemfile +2 -1
- data/gemfiles/active_record-3.1.1.gemfile.lock +22 -16
- data/gemfiles/active_record-3.2.12.gemfile +9 -0
- data/gemfiles/active_record-3.2.12.gemfile.lock +51 -0
- data/gemfiles/active_record-3.2.13.rc1.gemfile +9 -0
- data/gemfiles/active_record-3.2.13.rc1.gemfile.lock +51 -0
- data/gemfiles/active_record-4.0.0.gemfile +11 -0
- data/gemfiles/active_record-4.0.0.gemfile.lock +83 -0
- data/gemfiles/data_mapper-0.10.2.gemfile +1 -0
- data/gemfiles/data_mapper-0.10.2.gemfile.lock +13 -9
- data/gemfiles/data_mapper-0.9.11.gemfile +1 -0
- data/gemfiles/data_mapper-0.9.11.gemfile.lock +31 -7
- data/gemfiles/data_mapper-0.9.4.gemfile.lock +25 -14
- data/gemfiles/data_mapper-0.9.7.gemfile +1 -0
- data/gemfiles/data_mapper-0.9.7.gemfile.lock +27 -15
- data/gemfiles/data_mapper-1.0.0.gemfile.lock +20 -17
- data/gemfiles/data_mapper-1.0.1.gemfile.lock +20 -17
- data/gemfiles/data_mapper-1.0.2.gemfile.lock +20 -17
- data/gemfiles/data_mapper-1.1.0.gemfile.lock +19 -16
- data/gemfiles/data_mapper-1.2.0.gemfile.lock +19 -16
- data/gemfiles/default.gemfile.lock +8 -5
- data/gemfiles/graphviz-0.9.17.gemfile +7 -0
- data/gemfiles/graphviz-0.9.17.gemfile.lock +29 -0
- data/gemfiles/graphviz-0.9.21.gemfile.lock +7 -4
- data/gemfiles/graphviz-1.0.0.gemfile.lock +7 -4
- data/gemfiles/graphviz-1.0.3.gemfile +7 -0
- data/gemfiles/graphviz-1.0.3.gemfile.lock +29 -0
- data/gemfiles/graphviz-1.0.8.gemfile +7 -0
- data/gemfiles/graphviz-1.0.8.gemfile.lock +29 -0
- data/gemfiles/mongo_mapper-0.10.0.gemfile +1 -0
- data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +14 -11
- data/gemfiles/mongo_mapper-0.11.1.gemfile +7 -0
- data/gemfiles/mongo_mapper-0.11.1.gemfile.lock +44 -0
- data/gemfiles/mongo_mapper-0.11.2.gemfile +9 -0
- data/gemfiles/mongo_mapper-0.11.2.gemfile.lock +48 -0
- data/gemfiles/mongo_mapper-0.12.0.gemfile +9 -0
- data/gemfiles/mongo_mapper-0.12.0.gemfile.lock +48 -0
- data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +7 -4
- data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +7 -4
- data/gemfiles/mongoid-2.0.0.gemfile +2 -0
- data/gemfiles/mongoid-2.0.0.gemfile.lock +22 -18
- data/gemfiles/mongoid-2.1.4.gemfile +2 -0
- data/gemfiles/mongoid-2.1.4.gemfile.lock +21 -17
- data/gemfiles/mongoid-2.2.4.gemfile +2 -0
- data/gemfiles/mongoid-2.2.4.gemfile.lock +21 -17
- data/gemfiles/mongoid-2.3.3.gemfile +2 -0
- data/gemfiles/mongoid-2.3.3.gemfile.lock +21 -17
- data/gemfiles/mongoid-2.4.0.gemfile +9 -0
- data/gemfiles/mongoid-2.4.0.gemfile.lock +47 -0
- data/gemfiles/mongoid-2.4.10.gemfile +9 -0
- data/gemfiles/mongoid-2.4.10.gemfile.lock +47 -0
- data/gemfiles/mongoid-2.5.2.gemfile +9 -0
- data/gemfiles/mongoid-2.5.2.gemfile.lock +47 -0
- data/gemfiles/mongoid-2.6.0.gemfile +9 -0
- data/gemfiles/mongoid-2.6.0.gemfile.lock +47 -0
- data/gemfiles/mongoid-3.0.0.gemfile +8 -0
- data/gemfiles/mongoid-3.0.0.gemfile.lock +45 -0
- data/gemfiles/mongoid-3.0.22.gemfile +8 -0
- data/gemfiles/mongoid-3.0.22.gemfile.lock +45 -0
- data/gemfiles/mongoid-3.1.0.gemfile +8 -0
- data/gemfiles/mongoid-3.1.0.gemfile.lock +45 -0
- data/gemfiles/sequel-2.11.0.gemfile +2 -1
- data/gemfiles/sequel-2.11.0.gemfile.lock +11 -6
- data/gemfiles/sequel-2.12.0.gemfile +2 -1
- data/gemfiles/sequel-2.12.0.gemfile.lock +11 -6
- data/gemfiles/sequel-2.8.0.gemfile +2 -1
- data/gemfiles/sequel-2.8.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.0.0.gemfile +2 -1
- data/gemfiles/sequel-3.0.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.10.0.gemfile +9 -0
- data/gemfiles/sequel-3.10.0.gemfile.lock +33 -0
- data/gemfiles/sequel-3.13.0.gemfile +2 -1
- data/gemfiles/sequel-3.13.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.14.0.gemfile +2 -1
- data/gemfiles/sequel-3.14.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.23.0.gemfile +2 -1
- data/gemfiles/sequel-3.23.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.24.0.gemfile +2 -1
- data/gemfiles/sequel-3.24.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.29.0.gemfile +2 -1
- data/gemfiles/sequel-3.29.0.gemfile.lock +11 -6
- data/gemfiles/sequel-3.34.0.gemfile +9 -0
- data/gemfiles/sequel-3.34.0.gemfile.lock +33 -0
- data/gemfiles/sequel-3.35.0.gemfile +9 -0
- data/gemfiles/sequel-3.35.0.gemfile.lock +33 -0
- data/gemfiles/sequel-3.4.0.gemfile +9 -0
- data/gemfiles/sequel-3.4.0.gemfile.lock +33 -0
- data/gemfiles/sequel-3.44.0.gemfile +9 -0
- data/gemfiles/sequel-3.44.0.gemfile.lock +33 -0
- data/lib/state_machine.rb +6 -0
- data/lib/state_machine/branch.rb +9 -8
- data/lib/state_machine/callback.rb +2 -2
- data/lib/state_machine/core.rb +10 -0
- data/lib/state_machine/core_ext.rb +1 -0
- data/lib/state_machine/eval_helpers.rb +5 -3
- data/lib/state_machine/event.rb +17 -6
- data/lib/state_machine/graph.rb +92 -0
- data/lib/state_machine/integrations.rb +13 -1
- data/lib/state_machine/integrations/active_model.rb +14 -20
- data/lib/state_machine/integrations/active_model/observer.rb +3 -3
- data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
- data/lib/state_machine/integrations/active_record.rb +52 -25
- data/lib/state_machine/integrations/active_record/locale.rb +1 -1
- data/lib/state_machine/integrations/active_record/versions.rb +1 -17
- data/lib/state_machine/integrations/base.rb +15 -6
- data/lib/state_machine/integrations/data_mapper.rb +98 -35
- data/lib/state_machine/integrations/data_mapper/versions.rb +46 -8
- data/lib/state_machine/integrations/mongo_mapper.rb +39 -12
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +1 -1
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +3 -20
- data/lib/state_machine/integrations/mongoid.rb +52 -14
- data/lib/state_machine/integrations/mongoid/locale.rb +1 -1
- data/lib/state_machine/integrations/mongoid/versions.rb +52 -26
- data/lib/state_machine/integrations/sequel.rb +82 -33
- data/lib/state_machine/integrations/sequel/versions.rb +19 -44
- data/lib/state_machine/machine.rb +99 -59
- data/lib/state_machine/machine_collection.rb +1 -2
- data/lib/state_machine/macro_methods.rb +29 -0
- data/lib/state_machine/node_collection.rb +1 -1
- data/lib/state_machine/state.rb +18 -10
- data/lib/state_machine/state_context.rb +2 -2
- data/lib/state_machine/transition.rb +8 -1
- data/lib/state_machine/transition_collection.rb +2 -1
- data/lib/state_machine/version.rb +1 -1
- data/lib/state_machine/yard.rb +8 -0
- data/lib/state_machine/yard/handlers.rb +12 -0
- data/lib/state_machine/yard/handlers/base.rb +32 -0
- data/lib/state_machine/yard/handlers/event.rb +25 -0
- data/lib/state_machine/yard/handlers/machine.rb +344 -0
- data/lib/state_machine/yard/handlers/state.rb +25 -0
- data/lib/state_machine/yard/handlers/transition.rb +47 -0
- data/lib/state_machine/yard/templates.rb +3 -0
- data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
- data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
- data/lib/tasks/state_machine.rb +2 -1
- data/lib/yard-state_machine.rb +2 -0
- data/state_machine.gemspec +4 -3
- data/test/files/switch.rb +4 -0
- data/test/test_helper.rb +5 -0
- data/test/unit/branch_test.rb +117 -36
- data/test/unit/callback_test.rb +5 -2
- data/test/unit/eval_helpers_test.rb +49 -1
- data/test/unit/event_collection_test.rb +3 -1
- data/test/unit/event_test.rb +182 -12
- data/test/unit/graph_test.rb +98 -0
- data/test/unit/integrations/active_model_test.rb +82 -12
- data/test/unit/integrations/active_record_test.rb +393 -37
- data/test/unit/integrations/base_test.rb +7 -2
- data/test/unit/integrations/data_mapper_test.rb +326 -72
- data/test/unit/integrations/mongo_mapper_test.rb +338 -44
- data/test/unit/integrations/mongoid_test.rb +606 -98
- data/test/unit/integrations/sequel_test.rb +429 -102
- data/test/unit/integrations_test.rb +28 -6
- data/test/unit/machine_collection_test.rb +6 -2
- data/test/unit/machine_test.rb +134 -82
- data/test/unit/node_collection_test.rb +2 -2
- data/test/unit/path_test.rb +1 -1
- data/test/unit/state_test.rb +65 -21
- data/test/unit/transition_collection_test.rb +43 -23
- data/test/unit/transition_test.rb +8 -2
- metadata +303 -221
- data/gemfiles/active_model-3.2.0.gemfile.lock +0 -32
- data/gemfiles/active_record-3.2.0.gemfile.lock +0 -43
- 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
|
-
|
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])
|
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 |*
|
753
|
-
block.call((scope == :instance ? self.class : self).state_machine(name), self, *
|
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
|
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
|
-
#
|
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])
|
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>:
|
1867
|
-
|
1868
|
-
|
1869
|
-
|
1870
|
-
|
1871
|
-
|
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
|
-
|
1878
|
-
|
1879
|
-
|
1880
|
-
|
1881
|
-
|
1882
|
-
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
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
|
-
[
|
2146
|
-
[
|
2147
|
-
method = "#{kind}_#{
|
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 =>
|
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,
|
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
|
data/lib/state_machine/state.rb
CHANGED
@@ -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 {|
|
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 {|
|
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
|
-
|
129
|
-
|
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
|
-
#
|
230
|
-
|
231
|
-
|
232
|
-
|
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.
|
247
|
+
graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
|
240
248
|
|
241
|
-
|
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 |*
|
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 =
|
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.'
|
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
|
-
|
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.
|