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,149 @@
|
|
1
|
+
require 'state_machine/machine_collection'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
module ClassMethods
|
5
|
+
def self.extended(base) #:nodoc:
|
6
|
+
base.class_eval do
|
7
|
+
@state_machines = MachineCollection.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Gets the current list of state machines defined for this class. This
|
12
|
+
# class-level attribute acts like an inheritable attribute. The attribute
|
13
|
+
# is available to each subclass, each having a copy of its superclass's
|
14
|
+
# attribute.
|
15
|
+
#
|
16
|
+
# The hash of state machines maps <tt>:attribute</tt> => +machine+, e.g.
|
17
|
+
#
|
18
|
+
# Vehicle.state_machines # => {:state => #<StateMachine::Machine:0xb6f6e4a4 ...>}
|
19
|
+
def state_machines
|
20
|
+
@state_machines ||= superclass.state_machines.dup
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
# Runs one or more events in parallel. All events will run through the
|
26
|
+
# following steps:
|
27
|
+
# * Before callbacks
|
28
|
+
# * Persist state
|
29
|
+
# * Invoke action
|
30
|
+
# * After callbacks
|
31
|
+
#
|
32
|
+
# For example, if two events (for state machines A and B) are run in
|
33
|
+
# parallel, the order in which steps are run is:
|
34
|
+
# * A - Before transition callbacks
|
35
|
+
# * B - Before transition callbacks
|
36
|
+
# * A - Persist new state
|
37
|
+
# * B - Persist new state
|
38
|
+
# * A - Invoke action
|
39
|
+
# * B - Invoke action (only if different than A's action)
|
40
|
+
# * A - After transition callbacks
|
41
|
+
# * B - After transition callbacks
|
42
|
+
#
|
43
|
+
# *Note* that multiple events on the same state machine / attribute cannot
|
44
|
+
# be run in parallel. If this is attempted, an ArgumentError will be
|
45
|
+
# raised.
|
46
|
+
#
|
47
|
+
# == Halting callbacks
|
48
|
+
#
|
49
|
+
# When running multiple events in parallel, special consideration should
|
50
|
+
# be taken with regard to how halting within callbacks affects the flow.
|
51
|
+
#
|
52
|
+
# For *before* callbacks, any <tt>:halt</tt> error that's thrown will
|
53
|
+
# immediately cancel the perform for all transitions. As a result, it's
|
54
|
+
# possible for one event's transition to affect the continuation of
|
55
|
+
# another.
|
56
|
+
#
|
57
|
+
# On the other hand, any <tt>:halt</tt> error that's thrown within an
|
58
|
+
# *after* callback with only affect that event's transition. Other
|
59
|
+
# transitions will continue to run their own callbacks.
|
60
|
+
#
|
61
|
+
# == Example
|
62
|
+
#
|
63
|
+
# class Vehicle
|
64
|
+
# state_machine :initial => :parked do
|
65
|
+
# event :ignite do
|
66
|
+
# transition :parked => :idling
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# event :park do
|
70
|
+
# transition :idling => :parked
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :on do
|
75
|
+
# event :enable do
|
76
|
+
# transition all => :active
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# event :disable do
|
80
|
+
# transition all => :off
|
81
|
+
# end
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
86
|
+
# vehicle.state # => "parked"
|
87
|
+
# vehicle.alarm_state # => "active"
|
88
|
+
#
|
89
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
90
|
+
# vehicle.state # => "idling"
|
91
|
+
# vehicle.alarm_state # => "off"
|
92
|
+
#
|
93
|
+
# # If any event fails, the entire event chain fails
|
94
|
+
# vehicle.fire_events(:ignite, :enable_alarm) # => false
|
95
|
+
# vehicle.state # => "idling"
|
96
|
+
# vehicle.alarm_state # => "off"
|
97
|
+
#
|
98
|
+
# # Exception raised on invalid event
|
99
|
+
# vehicle.fire_events(:park, :invalid) # => StateMachine::InvalidEvent: :invalid is an unknown event
|
100
|
+
# vehicle.state # => "idling"
|
101
|
+
# vehicle.alarm_state # => "off"
|
102
|
+
def fire_events(*events)
|
103
|
+
self.class.state_machines.fire_events(self, *events)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Run one or more events in parallel. If any event fails to run, then
|
107
|
+
# a StateMachine::InvalidTransition exception will be raised.
|
108
|
+
#
|
109
|
+
# See StateMachine::InstanceMethods#fire_events for more information.
|
110
|
+
#
|
111
|
+
# == Example
|
112
|
+
#
|
113
|
+
# class Vehicle
|
114
|
+
# state_machine :initial => :parked do
|
115
|
+
# event :ignite do
|
116
|
+
# transition :parked => :idling
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# event :park do
|
120
|
+
# transition :idling => :parked
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# state_machine :alarm_state, :namespace => 'alarm', :initial => :active do
|
125
|
+
# event :enable do
|
126
|
+
# transition all => :active
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# event :disable do
|
130
|
+
# transition all => :off
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
|
136
|
+
# vehicle.fire_events(:ignite, :disable_alarm) # => true
|
137
|
+
#
|
138
|
+
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachine::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
|
139
|
+
def fire_events!(*events)
|
140
|
+
run_action = [true, false].include?(events.last) ? events.pop : true
|
141
|
+
fire_events(*(events + [run_action])) || raise(StateMachine::InvalidParallelTransition.new(self, events))
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
def initialize_state_machines(options = {}, &block) #:nodoc:
|
146
|
+
self.class.state_machines.initialize_states(self, options, &block)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'ruby-graphviz', '>=0.9.17'
|
4
|
+
require 'graphviz'
|
5
|
+
rescue LoadError => ex
|
6
|
+
$stderr.puts "Cannot draw the machine (#{ex.message}). `gem install ruby-graphviz` >= v0.9.17 and try again."
|
7
|
+
raise
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'state_machine/assertions'
|
11
|
+
|
12
|
+
module StateMachine
|
13
|
+
# Provides a set of higher-order features on top of the raw GraphViz graphs
|
14
|
+
class Graph < GraphViz
|
15
|
+
include Assertions
|
16
|
+
|
17
|
+
# The name of the font to draw state names in
|
18
|
+
attr_reader :font
|
19
|
+
|
20
|
+
# The graph's full filename
|
21
|
+
attr_reader :file_path
|
22
|
+
|
23
|
+
# The image format to generate the graph in
|
24
|
+
attr_reader :file_format
|
25
|
+
|
26
|
+
# Creates a new graph with the given name.
|
27
|
+
#
|
28
|
+
# Configuration options:
|
29
|
+
# * <tt>:path</tt> - The path to write the graph file to. Default is the
|
30
|
+
# current directory (".").
|
31
|
+
# * <tt>:format</tt> - The image format to generate the graph in.
|
32
|
+
# Default is "png'.
|
33
|
+
# * <tt>:font</tt> - The name of the font to draw state names in.
|
34
|
+
# Default is "Arial".
|
35
|
+
# * <tt>:orientation</tt> - The direction of the graph ("portrait" or
|
36
|
+
# "landscape"). Default is "portrait".
|
37
|
+
def initialize(name, options = {})
|
38
|
+
options = {:path => '.', :format => 'png', :font => 'Arial', :orientation => 'portrait'}.merge(options)
|
39
|
+
assert_valid_keys(options, :path, :format, :font, :orientation)
|
40
|
+
|
41
|
+
@font = options[:font]
|
42
|
+
@file_path = File.join(options[:path], "#{name}.#{options[:format]}")
|
43
|
+
@file_format = options[:format]
|
44
|
+
|
45
|
+
super('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB')
|
46
|
+
end
|
47
|
+
|
48
|
+
# Generates the actual image file based on the nodes / edges added to the
|
49
|
+
# graph. The path to the file is based on the configuration options for
|
50
|
+
# this graph.
|
51
|
+
def output
|
52
|
+
super(@file_format => @file_path)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Adds a new node to the graph. The font for the node will be automatically
|
56
|
+
# set based on the graph configuration. The generated node will be returned.
|
57
|
+
#
|
58
|
+
# For example,
|
59
|
+
#
|
60
|
+
# graph = StateMachine::Graph.new('test')
|
61
|
+
# graph.add_nodes('parked', :label => 'Parked', :width => '1', :height => '1', :shape => 'ellipse')
|
62
|
+
def add_nodes(*args)
|
63
|
+
node = v0_api? ? add_node(*args) : super
|
64
|
+
node.fontname = @font
|
65
|
+
node
|
66
|
+
end
|
67
|
+
|
68
|
+
# Adds a new edge to the graph. The font for the edge will be automatically
|
69
|
+
# set based on the graph configuration. The generated edge will be returned.
|
70
|
+
#
|
71
|
+
# For example,
|
72
|
+
#
|
73
|
+
# graph = StateMachine::Graph.new('test')
|
74
|
+
# graph.add_edges('parked', 'idling', :label => 'ignite')
|
75
|
+
def add_edges(*args)
|
76
|
+
edge = v0_api? ? add_edge(*args) : super
|
77
|
+
edge.fontname = @font
|
78
|
+
edge
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
# Determines whether the old v0 api is in use
|
83
|
+
def v0_api?
|
84
|
+
version[0] == '0' || version[0] == '1' && version[1] == '0' && version[2] <= '2'
|
85
|
+
end
|
86
|
+
|
87
|
+
# The ruby-graphviz version data
|
88
|
+
def version
|
89
|
+
Constants::RGV_VERSION.split('.')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Represents a type of module that defines instance / class methods for a
|
3
|
+
# state machine
|
4
|
+
class HelperModule < Module #:nodoc:
|
5
|
+
def initialize(machine, kind)
|
6
|
+
@machine = machine
|
7
|
+
@kind = kind
|
8
|
+
end
|
9
|
+
|
10
|
+
# Provides a human-readable description of the module
|
11
|
+
def to_s
|
12
|
+
owner_class = @machine.owner_class
|
13
|
+
owner_class_name = owner_class.name && !owner_class.name.empty? ? owner_class.name : owner_class.to_s
|
14
|
+
"#{owner_class_name} #{@machine.name.inspect} #{@kind} helpers"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Merb::Plugins.add_rakefiles("#{File.dirname(__FILE__)}/../../tasks/state_machine") if defined?(Merb::Plugins)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
if defined?(Rails)
|
2
|
+
# Track all of the applicable locales to load
|
3
|
+
locale_paths = []
|
4
|
+
StateMachine::Integrations.all.each do |integration|
|
5
|
+
locale_paths << integration.locale_path if integration.available? && integration.locale_path
|
6
|
+
end
|
7
|
+
|
8
|
+
if defined?(Rails::Engine)
|
9
|
+
# Rails 3.x
|
10
|
+
class StateMachine::RailsEngine < Rails::Engine
|
11
|
+
rake_tasks do
|
12
|
+
load 'tasks/state_machine.rb'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0
|
17
|
+
StateMachine::RailsEngine.paths.config.locales = locale_paths
|
18
|
+
else
|
19
|
+
StateMachine::RailsEngine.paths['config/locales'] = locale_paths
|
20
|
+
end
|
21
|
+
elsif defined?(I18n)
|
22
|
+
# Rails 2.x
|
23
|
+
I18n.load_path.unshift(*locale_paths)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Load each available integration
|
2
|
+
require 'state_machine/integrations/base'
|
3
|
+
Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
|
4
|
+
require "state_machine/integrations/#{File.basename(path)}"
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'state_machine/error'
|
8
|
+
|
9
|
+
module StateMachine
|
10
|
+
# An invalid integration was specified
|
11
|
+
class IntegrationNotFound < Error
|
12
|
+
def initialize(name)
|
13
|
+
super(nil, "#{name.inspect} is an invalid integration")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Integrations allow state machines to take advantage of features within the
|
18
|
+
# context of a particular library. This is currently most useful with
|
19
|
+
# database libraries. For example, the various database integrations allow
|
20
|
+
# state machines to hook into features like:
|
21
|
+
# * Saving
|
22
|
+
# * Transactions
|
23
|
+
# * Observers
|
24
|
+
# * Scopes
|
25
|
+
# * Callbacks
|
26
|
+
# * Validation errors
|
27
|
+
#
|
28
|
+
# This type of integration allows the user to work with state machines in a
|
29
|
+
# fashion similar to other object models in their application.
|
30
|
+
#
|
31
|
+
# The integration interface is loosely defined by various unimplemented
|
32
|
+
# methods in the StateMachine::Machine class. See that class or the various
|
33
|
+
# built-in integrations for more information about how to define additional
|
34
|
+
# integrations.
|
35
|
+
module Integrations
|
36
|
+
# Attempts to find an integration that matches the given class. This will
|
37
|
+
# look through all of the built-in integrations under the StateMachine::Integrations
|
38
|
+
# namespace and find one that successfully matches the class.
|
39
|
+
#
|
40
|
+
# == Examples
|
41
|
+
#
|
42
|
+
# class Vehicle
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class ActiveModelVehicle
|
46
|
+
# include ActiveModel::Observing
|
47
|
+
# include ActiveModel::Validations
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# class ActiveRecordVehicle < ActiveRecord::Base
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# class DataMapperVehicle
|
54
|
+
# include DataMapper::Resource
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# class MongoidVehicle
|
58
|
+
# include Mongoid::Document
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# class MongoMapperVehicle
|
62
|
+
# include MongoMapper::Document
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# class SequelVehicle < Sequel::Model
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# StateMachine::Integrations.match(Vehicle) # => nil
|
69
|
+
# StateMachine::Integrations.match(ActiveModelVehicle) # => StateMachine::Integrations::ActiveModel
|
70
|
+
# StateMachine::Integrations.match(ActiveRecordVehicle) # => StateMachine::Integrations::ActiveRecord
|
71
|
+
# StateMachine::Integrations.match(DataMapperVehicle) # => StateMachine::Integrations::DataMapper
|
72
|
+
# StateMachine::Integrations.match(MongoidVehicle) # => StateMachine::Integrations::Mongoid
|
73
|
+
# StateMachine::Integrations.match(MongoMapperVehicle) # => StateMachine::Integrations::MongoMapper
|
74
|
+
# StateMachine::Integrations.match(SequelVehicle) # => StateMachine::Integrations::Sequel
|
75
|
+
def self.match(klass)
|
76
|
+
all.detect {|integration| integration.matches?(klass)}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Attempts to find an integration that matches the given list of ancestors.
|
80
|
+
# This will look through all of the built-in integrations under the StateMachine::Integrations
|
81
|
+
# namespace and find one that successfully matches one of the ancestors.
|
82
|
+
#
|
83
|
+
# == Examples
|
84
|
+
#
|
85
|
+
# StateMachine::Integrations.match([]) # => nil
|
86
|
+
# StateMachine::Integrations.match(['ActiveRecord::Base') # => StateMachine::Integrations::ActiveModel
|
87
|
+
def self.match_ancestors(ancestors)
|
88
|
+
all.detect {|integration| integration.matches_ancestors?(ancestors)}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Finds an integration with the given name. If the integration cannot be
|
92
|
+
# found, then a NameError exception will be raised.
|
93
|
+
#
|
94
|
+
# == Examples
|
95
|
+
#
|
96
|
+
# StateMachine::Integrations.find_by_name(:active_record) # => StateMachine::Integrations::ActiveRecord
|
97
|
+
# StateMachine::Integrations.find_by_name(:active_model) # => StateMachine::Integrations::ActiveModel
|
98
|
+
# StateMachine::Integrations.find_by_name(:data_mapper) # => StateMachine::Integrations::DataMapper
|
99
|
+
# StateMachine::Integrations.find_by_name(:mongoid) # => StateMachine::Integrations::Mongoid
|
100
|
+
# StateMachine::Integrations.find_by_name(:mongo_mapper) # => StateMachine::Integrations::MongoMapper
|
101
|
+
# StateMachine::Integrations.find_by_name(:sequel) # => StateMachine::Integrations::Sequel
|
102
|
+
# StateMachine::Integrations.find_by_name(:invalid) # => StateMachine::IntegrationNotFound: :invalid is an invalid integration
|
103
|
+
def self.find_by_name(name)
|
104
|
+
all.detect {|integration| integration.integration_name == name} || raise(IntegrationNotFound.new(name))
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets a list of all of the available integrations for use. This will
|
108
|
+
# always list the ActiveModel integration last.
|
109
|
+
#
|
110
|
+
# == Example
|
111
|
+
#
|
112
|
+
# StateMachine::Integrations.all
|
113
|
+
# # => [StateMachine::Integrations::ActiveRecord, StateMachine::Integrations::DataMapper
|
114
|
+
# # StateMachine::Integrations::Mongoid, StateMachine::Integrations::MongoMapper,
|
115
|
+
# # StateMachine::Integrations::Sequel, StateMachine::Integrations::ActiveModel]
|
116
|
+
def self.all
|
117
|
+
constants = self.constants.map {|c| c.to_s}.select {|c| c != 'ActiveModel'}.sort << 'ActiveModel'
|
118
|
+
constants.map {|c| const_get(c)}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,585 @@
|
|
1
|
+
module StateMachine
|
2
|
+
module Integrations #:nodoc:
|
3
|
+
# Adds support for integrating state machines with ActiveModel classes.
|
4
|
+
#
|
5
|
+
# == Examples
|
6
|
+
#
|
7
|
+
# If using ActiveModel directly within your class, then any one of the
|
8
|
+
# following features need to be included in order for the integration to be
|
9
|
+
# detected:
|
10
|
+
# * ActiveModel::Observing
|
11
|
+
# * ActiveModel::Validations
|
12
|
+
#
|
13
|
+
# Below is an example of a simple state machine defined within an
|
14
|
+
# ActiveModel class:
|
15
|
+
#
|
16
|
+
# class Vehicle
|
17
|
+
# include ActiveModel::Observing
|
18
|
+
# include ActiveModel::Validations
|
19
|
+
#
|
20
|
+
# attr_accessor :state
|
21
|
+
# define_attribute_methods [:state]
|
22
|
+
#
|
23
|
+
# state_machine :initial => :parked do
|
24
|
+
# event :ignite do
|
25
|
+
# transition :parked => :idling
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The examples in the sections below will use the above class as a
|
31
|
+
# reference.
|
32
|
+
#
|
33
|
+
# == Actions
|
34
|
+
#
|
35
|
+
# By default, no action will be invoked when a state is transitioned. This
|
36
|
+
# means that if you want to save changes when transitioning, you must
|
37
|
+
# define the action yourself like so:
|
38
|
+
#
|
39
|
+
# class Vehicle
|
40
|
+
# include ActiveModel::Validations
|
41
|
+
# attr_accessor :state
|
42
|
+
#
|
43
|
+
# state_machine :action => :save do
|
44
|
+
# ...
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# def save
|
48
|
+
# # Save changes
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# == Validations
|
53
|
+
#
|
54
|
+
# As mentioned in StateMachine::Machine#state, you can define behaviors,
|
55
|
+
# like validations, that only execute for certain states. One *important*
|
56
|
+
# caveat here is that, due to a constraint in ActiveModel's validation
|
57
|
+
# framework, custom validators will not work as expected when defined to run
|
58
|
+
# in multiple states. For example:
|
59
|
+
#
|
60
|
+
# class Vehicle
|
61
|
+
# include ActiveModel::Validations
|
62
|
+
#
|
63
|
+
# state_machine do
|
64
|
+
# ...
|
65
|
+
# state :first_gear, :second_gear do
|
66
|
+
# validate :speed_is_legal
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# In this case, the <tt>:speed_is_legal</tt> validation will only get run
|
72
|
+
# for the <tt>:second_gear</tt> state. To avoid this, you can define your
|
73
|
+
# custom validation like so:
|
74
|
+
#
|
75
|
+
# class Vehicle
|
76
|
+
# include ActiveModel::Validations
|
77
|
+
#
|
78
|
+
# state_machine do
|
79
|
+
# ...
|
80
|
+
# state :first_gear, :second_gear do
|
81
|
+
# validate {|vehicle| vehicle.speed_is_legal}
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# == Validation errors
|
87
|
+
#
|
88
|
+
# In order to hook in validation support for your model, the
|
89
|
+
# ActiveModel::Validations feature must be included. If this is included
|
90
|
+
# and an event fails to successfully fire because there are no matching
|
91
|
+
# transitions for the object, a validation error is added to the object's
|
92
|
+
# state attribute to help in determining why it failed.
|
93
|
+
#
|
94
|
+
# For example,
|
95
|
+
#
|
96
|
+
# vehicle = Vehicle.new
|
97
|
+
# vehicle.ignite # => false
|
98
|
+
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
|
99
|
+
#
|
100
|
+
# In addition, if you're using the <tt>ignite!</tt> version of the event,
|
101
|
+
# then the failure reason (such as the current validation errors) will be
|
102
|
+
# included in the exception that gets raised when the event fails. For
|
103
|
+
# example, assuming there's a validation on a field called +name+ on the class:
|
104
|
+
#
|
105
|
+
# vehicle = Vehicle.new
|
106
|
+
# vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
|
107
|
+
#
|
108
|
+
# === Security implications
|
109
|
+
#
|
110
|
+
# Beware that public event attributes mean that events can be fired
|
111
|
+
# whenever mass-assignment is being used. If you want to prevent malicious
|
112
|
+
# users from tampering with events through URLs / forms, the attribute
|
113
|
+
# should be protected like so:
|
114
|
+
#
|
115
|
+
# class Vehicle
|
116
|
+
# include ActiveModel::MassAssignmentSecurity
|
117
|
+
# attr_accessor :state
|
118
|
+
#
|
119
|
+
# attr_protected :state_event
|
120
|
+
# # attr_accessible ... # Alternative technique
|
121
|
+
#
|
122
|
+
# state_machine do
|
123
|
+
# ...
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# If you want to only have *some* events be able to fire via mass-assignment,
|
128
|
+
# you can build two state machines (one public and one protected) like so:
|
129
|
+
#
|
130
|
+
# class Vehicle
|
131
|
+
# include ActiveModel::MassAssignmentSecurity
|
132
|
+
# attr_accessor :state
|
133
|
+
#
|
134
|
+
# attr_protected :state_event # Prevent access to events in the first machine
|
135
|
+
#
|
136
|
+
# state_machine do
|
137
|
+
# # Define private events here
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# # Public machine targets the same state as the private machine
|
141
|
+
# state_machine :public_state, :attribute => :state do
|
142
|
+
# # Define public events here
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# == Callbacks
|
147
|
+
#
|
148
|
+
# All before/after transition callbacks defined for ActiveModel models
|
149
|
+
# behave in the same way that other ActiveSupport callbacks behave. The
|
150
|
+
# object involved in the transition is passed in as an argument.
|
151
|
+
#
|
152
|
+
# For example,
|
153
|
+
#
|
154
|
+
# class Vehicle
|
155
|
+
# include ActiveModel::Validations
|
156
|
+
# attr_accessor :state
|
157
|
+
#
|
158
|
+
# state_machine :initial => :parked do
|
159
|
+
# before_transition any => :idling do |vehicle|
|
160
|
+
# vehicle.put_on_seatbelt
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# before_transition do |vehicle, transition|
|
164
|
+
# # log message
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# event :ignite do
|
168
|
+
# transition :parked => :idling
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# def put_on_seatbelt
|
173
|
+
# ...
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# Note, also, that the transition can be accessed by simply defining
|
178
|
+
# additional arguments in the callback block.
|
179
|
+
#
|
180
|
+
# == Observers
|
181
|
+
#
|
182
|
+
# In order to hook in observer support for your application, the
|
183
|
+
# ActiveModel::Observing feature must be included. Because of the way
|
184
|
+
# ActiveModel observers are designed, there is less flexibility around the
|
185
|
+
# specific transitions that can be hooked in. However, a large number of
|
186
|
+
# hooks *are* supported. For example, if a transition for a object's
|
187
|
+
# +state+ attribute changes the state from +parked+ to +idling+ via the
|
188
|
+
# +ignite+ event, the following observer methods are supported:
|
189
|
+
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
190
|
+
# * before/after/after_failure_to-_ignite_from_parked
|
191
|
+
# * before/after/after_failure_to-_ignite_to_idling
|
192
|
+
# * before/after/after_failure_to-_ignite
|
193
|
+
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
194
|
+
# * before/after/after_failure_to-_transition_state_from_parked
|
195
|
+
# * before/after/after_failure_to-_transition_state_to_idling
|
196
|
+
# * before/after/after_failure_to-_transition_state
|
197
|
+
# * before/after/after_failure_to-_transition
|
198
|
+
#
|
199
|
+
# The following class shows an example of some of these hooks:
|
200
|
+
#
|
201
|
+
# class VehicleObserver < ActiveModel::Observer
|
202
|
+
# # Callback for :ignite event *before* the transition is performed
|
203
|
+
# def before_ignite(vehicle, transition)
|
204
|
+
# # log message
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# # Callback for :ignite event *after* the transition has been performed
|
208
|
+
# def after_ignite(vehicle, transition)
|
209
|
+
# # put on seatbelt
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# # Generic transition callback *before* the transition is performed
|
213
|
+
# def after_transition(vehicle, transition)
|
214
|
+
# Audit.log(vehicle, transition)
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# def after_failure_to_transition(vehicle, transition)
|
218
|
+
# Audit.error(vehicle, transition)
|
219
|
+
# end
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# More flexible transition callbacks can be defined directly within the
|
223
|
+
# model as described in StateMachine::Machine#before_transition
|
224
|
+
# and StateMachine::Machine#after_transition.
|
225
|
+
#
|
226
|
+
# To define a single observer for multiple state machines:
|
227
|
+
#
|
228
|
+
# class StateMachineObserver < ActiveModel::Observer
|
229
|
+
# observe Vehicle, Switch, Project
|
230
|
+
#
|
231
|
+
# def after_transition(object, transition)
|
232
|
+
# Audit.log(object, transition)
|
233
|
+
# end
|
234
|
+
# end
|
235
|
+
#
|
236
|
+
# == Internationalization
|
237
|
+
#
|
238
|
+
# Any error message that is generated from performing invalid transitions
|
239
|
+
# can be localized. The following default translations are used:
|
240
|
+
#
|
241
|
+
# en:
|
242
|
+
# activemodel:
|
243
|
+
# errors:
|
244
|
+
# messages:
|
245
|
+
# invalid: "is invalid"
|
246
|
+
# # %{value} = attribute value, %{state} = Human state name
|
247
|
+
# invalid_event: "cannot transition when %{state}"
|
248
|
+
# # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
|
249
|
+
# invalid_transition: "cannot transition via %{event}"
|
250
|
+
#
|
251
|
+
# You can override these for a specific model like so:
|
252
|
+
#
|
253
|
+
# en:
|
254
|
+
# activemodel:
|
255
|
+
# errors:
|
256
|
+
# models:
|
257
|
+
# user:
|
258
|
+
# invalid: "is not valid"
|
259
|
+
#
|
260
|
+
# In addition to the above, you can also provide translations for the
|
261
|
+
# various states / events in each state machine. Using the Vehicle example,
|
262
|
+
# state translations will be looked for using the following keys, where
|
263
|
+
# +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
|
264
|
+
# * <tt>activemodel.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
|
265
|
+
# * <tt>activemodel.state_machines.#{model_name}.states.#{state_name}</tt>
|
266
|
+
# * <tt>activemodel.state_machines.#{machine_name}.states.#{state_name}</tt>
|
267
|
+
# * <tt>activemodel.state_machines.states.#{state_name}</tt>
|
268
|
+
#
|
269
|
+
# Event translations will be looked for using the following keys, where
|
270
|
+
# +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
|
271
|
+
# * <tt>activemodel.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
|
272
|
+
# * <tt>activemodel.state_machines.#{model_name}.events.#{event_name}</tt>
|
273
|
+
# * <tt>activemodel.state_machines.#{machine_name}.events.#{event_name}</tt>
|
274
|
+
# * <tt>activemodel.state_machines.events.#{event_name}</tt>
|
275
|
+
#
|
276
|
+
# An example translation configuration might look like so:
|
277
|
+
#
|
278
|
+
# es:
|
279
|
+
# activemodel:
|
280
|
+
# state_machines:
|
281
|
+
# states:
|
282
|
+
# parked: 'estacionado'
|
283
|
+
# events:
|
284
|
+
# park: 'estacionarse'
|
285
|
+
#
|
286
|
+
# == Dirty Attribute Tracking
|
287
|
+
#
|
288
|
+
# When using the ActiveModel::Dirty extension, your model will keep track of
|
289
|
+
# any changes that are made to attributes. Depending on your ORM, an object
|
290
|
+
# will only be saved when there are attributes that have changed on the
|
291
|
+
# object. When integrating with state_machine, typically the +state+ field
|
292
|
+
# will be marked as dirty after a transition occurs. In some situations,
|
293
|
+
# however, this isn't the case.
|
294
|
+
#
|
295
|
+
# If you define loopback transitions in your state machine, the value for
|
296
|
+
# the machine's attribute (e.g. state) will not change. Unless you explicitly
|
297
|
+
# indicate so, this means that your object won't persist anything on a
|
298
|
+
# loopback. For example:
|
299
|
+
#
|
300
|
+
# class Vehicle
|
301
|
+
# include ActiveModel::Validations
|
302
|
+
# include ActiveModel::Dirty
|
303
|
+
# attr_accessor :state
|
304
|
+
#
|
305
|
+
# state_machine :initial => :parked do
|
306
|
+
# event :park do
|
307
|
+
# transition :parked => :parked, ...
|
308
|
+
# end
|
309
|
+
# end
|
310
|
+
# end
|
311
|
+
#
|
312
|
+
# If, instead, you'd like your object to always persist regardless of
|
313
|
+
# whether the value actually changed, you can do so by using the
|
314
|
+
# <tt>#{attribute}_will_change!</tt> helpers or defining a +before_transition+
|
315
|
+
# callback that actually changes an attribute on the model. For example:
|
316
|
+
#
|
317
|
+
# class Vehicle
|
318
|
+
# ...
|
319
|
+
# state_machine :initial => :parked do
|
320
|
+
# before_transition all => same do |vehicle|
|
321
|
+
# vehicle.state_will_change!
|
322
|
+
#
|
323
|
+
# # Alternative solution, updating timestamp
|
324
|
+
# # vehicle.updated_at = Time.curent
|
325
|
+
# end
|
326
|
+
# end
|
327
|
+
# end
|
328
|
+
#
|
329
|
+
# == Creating new integrations
|
330
|
+
#
|
331
|
+
# If you want to integrate state_machine with an ORM that implements parts
|
332
|
+
# or all of the ActiveModel API, only the machine defaults need to be
|
333
|
+
# specified. Otherwise, the implementation is similar to any other
|
334
|
+
# integration.
|
335
|
+
#
|
336
|
+
# For example,
|
337
|
+
#
|
338
|
+
# module StateMachine::Integrations::MyORM
|
339
|
+
# include StateMachine::Integrations::ActiveModel
|
340
|
+
#
|
341
|
+
# @defaults = {:action = > :persist}
|
342
|
+
#
|
343
|
+
# def self.matches?(klass)
|
344
|
+
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
|
345
|
+
# end
|
346
|
+
#
|
347
|
+
# protected
|
348
|
+
# def runs_validations_on_action?
|
349
|
+
# action == :persist
|
350
|
+
# end
|
351
|
+
# end
|
352
|
+
#
|
353
|
+
# If you wish to implement other features, such as attribute initialization
|
354
|
+
# with protected attributes, named scopes, or database transactions, you
|
355
|
+
# must add these independent of the ActiveModel integration. See the
|
356
|
+
# ActiveRecord implementation for examples of these customizations.
|
357
|
+
module ActiveModel
|
358
|
+
def self.included(base) #:nodoc:
|
359
|
+
base.versions.unshift(*versions)
|
360
|
+
end
|
361
|
+
|
362
|
+
include Base
|
363
|
+
extend ClassMethods
|
364
|
+
|
365
|
+
require 'state_machine/integrations/active_model/versions'
|
366
|
+
|
367
|
+
@defaults = {}
|
368
|
+
|
369
|
+
# Classes that include ActiveModel::Observing or ActiveModel::Validations
|
370
|
+
# will automatically use the ActiveModel integration.
|
371
|
+
def self.matching_ancestors
|
372
|
+
%w(ActiveModel ActiveModel::Observing ActiveModel::Validations)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Adds a validation error to the given object
|
376
|
+
def invalidate(object, attribute, message, values = [])
|
377
|
+
if supports_validations?
|
378
|
+
attribute = self.attribute(attribute)
|
379
|
+
options = values.inject({}) do |h, (key, value)|
|
380
|
+
h[key] = value
|
381
|
+
h
|
382
|
+
end
|
383
|
+
|
384
|
+
default_options = default_error_message_options(object, attribute, message)
|
385
|
+
object.errors.add(attribute, message, options.merge(default_options))
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Describes the current validation errors on the given object. If none
|
390
|
+
# are specific, then the default error is interpeted as a "halt".
|
391
|
+
def errors_for(object)
|
392
|
+
object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
|
393
|
+
end
|
394
|
+
|
395
|
+
# Resets any errors previously added when invalidating the given object
|
396
|
+
def reset(object)
|
397
|
+
object.errors.clear if supports_validations?
|
398
|
+
end
|
399
|
+
|
400
|
+
# Runs state events around the object's validation process
|
401
|
+
def around_validation(object)
|
402
|
+
object.class.state_machines.transitions(object, action, :after => false).perform { yield }
|
403
|
+
end
|
404
|
+
|
405
|
+
protected
|
406
|
+
# Whether observers are supported in the integration. Only true if
|
407
|
+
# ActiveModel::Observer is available.
|
408
|
+
def supports_observers?
|
409
|
+
defined?(::ActiveModel::Observing) && owner_class <= ::ActiveModel::Observing
|
410
|
+
end
|
411
|
+
|
412
|
+
# Whether validations are supported in the integration. Only true if
|
413
|
+
# the ActiveModel feature is enabled on the owner class.
|
414
|
+
def supports_validations?
|
415
|
+
defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
|
416
|
+
end
|
417
|
+
|
418
|
+
# Do validations run when the action configured this machine is
|
419
|
+
# invoked? This is used to determine whether to fire off attribute-based
|
420
|
+
# event transitions when the action is run.
|
421
|
+
def runs_validations_on_action?
|
422
|
+
false
|
423
|
+
end
|
424
|
+
|
425
|
+
# Gets the terminator to use for callbacks
|
426
|
+
def callback_terminator
|
427
|
+
@terminator ||= lambda {|result| result == false}
|
428
|
+
end
|
429
|
+
|
430
|
+
# Determines the base scope to use when looking up translations
|
431
|
+
def i18n_scope(klass)
|
432
|
+
klass.i18n_scope
|
433
|
+
end
|
434
|
+
|
435
|
+
# The default options to use when generating messages for validation
|
436
|
+
# errors
|
437
|
+
def default_error_message_options(object, attribute, message)
|
438
|
+
{:message => @messages[message]}
|
439
|
+
end
|
440
|
+
|
441
|
+
# Translates the given key / value combo. Translation keys are looked
|
442
|
+
# up in the following order:
|
443
|
+
# * <tt>#{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}</tt>
|
444
|
+
# * <tt>#{i18n_scope}.state_machines.#{model_name}.#{plural_key}.#{value}</tt>
|
445
|
+
# * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}</tt>
|
446
|
+
# * <tt>#{i18n_scope}.state_machines.#{plural_key}.#{value}</tt>
|
447
|
+
#
|
448
|
+
# If no keys are found, then the humanized value will be the fallback.
|
449
|
+
def translate(klass, key, value)
|
450
|
+
ancestors = ancestors_for(klass)
|
451
|
+
group = key.to_s.pluralize
|
452
|
+
value = value ? value.to_s : 'nil'
|
453
|
+
|
454
|
+
# Generate all possible translation keys
|
455
|
+
translations = ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}"}
|
456
|
+
translations.concat(ancestors.map {|ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}"})
|
457
|
+
translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
|
458
|
+
I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
|
459
|
+
end
|
460
|
+
|
461
|
+
# Build a list of ancestors for the given class to use when
|
462
|
+
# determining which localization key to use for a particular string.
|
463
|
+
def ancestors_for(klass)
|
464
|
+
klass.lookup_ancestors
|
465
|
+
end
|
466
|
+
|
467
|
+
# Initializes class-level extensions and defaults for this machine
|
468
|
+
def after_initialize
|
469
|
+
super
|
470
|
+
load_locale
|
471
|
+
load_observer_extensions
|
472
|
+
add_default_callbacks
|
473
|
+
end
|
474
|
+
|
475
|
+
# Loads any locale files needed for translating validation errors
|
476
|
+
def load_locale
|
477
|
+
I18n.load_path.unshift(@integration.locale_path) unless I18n.load_path.include?(@integration.locale_path)
|
478
|
+
end
|
479
|
+
|
480
|
+
# Loads extensions to ActiveModel's Observers
|
481
|
+
def load_observer_extensions
|
482
|
+
require 'state_machine/integrations/active_model/observer'
|
483
|
+
require 'state_machine/integrations/active_model/observer_update'
|
484
|
+
end
|
485
|
+
|
486
|
+
# Adds a set of default callbacks that utilize the Observer extensions
|
487
|
+
def add_default_callbacks
|
488
|
+
if supports_observers?
|
489
|
+
callbacks[:before] << Callback.new(:before) {|object, transition| notify(:before, object, transition)}
|
490
|
+
callbacks[:after] << Callback.new(:after) {|object, transition| notify(:after, object, transition)}
|
491
|
+
callbacks[:failure] << Callback.new(:failure) {|object, transition| notify(:after_failure_to, object, transition)}
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# Skips defining reader/writer methods since this is done automatically
|
496
|
+
def define_state_accessor
|
497
|
+
name = self.name
|
498
|
+
|
499
|
+
owner_class.validates_each(attribute) do |object, attr, value|
|
500
|
+
machine = object.class.state_machine(name)
|
501
|
+
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
|
502
|
+
end if supports_validations?
|
503
|
+
end
|
504
|
+
|
505
|
+
# Adds hooks into validation for automatically firing events
|
506
|
+
def define_action_helpers
|
507
|
+
super
|
508
|
+
define_validation_hook if runs_validations_on_action?
|
509
|
+
end
|
510
|
+
|
511
|
+
# Hooks into validations by defining around callbacks for the
|
512
|
+
# :validation event
|
513
|
+
def define_validation_hook
|
514
|
+
owner_class.set_callback(:validation, :around, self, :prepend => true)
|
515
|
+
end
|
516
|
+
|
517
|
+
# Creates a new callback in the callback chain, always inserting it
|
518
|
+
# before the default Observer callbacks that were created after
|
519
|
+
# initialization.
|
520
|
+
def add_callback(type, options, &block)
|
521
|
+
options[:terminator] = callback_terminator
|
522
|
+
|
523
|
+
if supports_observers?
|
524
|
+
@callbacks[type == :around ? :before : type].insert(-2, callback = Callback.new(type, options, &block))
|
525
|
+
add_states(callback.known_states)
|
526
|
+
callback
|
527
|
+
else
|
528
|
+
super
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Configures new states with the built-in humanize scheme
|
533
|
+
def add_states(new_states)
|
534
|
+
super.each do |new_state|
|
535
|
+
new_state.human_name = lambda {|state, klass| translate(klass, :state, state.name)}
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Configures new event with the built-in humanize scheme
|
540
|
+
def add_events(new_events)
|
541
|
+
super.each do |new_event|
|
542
|
+
new_event.human_name = lambda {|event, klass| translate(klass, :event, event.name)}
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
# Notifies observers on the given object that a callback occurred
|
547
|
+
# involving the given transition. This will attempt to call the
|
548
|
+
# following methods on observers:
|
549
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}_to_#{to}</tt>
|
550
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}</tt>
|
551
|
+
# * <tt>#{type}_#{qualified_event}_to_#{to}</tt>
|
552
|
+
# * <tt>#{type}_#{qualified_event}</tt>
|
553
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}_to_#{to}</tt>
|
554
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}</tt>
|
555
|
+
# * <tt>#{type}_transition_#{machine_name}_to_#{to}</tt>
|
556
|
+
# * <tt>#{type}_transition_#{machine_name}</tt>
|
557
|
+
# * <tt>#{type}_transition</tt>
|
558
|
+
#
|
559
|
+
# This will always return true regardless of the results of the
|
560
|
+
# callbacks.
|
561
|
+
def notify(type, object, transition)
|
562
|
+
name = self.name
|
563
|
+
event = transition.qualified_event
|
564
|
+
from = transition.from_name || 'nil'
|
565
|
+
to = transition.to_name || 'nil'
|
566
|
+
|
567
|
+
# Machine-specific updates
|
568
|
+
["#{type}_#{event}", "#{type}_transition_#{name}"].each do |event_segment|
|
569
|
+
["_from_#{from}", nil].each do |from_segment|
|
570
|
+
["_to_#{to}", nil].each do |to_segment|
|
571
|
+
object.class.changed if object.class.respond_to?(:changed)
|
572
|
+
object.class.notify_observers('update_with_transition', ObserverUpdate.new([event_segment, from_segment, to_segment].join, object, transition))
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
# Generic updates
|
578
|
+
object.class.changed if object.class.respond_to?(:changed)
|
579
|
+
object.class.notify_observers('update_with_transition', ObserverUpdate.new("#{type}_transition", object, transition))
|
580
|
+
|
581
|
+
true
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|