telvue_state_machine 1.2.1
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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +72 -0
- data/.yardopts +5 -0
- data/Appraisals +491 -0
- data/CHANGELOG.md +502 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +1263 -0
- data/Rakefile +41 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +14 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +13 -0
- data/examples/car.rb +21 -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/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +7 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view__form.html.erb +34 -0
- data/examples/rails-rest/view_edit.html.erb +6 -0
- data/examples/rails-rest/view_index.html.erb +25 -0
- data/examples/rails-rest/view_new.html.erb +5 -0
- data/examples/rails-rest/view_show.html.erb +19 -0
- data/examples/traffic_light.rb +9 -0
- data/examples/vehicle.rb +33 -0
- data/gemfiles/active_model_3.0.0.gemfile +7 -0
- data/gemfiles/active_model_3.0.0.gemfile.lock +35 -0
- data/gemfiles/active_model_3.0.5.gemfile +7 -0
- data/gemfiles/active_model_3.0.5.gemfile.lock +35 -0
- data/gemfiles/active_model_3.1.1.gemfile +7 -0
- data/gemfiles/active_model_3.1.1.gemfile.lock +36 -0
- data/gemfiles/active_model_3.2.1.gemfile +7 -0
- data/gemfiles/active_model_3.2.12.gemfile +7 -0
- data/gemfiles/active_model_3.2.12.gemfile.lock +36 -0
- data/gemfiles/active_model_3.2.13.rc1.gemfile +7 -0
- 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 +9 -0
- data/gemfiles/active_record_2.0.0.gemfile.lock +39 -0
- data/gemfiles/active_record_2.0.5.gemfile +9 -0
- data/gemfiles/active_record_2.0.5.gemfile.lock +39 -0
- data/gemfiles/active_record_2.1.0.gemfile +9 -0
- data/gemfiles/active_record_2.1.0.gemfile.lock +39 -0
- data/gemfiles/active_record_2.1.2.gemfile +9 -0
- data/gemfiles/active_record_2.1.2.gemfile.lock +39 -0
- data/gemfiles/active_record_2.2.3.gemfile +9 -0
- data/gemfiles/active_record_2.2.3.gemfile.lock +39 -0
- data/gemfiles/active_record_2.3.12.gemfile +9 -0
- data/gemfiles/active_record_2.3.12.gemfile.lock +39 -0
- 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 +9 -0
- data/gemfiles/active_record_3.0.0.gemfile.lock +51 -0
- data/gemfiles/active_record_3.0.5.gemfile +9 -0
- data/gemfiles/active_record_3.0.5.gemfile.lock +50 -0
- data/gemfiles/active_record_3.1.1.gemfile +9 -0
- data/gemfiles/active_record_3.1.1.gemfile.lock +51 -0
- 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 +13 -0
- data/gemfiles/data_mapper_0.10.2.gemfile.lock +56 -0
- data/gemfiles/data_mapper_0.9.11.gemfile +13 -0
- data/gemfiles/data_mapper_0.9.11.gemfile.lock +71 -0
- data/gemfiles/data_mapper_0.9.4.gemfile +12 -0
- data/gemfiles/data_mapper_0.9.4.gemfile.lock +70 -0
- data/gemfiles/data_mapper_0.9.7.gemfile +13 -0
- data/gemfiles/data_mapper_0.9.7.gemfile.lock +67 -0
- data/gemfiles/data_mapper_1.0.0.gemfile +12 -0
- data/gemfiles/data_mapper_1.0.0.gemfile.lock +63 -0
- data/gemfiles/data_mapper_1.0.1.gemfile +12 -0
- data/gemfiles/data_mapper_1.0.1.gemfile.lock +63 -0
- data/gemfiles/data_mapper_1.0.2.gemfile +12 -0
- data/gemfiles/data_mapper_1.0.2.gemfile.lock +63 -0
- data/gemfiles/data_mapper_1.1.0.gemfile +12 -0
- data/gemfiles/data_mapper_1.1.0.gemfile.lock +61 -0
- data/gemfiles/data_mapper_1.2.0.gemfile +12 -0
- data/gemfiles/data_mapper_1.2.0.gemfile.lock +61 -0
- data/gemfiles/default.gemfile +7 -0
- data/gemfiles/default.gemfile.lock +27 -0
- 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 +7 -0
- data/gemfiles/graphviz_0.9.21.gemfile.lock +29 -0
- data/gemfiles/graphviz_1.0.0.gemfile +7 -0
- data/gemfiles/graphviz_1.0.0.gemfile.lock +29 -0
- 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 +8 -0
- data/gemfiles/mongo_mapper_0.10.0.gemfile.lock +47 -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 +8 -0
- data/gemfiles/mongo_mapper_0.5.5.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.5.8.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.5.8.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.6.0.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.6.0.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.6.10.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.6.10.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.7.0.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.7.0.gemfile.lock +36 -0
- data/gemfiles/mongo_mapper_0.7.5.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.7.5.gemfile.lock +39 -0
- data/gemfiles/mongo_mapper_0.8.0.gemfile +10 -0
- data/gemfiles/mongo_mapper_0.8.0.gemfile.lock +43 -0
- data/gemfiles/mongo_mapper_0.8.3.gemfile +10 -0
- data/gemfiles/mongo_mapper_0.8.3.gemfile.lock +43 -0
- data/gemfiles/mongo_mapper_0.8.4.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.8.4.gemfile.lock +42 -0
- data/gemfiles/mongo_mapper_0.8.6.gemfile +8 -0
- data/gemfiles/mongo_mapper_0.8.6.gemfile.lock +42 -0
- data/gemfiles/mongo_mapper_0.9.0.gemfile +7 -0
- data/gemfiles/mongo_mapper_0.9.0.gemfile.lock +45 -0
- data/gemfiles/mongoid_2.0.0.gemfile +9 -0
- data/gemfiles/mongoid_2.0.0.gemfile.lock +49 -0
- data/gemfiles/mongoid_2.1.4.gemfile +9 -0
- data/gemfiles/mongoid_2.1.4.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.2.4.gemfile +9 -0
- data/gemfiles/mongoid_2.2.4.gemfile.lock +47 -0
- data/gemfiles/mongoid_2.3.3.gemfile +9 -0
- data/gemfiles/mongoid_2.3.3.gemfile.lock +47 -0
- 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 +9 -0
- data/gemfiles/sequel_2.11.0.gemfile.lock +33 -0
- data/gemfiles/sequel_2.12.0.gemfile +9 -0
- data/gemfiles/sequel_2.12.0.gemfile.lock +33 -0
- data/gemfiles/sequel_2.8.0.gemfile +9 -0
- data/gemfiles/sequel_2.8.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.0.0.gemfile +9 -0
- data/gemfiles/sequel_3.0.0.gemfile.lock +33 -0
- 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 +9 -0
- data/gemfiles/sequel_3.13.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.14.0.gemfile +9 -0
- data/gemfiles/sequel_3.14.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.23.0.gemfile +9 -0
- data/gemfiles/sequel_3.23.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.24.0.gemfile +9 -0
- data/gemfiles/sequel_3.24.0.gemfile.lock +33 -0
- data/gemfiles/sequel_3.29.0.gemfile +9 -0
- data/gemfiles/sequel_3.29.0.gemfile.lock +33 -0
- 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/init.rb +1 -0
- data/lib/state_machine.rb +8 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/branch.rb +225 -0
- data/lib/state_machine/callback.rb +236 -0
- data/lib/state_machine/core.rb +12 -0
- data/lib/state_machine/core_ext.rb +2 -0
- data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +87 -0
- data/lib/state_machine/event.rb +257 -0
- data/lib/state_machine/event_collection.rb +141 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/graph.rb +92 -0
- data/lib/state_machine/helper_module.rb +17 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +25 -0
- data/lib/state_machine/integrations.rb +121 -0
- data/lib/state_machine/integrations/active_model.rb +585 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +33 -0
- data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
- data/lib/state_machine/integrations/active_model/versions.rb +31 -0
- data/lib/state_machine/integrations/active_record.rb +552 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record/versions.rb +123 -0
- data/lib/state_machine/integrations/base.rb +100 -0
- data/lib/state_machine/integrations/data_mapper.rb +511 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
- data/lib/state_machine/integrations/data_mapper/versions.rb +85 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +389 -0
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +89 -0
- data/lib/state_machine/integrations/mongoid.rb +465 -0
- data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
- data/lib/state_machine/integrations/mongoid/versions.rb +81 -0
- data/lib/state_machine/integrations/sequel.rb +486 -0
- data/lib/state_machine/integrations/sequel/versions.rb +95 -0
- data/lib/state_machine/machine.rb +2292 -0
- data/lib/state_machine/machine_collection.rb +86 -0
- data/lib/state_machine/macro_methods.rb +522 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +222 -0
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +297 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/state_context.rb +138 -0
- data/lib/state_machine/transition.rb +470 -0
- data/lib/state_machine/transition_collection.rb +245 -0
- data/lib/state_machine/version.rb +3 -0
- 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.rake +1 -0
- data/lib/tasks/state_machine.rb +30 -0
- data/lib/yard-state_machine.rb +2 -0
- data/state_machine.gemspec +22 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +15 -0
- data/test/functional/state_machine_test.rb +1066 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/branch_test.rb +969 -0
- data/test/unit/callback_test.rb +704 -0
- data/test/unit/error_test.rb +43 -0
- data/test/unit/eval_helpers_test.rb +270 -0
- data/test/unit/event_collection_test.rb +398 -0
- data/test/unit/event_test.rb +1196 -0
- data/test/unit/graph_test.rb +98 -0
- data/test/unit/helper_module_test.rb +17 -0
- data/test/unit/integrations/active_model_test.rb +1245 -0
- data/test/unit/integrations/active_record_test.rb +2551 -0
- data/test/unit/integrations/base_test.rb +104 -0
- data/test/unit/integrations/data_mapper_test.rb +2194 -0
- data/test/unit/integrations/mongo_mapper_test.rb +2026 -0
- data/test/unit/integrations/mongoid_test.rb +2309 -0
- data/test/unit/integrations/sequel_test.rb +1896 -0
- data/test/unit/integrations_test.rb +83 -0
- data/test/unit/invalid_event_test.rb +20 -0
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +115 -0
- data/test/unit/machine_collection_test.rb +603 -0
- data/test/unit/machine_test.rb +3431 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +362 -0
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +352 -0
- data/test/unit/state_context_test.rb +441 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +1101 -0
- data/test/unit/transition_collection_test.rb +2168 -0
- data/test/unit/transition_test.rb +1558 -0
- metadata +435 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'state_machine/assertions'
|
2
|
+
require 'state_machine/eval_helpers'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# A method was called in an invalid state context
|
6
|
+
class InvalidContext < Error
|
7
|
+
end
|
8
|
+
|
9
|
+
# Represents a module which will get evaluated within the context of a state.
|
10
|
+
#
|
11
|
+
# Class-level methods are proxied to the owner class, injecting a custom
|
12
|
+
# <tt>:if</tt> condition along with method. This assumes that the method has
|
13
|
+
# support for a set of configuration options, including <tt>:if</tt>. This
|
14
|
+
# condition will check that the object's state matches this context's state.
|
15
|
+
#
|
16
|
+
# Instance-level methods are used to define state-driven behavior on the
|
17
|
+
# state's owner class.
|
18
|
+
#
|
19
|
+
# == Examples
|
20
|
+
#
|
21
|
+
# class Vehicle
|
22
|
+
# class << self
|
23
|
+
# attr_accessor :validations
|
24
|
+
#
|
25
|
+
# def validate(options, &block)
|
26
|
+
# validations << options
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# self.validations = []
|
31
|
+
# attr_accessor :state, :simulate
|
32
|
+
#
|
33
|
+
# def moving?
|
34
|
+
# self.class.validations.all? {|validation| validation[:if].call(self)}
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# In the above class, a simple set of validation behaviors have been defined.
|
39
|
+
# Each validation consists of a configuration like so:
|
40
|
+
#
|
41
|
+
# Vehicle.validate :unless => :simulate
|
42
|
+
# Vehicle.validate :if => lambda {|vehicle| ...}
|
43
|
+
#
|
44
|
+
# In order to scope validations to a particular state context, the class-level
|
45
|
+
# +validate+ method can be invoked like so:
|
46
|
+
#
|
47
|
+
# machine = StateMachine::Machine.new(Vehicle)
|
48
|
+
# context = StateMachine::StateContext.new(machine.state(:first_gear))
|
49
|
+
# context.validate(:unless => :simulate)
|
50
|
+
#
|
51
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
|
52
|
+
# vehicle.moving? # => false
|
53
|
+
#
|
54
|
+
# vehicle.state = 'first_gear'
|
55
|
+
# vehicle.moving? # => true
|
56
|
+
#
|
57
|
+
# vehicle.simulate = true
|
58
|
+
# vehicle.moving? # => false
|
59
|
+
class StateContext < Module
|
60
|
+
include Assertions
|
61
|
+
include EvalHelpers
|
62
|
+
|
63
|
+
# The state machine for which this context's state is defined
|
64
|
+
attr_reader :machine
|
65
|
+
|
66
|
+
# The state that must be present in an object for this context to be active
|
67
|
+
attr_reader :state
|
68
|
+
|
69
|
+
# Creates a new context for the given state
|
70
|
+
def initialize(state)
|
71
|
+
@state = state
|
72
|
+
@machine = state.machine
|
73
|
+
|
74
|
+
state_name = state.name
|
75
|
+
machine_name = machine.name
|
76
|
+
@condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Creates a new transition that determines what to change the current state
|
80
|
+
# to when an event fires from this state.
|
81
|
+
#
|
82
|
+
# Since this transition is being defined within a state context, you do
|
83
|
+
# *not* need to specify the <tt>:from</tt> option for the transition. For
|
84
|
+
# example:
|
85
|
+
#
|
86
|
+
# state_machine do
|
87
|
+
# state :parked do
|
88
|
+
# transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
|
89
|
+
# transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# See StateMachine::Machine#transition for a description of the possible
|
94
|
+
# configurations for defining transitions.
|
95
|
+
def transition(options)
|
96
|
+
assert_valid_keys(options, :from, :to, :on, :if, :unless)
|
97
|
+
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
98
|
+
raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
|
99
|
+
|
100
|
+
machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
|
101
|
+
end
|
102
|
+
|
103
|
+
# Hooks in condition-merging to methods that don't exist in this module
|
104
|
+
def method_missing(*args, &block)
|
105
|
+
# Get the configuration
|
106
|
+
if args.last.is_a?(Hash)
|
107
|
+
options = args.last
|
108
|
+
else
|
109
|
+
args << options = {}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get any existing condition that may need to be merged
|
113
|
+
if_condition = options.delete(:if)
|
114
|
+
unless_condition = options.delete(:unless)
|
115
|
+
|
116
|
+
# Provide scope access to configuration in case the block is evaluated
|
117
|
+
# within the object instance
|
118
|
+
proxy = self
|
119
|
+
proxy_condition = @condition
|
120
|
+
|
121
|
+
# Replace the configuration condition with the one configured for this
|
122
|
+
# proxy, merging together any existing conditions
|
123
|
+
options[:if] = lambda do |*condition_args|
|
124
|
+
# Block may be executed within the context of the actual object, so
|
125
|
+
# it'll either be the first argument or the executing context
|
126
|
+
object = condition_args.first || self
|
127
|
+
|
128
|
+
proxy.evaluate_method(object, proxy_condition) &&
|
129
|
+
Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
|
130
|
+
!Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
|
131
|
+
end
|
132
|
+
|
133
|
+
# Evaluate the method on the owner class with the condition proxied
|
134
|
+
# through
|
135
|
+
machine.owner_class.send(*args, &block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,470 @@
|
|
1
|
+
require 'state_machine/transition_collection'
|
2
|
+
require 'state_machine/error'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# An invalid transition was attempted
|
6
|
+
class InvalidTransition < Error
|
7
|
+
# The machine attempting to be transitioned
|
8
|
+
attr_reader :machine
|
9
|
+
|
10
|
+
# The current state value for the machine
|
11
|
+
attr_reader :from
|
12
|
+
|
13
|
+
def initialize(object, machine, event) #:nodoc:
|
14
|
+
@machine = machine
|
15
|
+
@from_state = machine.states.match!(object)
|
16
|
+
@from = machine.read(object, :state)
|
17
|
+
@event = machine.events.fetch(event)
|
18
|
+
errors = machine.errors_for(object)
|
19
|
+
|
20
|
+
message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
|
21
|
+
message << " (Reason(s): #{errors})" unless errors.empty?
|
22
|
+
super(object, message)
|
23
|
+
end
|
24
|
+
|
25
|
+
# The event that triggered the failed transition
|
26
|
+
def event
|
27
|
+
@event.name
|
28
|
+
end
|
29
|
+
|
30
|
+
# The fully-qualified name of the event that triggered the failed transition
|
31
|
+
def qualified_event
|
32
|
+
@event.qualified_name
|
33
|
+
end
|
34
|
+
|
35
|
+
# The name for the current state
|
36
|
+
def from_name
|
37
|
+
@from_state.name
|
38
|
+
end
|
39
|
+
|
40
|
+
# The fully-qualified name for the current state
|
41
|
+
def qualified_from_name
|
42
|
+
@from_state.qualified_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# A set of transition failed to run in parallel
|
47
|
+
class InvalidParallelTransition < Error
|
48
|
+
# The set of events that failed the transition(s)
|
49
|
+
attr_reader :events
|
50
|
+
|
51
|
+
def initialize(object, events) #:nodoc:
|
52
|
+
@events = events
|
53
|
+
|
54
|
+
super(object, "Cannot run events in parallel: #{events * ', '}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# A transition represents a state change for a specific attribute.
|
59
|
+
#
|
60
|
+
# Transitions consist of:
|
61
|
+
# * An event
|
62
|
+
# * A starting state
|
63
|
+
# * An ending state
|
64
|
+
class Transition
|
65
|
+
# The object being transitioned
|
66
|
+
attr_reader :object
|
67
|
+
|
68
|
+
# The state machine for which this transition is defined
|
69
|
+
attr_reader :machine
|
70
|
+
|
71
|
+
# The original state value *before* the transition
|
72
|
+
attr_reader :from
|
73
|
+
|
74
|
+
# The new state value *after* the transition
|
75
|
+
attr_reader :to
|
76
|
+
|
77
|
+
# The arguments passed in to the event that triggered the transition
|
78
|
+
# (does not include the +run_action+ boolean argument if specified)
|
79
|
+
attr_accessor :args
|
80
|
+
|
81
|
+
# The result of invoking the action associated with the machine
|
82
|
+
attr_reader :result
|
83
|
+
|
84
|
+
# Whether the transition is only existing temporarily for the object
|
85
|
+
attr_writer :transient
|
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
|
+
|
93
|
+
# Creates a new, specific transition
|
94
|
+
def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
|
95
|
+
@object = object
|
96
|
+
@machine = machine
|
97
|
+
@args = []
|
98
|
+
@transient = false
|
99
|
+
@resume_block = nil
|
100
|
+
|
101
|
+
@event = machine.events.fetch(event)
|
102
|
+
@from_state = machine.states.fetch(from_name)
|
103
|
+
@from = read_state ? machine.read(object, :state) : @from_state.value
|
104
|
+
@to_state = machine.states.fetch(to_name)
|
105
|
+
@to = @to_state.value
|
106
|
+
|
107
|
+
reset
|
108
|
+
end
|
109
|
+
|
110
|
+
# The attribute which this transition's machine is defined for
|
111
|
+
def attribute
|
112
|
+
machine.attribute
|
113
|
+
end
|
114
|
+
|
115
|
+
# The action that will be run when this transition is performed
|
116
|
+
def action
|
117
|
+
machine.action
|
118
|
+
end
|
119
|
+
|
120
|
+
# The event that triggered the transition
|
121
|
+
def event
|
122
|
+
@event.name
|
123
|
+
end
|
124
|
+
|
125
|
+
# The fully-qualified name of the event that triggered the transition
|
126
|
+
def qualified_event
|
127
|
+
@event.qualified_name
|
128
|
+
end
|
129
|
+
|
130
|
+
# The human-readable name of the event that triggered the transition
|
131
|
+
def human_event
|
132
|
+
@event.human_name(@object.class)
|
133
|
+
end
|
134
|
+
|
135
|
+
# The state name *before* the transition
|
136
|
+
def from_name
|
137
|
+
@from_state.name
|
138
|
+
end
|
139
|
+
|
140
|
+
# The fully-qualified state name *before* the transition
|
141
|
+
def qualified_from_name
|
142
|
+
@from_state.qualified_name
|
143
|
+
end
|
144
|
+
|
145
|
+
# The human-readable state name *before* the transition
|
146
|
+
def human_from_name
|
147
|
+
@from_state.human_name(@object.class)
|
148
|
+
end
|
149
|
+
|
150
|
+
# The new state name *after* the transition
|
151
|
+
def to_name
|
152
|
+
@to_state.name
|
153
|
+
end
|
154
|
+
|
155
|
+
# The new fully-qualified state name *after* the transition
|
156
|
+
def qualified_to_name
|
157
|
+
@to_state.qualified_name
|
158
|
+
end
|
159
|
+
|
160
|
+
# The new human-readable state name *after* the transition
|
161
|
+
def human_to_name
|
162
|
+
@to_state.human_name(@object.class)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Does this transition represent a loopback (i.e. the from and to state
|
166
|
+
# are the same)
|
167
|
+
#
|
168
|
+
# == Example
|
169
|
+
#
|
170
|
+
# machine = StateMachine.new(Vehicle)
|
171
|
+
# StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
|
172
|
+
# StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
|
173
|
+
def loopback?
|
174
|
+
from_name == to_name
|
175
|
+
end
|
176
|
+
|
177
|
+
# Is this transition existing for a short period only? If this is set, it
|
178
|
+
# indicates that the transition (or the event backing it) should not be
|
179
|
+
# written to the object if it fails.
|
180
|
+
def transient?
|
181
|
+
@transient
|
182
|
+
end
|
183
|
+
|
184
|
+
# A hash of all the core attributes defined for this transition with their
|
185
|
+
# names as keys and values of the attributes as values.
|
186
|
+
#
|
187
|
+
# == Example
|
188
|
+
#
|
189
|
+
# machine = StateMachine.new(Vehicle)
|
190
|
+
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
191
|
+
# transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
|
192
|
+
def attributes
|
193
|
+
@attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
|
194
|
+
end
|
195
|
+
|
196
|
+
# Runs the actual transition and any before/after callbacks associated
|
197
|
+
# with the transition. The action associated with the transition/machine
|
198
|
+
# can be skipped by passing in +false+.
|
199
|
+
#
|
200
|
+
# == Examples
|
201
|
+
#
|
202
|
+
# class Vehicle
|
203
|
+
# state_machine :action => :save do
|
204
|
+
# ...
|
205
|
+
# end
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# vehicle = Vehicle.new
|
209
|
+
# transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
|
210
|
+
# transition.perform # => Runs the +save+ action after setting the state attribute
|
211
|
+
# transition.perform(false) # => Only sets the state attribute
|
212
|
+
# transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
|
213
|
+
# transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
|
214
|
+
def perform(*args)
|
215
|
+
run_action = [true, false].include?(args.last) ? args.pop : true
|
216
|
+
self.args = args
|
217
|
+
|
218
|
+
# Run the transition
|
219
|
+
!!TransitionCollection.new([self], :actions => run_action).perform
|
220
|
+
end
|
221
|
+
|
222
|
+
# Runs a block within a transaction for the object being transitioned.
|
223
|
+
# By default, transactions are a no-op unless otherwise defined by the
|
224
|
+
# machine's integration.
|
225
|
+
def within_transaction
|
226
|
+
machine.within_transaction(object) do
|
227
|
+
yield
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Runs the before / after callbacks for this transition. If a block is
|
232
|
+
# provided, then it will be executed between the before and after callbacks.
|
233
|
+
#
|
234
|
+
# Configuration options:
|
235
|
+
# * +before+ - Whether to run before callbacks.
|
236
|
+
# * +after+ - Whether to run after callbacks. If false, then any around
|
237
|
+
# callbacks will be paused until called again with +after+ enabled.
|
238
|
+
# Default is true.
|
239
|
+
#
|
240
|
+
# This will return true if all before callbacks gets executed. After
|
241
|
+
# callbacks will not have an effect on the result.
|
242
|
+
def run_callbacks(options = {}, &block)
|
243
|
+
options = {:before => true, :after => true}.merge(options)
|
244
|
+
@success = false
|
245
|
+
|
246
|
+
halted = pausable { before(options[:after], &block) } if options[:before]
|
247
|
+
|
248
|
+
# After callbacks are only run if:
|
249
|
+
# * An around callback didn't halt after yielding
|
250
|
+
# * They're enabled or the run didn't succeed
|
251
|
+
after if !(@before_run && halted) && (options[:after] || !@success)
|
252
|
+
|
253
|
+
@before_run
|
254
|
+
end
|
255
|
+
|
256
|
+
# Transitions the current value of the state to that specified by the
|
257
|
+
# transition. Once the state is persisted, it cannot be persisted again
|
258
|
+
# until this transition is reset.
|
259
|
+
#
|
260
|
+
# == Example
|
261
|
+
#
|
262
|
+
# class Vehicle
|
263
|
+
# state_machine do
|
264
|
+
# event :ignite do
|
265
|
+
# transition :parked => :idling
|
266
|
+
# end
|
267
|
+
# end
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# vehicle = Vehicle.new
|
271
|
+
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
272
|
+
# transition.persist
|
273
|
+
#
|
274
|
+
# vehicle.state # => 'idling'
|
275
|
+
def persist
|
276
|
+
unless @persisted
|
277
|
+
machine.write(object, :state, to)
|
278
|
+
@persisted = true
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Rolls back changes made to the object's state via this transition. This
|
283
|
+
# will revert the state back to the +from+ value.
|
284
|
+
#
|
285
|
+
# == Example
|
286
|
+
#
|
287
|
+
# class Vehicle
|
288
|
+
# state_machine :initial => :parked do
|
289
|
+
# event :ignite do
|
290
|
+
# transition :parked => :idling
|
291
|
+
# end
|
292
|
+
# end
|
293
|
+
# end
|
294
|
+
#
|
295
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
|
296
|
+
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
297
|
+
#
|
298
|
+
# # Persist the new state
|
299
|
+
# vehicle.state # => "parked"
|
300
|
+
# transition.persist
|
301
|
+
# vehicle.state # => "idling"
|
302
|
+
#
|
303
|
+
# # Roll back to the original state
|
304
|
+
# transition.rollback
|
305
|
+
# vehicle.state # => "parked"
|
306
|
+
def rollback
|
307
|
+
reset
|
308
|
+
machine.write(object, :state, from)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Resets any tracking of which callbacks have already been run and whether
|
312
|
+
# the state has already been persisted
|
313
|
+
def reset
|
314
|
+
@before_run = @persisted = @after_run = false
|
315
|
+
@paused_block = nil
|
316
|
+
end
|
317
|
+
|
318
|
+
# Determines equality of transitions by testing whether the object, states,
|
319
|
+
# and event involved in the transition are equal
|
320
|
+
def ==(other)
|
321
|
+
other.instance_of?(self.class) &&
|
322
|
+
other.object == object &&
|
323
|
+
other.machine == machine &&
|
324
|
+
other.from_name == from_name &&
|
325
|
+
other.to_name == to_name &&
|
326
|
+
other.event == event
|
327
|
+
end
|
328
|
+
|
329
|
+
# Generates a nicely formatted description of this transitions's contents.
|
330
|
+
#
|
331
|
+
# For example,
|
332
|
+
#
|
333
|
+
# transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
|
334
|
+
# transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
335
|
+
def inspect
|
336
|
+
"#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
|
337
|
+
end
|
338
|
+
|
339
|
+
private
|
340
|
+
# Runs a block that may get paused. If the block doesn't pause, then
|
341
|
+
# execution will continue as normal. If the block gets paused, then it
|
342
|
+
# will take care of switching the execution context when it's resumed.
|
343
|
+
#
|
344
|
+
# This will return true if the given block halts for a reason other than
|
345
|
+
# getting paused.
|
346
|
+
def pausable
|
347
|
+
begin
|
348
|
+
halted = !catch(:halt) { yield; true }
|
349
|
+
rescue Exception => error
|
350
|
+
raise unless @resume_block
|
351
|
+
end
|
352
|
+
|
353
|
+
if @resume_block
|
354
|
+
@resume_block.call(halted, error)
|
355
|
+
else
|
356
|
+
halted
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# Pauses the current callback execution. This should only occur within
|
361
|
+
# around callbacks when the remainder of the callback will be executed at
|
362
|
+
# a later point in time.
|
363
|
+
def pause
|
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?
|
365
|
+
|
366
|
+
unless @resume_block
|
367
|
+
require 'continuation' unless defined?(callcc)
|
368
|
+
callcc do |block|
|
369
|
+
@paused_block = block
|
370
|
+
throw :halt, true
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Resumes the execution of a previously paused callback execution. Once
|
376
|
+
# the paused callbacks complete, the current execution will continue.
|
377
|
+
def resume
|
378
|
+
if @paused_block
|
379
|
+
halted, error = callcc do |block|
|
380
|
+
@resume_block = block
|
381
|
+
@paused_block.call
|
382
|
+
end
|
383
|
+
|
384
|
+
@resume_block = @paused_block = nil
|
385
|
+
|
386
|
+
raise error if error
|
387
|
+
!halted
|
388
|
+
else
|
389
|
+
true
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Runs the machine's +before+ callbacks for this transition. Only
|
394
|
+
# callbacks that are configured to match the event, from state, and to
|
395
|
+
# state will be invoked.
|
396
|
+
#
|
397
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
398
|
+
# is reset.
|
399
|
+
def before(complete = true, index = 0, &block)
|
400
|
+
unless @before_run
|
401
|
+
while callback = machine.callbacks[:before][index]
|
402
|
+
index += 1
|
403
|
+
|
404
|
+
if callback.type == :around
|
405
|
+
# Around callback: need to handle recursively. Execution only gets
|
406
|
+
# paused if:
|
407
|
+
# * The block fails and the callback doesn't run on failures OR
|
408
|
+
# * The block succeeds, but after callbacks are disabled (in which
|
409
|
+
# case a continuation is stored for later execution)
|
410
|
+
return if catch(:cancel) do
|
411
|
+
callback.call(object, context, self) do
|
412
|
+
before(complete, index, &block)
|
413
|
+
|
414
|
+
pause if @success && !complete
|
415
|
+
throw :cancel, true unless @success
|
416
|
+
end
|
417
|
+
end
|
418
|
+
else
|
419
|
+
# Normal before callback
|
420
|
+
callback.call(object, context, self)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
@before_run = true
|
425
|
+
end
|
426
|
+
|
427
|
+
action = {:success => true}.merge(block_given? ? yield : {})
|
428
|
+
@result, @success = action[:result], action[:success]
|
429
|
+
end
|
430
|
+
|
431
|
+
# Runs the machine's +after+ callbacks for this transition. Only
|
432
|
+
# callbacks that are configured to match the event, from state, and to
|
433
|
+
# state will be invoked.
|
434
|
+
#
|
435
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
436
|
+
# is reset.
|
437
|
+
#
|
438
|
+
# == Halting
|
439
|
+
#
|
440
|
+
# If any callback throws a <tt>:halt</tt> exception, it will be caught
|
441
|
+
# and the callback chain will be automatically stopped. However, this
|
442
|
+
# exception will not bubble up to the caller since +after+ callbacks
|
443
|
+
# should never halt the execution of a +perform+.
|
444
|
+
def after
|
445
|
+
unless @after_run
|
446
|
+
# First resume previously paused callbacks
|
447
|
+
if resume
|
448
|
+
catch(:halt) do
|
449
|
+
type = @success ? :after : :failure
|
450
|
+
machine.callbacks[type].each {|callback| callback.call(object, context, self)}
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
@after_run = true
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Gets a hash of the context defining this unique transition (including
|
459
|
+
# event, from state, and to state).
|
460
|
+
#
|
461
|
+
# == Example
|
462
|
+
#
|
463
|
+
# machine = StateMachine.new(Vehicle)
|
464
|
+
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
465
|
+
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
|
466
|
+
def context
|
467
|
+
@context ||= {:on => event, :from => from_name, :to => to_name}
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|