state_machine_deuxito 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +8 -0
- data/.yardopts +5 -0
- data/Appraisals +12 -0
- data/CHANGELOG.md +502 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +8 -0
- data/Rakefile +41 -0
- data/TODO.md +2 -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/4.2.11.1.gemfile +9 -0
- data/gemfiles/4.2.11.1.gemfile.lock +57 -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 +5 -0
- data/gemfiles/default.gemfile.lock +30 -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 +21 -0
- data/test/files/en.yml +17 -0
- data/test/files/switch.rb +15 -0
- data/test/functional/state_machine_test.rb +1067 -0
- data/test/test_helper.rb +8 -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 +451 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'state_machine/path'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Represents a collection of paths that are generated based on a set of
|
5
|
+
# requirements regarding what states to start and end on
|
6
|
+
class PathCollection < Array
|
7
|
+
include Assertions
|
8
|
+
|
9
|
+
# The object whose state machine is being walked
|
10
|
+
attr_reader :object
|
11
|
+
|
12
|
+
# The state machine these path are walking
|
13
|
+
attr_reader :machine
|
14
|
+
|
15
|
+
# The initial state to start each path from
|
16
|
+
attr_reader :from_name
|
17
|
+
|
18
|
+
# The target state for each path
|
19
|
+
attr_reader :to_name
|
20
|
+
|
21
|
+
# Creates a new collection of paths with the given requirements.
|
22
|
+
#
|
23
|
+
# Configuration options:
|
24
|
+
# * <tt>:from</tt> - The initial state to start from
|
25
|
+
# * <tt>:to</tt> - The target end state
|
26
|
+
# * <tt>:deep</tt> - Whether to enable deep searches for the target state.
|
27
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
28
|
+
# conditionals defined for each one
|
29
|
+
def initialize(object, machine, options = {})
|
30
|
+
options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
|
31
|
+
assert_valid_keys(options, :from, :to, :deep, :guard)
|
32
|
+
|
33
|
+
@object = object
|
34
|
+
@machine = machine
|
35
|
+
@from_name = machine.states.fetch(options[:from]).name
|
36
|
+
@to_name = options[:to] && machine.states.fetch(options[:to]).name
|
37
|
+
@guard = options[:guard]
|
38
|
+
@deep = options[:deep]
|
39
|
+
|
40
|
+
initial_paths.each {|path| walk(path)}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Lists all of the states that can be transitioned from through the paths in
|
44
|
+
# this collection.
|
45
|
+
#
|
46
|
+
# For example,
|
47
|
+
#
|
48
|
+
# paths.from_states # => [:parked, :idling, :first_gear, ...]
|
49
|
+
def from_states
|
50
|
+
map {|path| path.from_states}.flatten.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
# Lists all of the states that can be transitioned to through the paths in
|
54
|
+
# this collection.
|
55
|
+
#
|
56
|
+
# For example,
|
57
|
+
#
|
58
|
+
# paths.to_states # => [:idling, :first_gear, :second_gear, ...]
|
59
|
+
def to_states
|
60
|
+
map {|path| path.to_states}.flatten.uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
# Lists all of the events that can be fired through the paths in this
|
64
|
+
# collection.
|
65
|
+
#
|
66
|
+
# For example,
|
67
|
+
#
|
68
|
+
# paths.events # => [:park, :ignite, :shift_up, ...]
|
69
|
+
def events
|
70
|
+
map {|path| path.events}.flatten.uniq
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
# Gets the initial set of paths to walk
|
75
|
+
def initial_paths
|
76
|
+
machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
|
77
|
+
path = Path.new(object, machine, :target => to_name, :guard => @guard)
|
78
|
+
path << transition
|
79
|
+
path
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Walks down the given path. Each new path that matches the configured
|
84
|
+
# requirements will be added to this collection.
|
85
|
+
def walk(path)
|
86
|
+
self << path if path.complete?
|
87
|
+
path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
require 'state_machine/assertions'
|
2
|
+
require 'state_machine/state_context'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# A state defines a value that an attribute can be in after being transitioned
|
6
|
+
# 0 or more times. States can represent a value of any type in Ruby, though
|
7
|
+
# the most common (and default) type is String.
|
8
|
+
#
|
9
|
+
# In addition to defining the machine's value, a state can also define a
|
10
|
+
# behavioral context for an object when that object is in the state. See
|
11
|
+
# StateMachine::Machine#state for more information about how state-driven
|
12
|
+
# behavior can be utilized.
|
13
|
+
class State
|
14
|
+
include Assertions
|
15
|
+
|
16
|
+
# The state machine for which this state is defined
|
17
|
+
attr_accessor :machine
|
18
|
+
|
19
|
+
# The unique identifier for the state used in event and callback definitions
|
20
|
+
attr_reader :name
|
21
|
+
|
22
|
+
# The fully-qualified identifier for the state, scoped by the machine's
|
23
|
+
# namespace
|
24
|
+
attr_reader :qualified_name
|
25
|
+
|
26
|
+
# The human-readable name for the state
|
27
|
+
attr_writer :human_name
|
28
|
+
|
29
|
+
# The value that is written to a machine's attribute when an object
|
30
|
+
# transitions into this state
|
31
|
+
attr_writer :value
|
32
|
+
|
33
|
+
# Whether this state's value should be cached after being evaluated
|
34
|
+
attr_accessor :cache
|
35
|
+
|
36
|
+
# Whether or not this state is the initial state to use for new objects
|
37
|
+
attr_accessor :initial
|
38
|
+
alias_method :initial?, :initial
|
39
|
+
|
40
|
+
# A custom lambda block for determining whether a given value matches this
|
41
|
+
# state
|
42
|
+
attr_accessor :matcher
|
43
|
+
|
44
|
+
# Creates a new state within the context of the given machine.
|
45
|
+
#
|
46
|
+
# Configuration options:
|
47
|
+
# * <tt>:initial</tt> - Whether this state is the beginning state for the
|
48
|
+
# machine. Default is false.
|
49
|
+
# * <tt>:value</tt> - The value to store when an object transitions to this
|
50
|
+
# state. Default is the name (stringified).
|
51
|
+
# * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
|
52
|
+
# then setting this to true will cache the evaluated result
|
53
|
+
# * <tt>:if</tt> - Determines whether a value matches this state
|
54
|
+
# (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
|
55
|
+
# By default, the configured value is matched.
|
56
|
+
# * <tt>:human_name</tt> - The human-readable version of this state's name
|
57
|
+
def initialize(machine, name, options = {}) #:nodoc:
|
58
|
+
assert_valid_keys(options, :initial, :value, :cache, :if, :human_name)
|
59
|
+
|
60
|
+
@machine = machine
|
61
|
+
@name = name
|
62
|
+
@qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
|
63
|
+
@human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
|
64
|
+
@value = options.include?(:value) ? options[:value] : name && name.to_s
|
65
|
+
@cache = options[:cache]
|
66
|
+
@matcher = options[:if]
|
67
|
+
@initial = options[:initial] == true
|
68
|
+
@context = StateContext.new(self)
|
69
|
+
|
70
|
+
if name
|
71
|
+
conflicting_machines = machine.owner_class.state_machines.select {|other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
|
72
|
+
|
73
|
+
# Output a warning if another machine has a conflicting qualified name
|
74
|
+
# for a different attribute
|
75
|
+
if conflict = conflicting_machines.detect {|other_name, other_machine| other_machine.attribute != machine.attribute}
|
76
|
+
name, other_machine = conflict
|
77
|
+
warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
|
78
|
+
elsif conflicting_machines.empty?
|
79
|
+
# Only bother adding predicates when another machine for the same
|
80
|
+
# attribute hasn't already done so
|
81
|
+
add_predicate
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Creates a copy of this state, excluding the context to prevent conflicts
|
87
|
+
# across different machines.
|
88
|
+
def initialize_copy(orig) #:nodoc:
|
89
|
+
super
|
90
|
+
@context = StateContext.new(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Determines whether there are any states that can be transitioned to from
|
94
|
+
# this state. If there are none, then this state is considered *final*.
|
95
|
+
# Any objects in a final state will remain so forever given the current
|
96
|
+
# machine's definition.
|
97
|
+
def final?
|
98
|
+
!machine.events.any? do |event|
|
99
|
+
event.branches.any? do |branch|
|
100
|
+
branch.state_requirements.any? do |requirement|
|
101
|
+
requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Transforms the state name into a more human-readable format, such as
|
108
|
+
# "first gear" instead of "first_gear"
|
109
|
+
def human_name(klass = @machine.owner_class)
|
110
|
+
@human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
|
111
|
+
end
|
112
|
+
|
113
|
+
# Generates a human-readable description of this state's name / value:
|
114
|
+
#
|
115
|
+
# For example,
|
116
|
+
#
|
117
|
+
# State.new(machine, :parked).description # => "parked"
|
118
|
+
# State.new(machine, :parked, :value => :parked).description # => "parked"
|
119
|
+
# State.new(machine, :parked, :value => nil).description # => "parked (nil)"
|
120
|
+
# State.new(machine, :parked, :value => 1).description # => "parked (1)"
|
121
|
+
# State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
|
122
|
+
#
|
123
|
+
# Configuration options:
|
124
|
+
# * <tt>:human_name</tt> - Whether to use this state's human name in the
|
125
|
+
# description or just the internal name
|
126
|
+
def description(options = {})
|
127
|
+
label = options[:human_name] ? human_name : name
|
128
|
+
description = label ? label.to_s : label.inspect
|
129
|
+
description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
|
130
|
+
description
|
131
|
+
end
|
132
|
+
|
133
|
+
# The value that represents this state. This will optionally evaluate the
|
134
|
+
# original block if it's a lambda block. Otherwise, the static value is
|
135
|
+
# returned.
|
136
|
+
#
|
137
|
+
# For example,
|
138
|
+
#
|
139
|
+
# State.new(machine, :parked, :value => 1).value # => 1
|
140
|
+
# State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
|
141
|
+
# State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
|
142
|
+
def value(eval = true)
|
143
|
+
if @value.is_a?(Proc) && eval
|
144
|
+
if cache_value?
|
145
|
+
@value = @value.call
|
146
|
+
machine.states.update(self)
|
147
|
+
@value
|
148
|
+
else
|
149
|
+
@value.call
|
150
|
+
end
|
151
|
+
else
|
152
|
+
@value
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Determines whether this state matches the given value. If no matcher is
|
157
|
+
# configured, then this will check whether the values are equivalent.
|
158
|
+
# Otherwise, the matcher will determine the result.
|
159
|
+
#
|
160
|
+
# For example,
|
161
|
+
#
|
162
|
+
# # Without a matcher
|
163
|
+
# state = State.new(machine, :parked, :value => 1)
|
164
|
+
# state.matches?(1) # => true
|
165
|
+
# state.matches?(2) # => false
|
166
|
+
#
|
167
|
+
# # With a matcher
|
168
|
+
# state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
|
169
|
+
# state.matches?(nil) # => false
|
170
|
+
# state.matches?(Time.now) # => true
|
171
|
+
def matches?(other_value)
|
172
|
+
matcher ? matcher.call(other_value) : other_value == value
|
173
|
+
end
|
174
|
+
|
175
|
+
# Defines a context for the state which will be enabled on instances of
|
176
|
+
# the owner class when the machine is in this state.
|
177
|
+
#
|
178
|
+
# This can be called multiple times. Each time a new context is created,
|
179
|
+
# a new module will be included in the owner class.
|
180
|
+
def context(&block)
|
181
|
+
# Include the context
|
182
|
+
context = @context
|
183
|
+
machine.owner_class.class_eval { include context }
|
184
|
+
|
185
|
+
# Evaluate the method definitions and track which ones were added
|
186
|
+
old_methods = context_methods
|
187
|
+
context.class_eval(&block)
|
188
|
+
new_methods = context_methods.to_a.select {|(name, method)| old_methods[name] != method}
|
189
|
+
|
190
|
+
# Alias new methods so that the only execute when the object is in this state
|
191
|
+
new_methods.each do |(method_name, method)|
|
192
|
+
context_name = context_name_for(method_name)
|
193
|
+
context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
|
194
|
+
alias_method :"#{context_name}", :#{method_name}
|
195
|
+
def #{method_name}(*args, &block)
|
196
|
+
state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
|
197
|
+
options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
|
198
|
+
state.call(self, :"#{context_name}", *(args + [options]), &block)
|
199
|
+
end
|
200
|
+
end_eval
|
201
|
+
end
|
202
|
+
|
203
|
+
true
|
204
|
+
end
|
205
|
+
|
206
|
+
# The list of methods that have been defined in this state's context
|
207
|
+
def context_methods
|
208
|
+
@context.instance_methods.inject({}) do |methods, name|
|
209
|
+
methods.merge(name.to_sym => @context.instance_method(name))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Calls a method defined in this state's context on the given object. All
|
214
|
+
# arguments and any block will be passed into the method defined.
|
215
|
+
#
|
216
|
+
# If the method has never been defined for this state, then a NoMethodError
|
217
|
+
# will be raised.
|
218
|
+
def call(object, method, *args, &block)
|
219
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
220
|
+
options = {:method_name => method}.merge(options)
|
221
|
+
state = machine.states.match!(object)
|
222
|
+
|
223
|
+
if state == self && object.respond_to?(method)
|
224
|
+
object.send(method, *args, &block)
|
225
|
+
elsif method_missing = options[:method_missing]
|
226
|
+
# Dispatch to the superclass since the object either isn't in this state
|
227
|
+
# or this state doesn't handle the method
|
228
|
+
begin
|
229
|
+
method_missing.call
|
230
|
+
rescue NoMethodError => ex
|
231
|
+
if ex.name.to_s == options[:method_name].to_s && ex.args == args
|
232
|
+
# No valid context for this method
|
233
|
+
raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
|
234
|
+
else
|
235
|
+
raise
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Draws a representation of this state on the given machine. This will
|
242
|
+
# create a new node on the graph with the following properties:
|
243
|
+
# * +label+ - The human-friendly description of the state.
|
244
|
+
# * +width+ - The width of the node. Always 1.
|
245
|
+
# * +height+ - The height of the node. Always 1.
|
246
|
+
# * +shape+ - The actual shape of the node. If the state is a final
|
247
|
+
# state, then "doublecircle", otherwise "ellipse".
|
248
|
+
#
|
249
|
+
# Configuration options:
|
250
|
+
# * <tt>:human_name</tt> - Whether to use the state's human name for the
|
251
|
+
# node's label that gets drawn on the graph
|
252
|
+
def draw(graph, options = {})
|
253
|
+
node = graph.add_nodes(name ? name.to_s : 'nil',
|
254
|
+
:label => description(options),
|
255
|
+
:width => '1',
|
256
|
+
:height => '1',
|
257
|
+
:shape => final? ? 'doublecircle' : 'ellipse'
|
258
|
+
)
|
259
|
+
|
260
|
+
# Add open arrow for initial state
|
261
|
+
graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
|
262
|
+
|
263
|
+
true
|
264
|
+
end
|
265
|
+
|
266
|
+
# Generates a nicely formatted description of this state's contents.
|
267
|
+
#
|
268
|
+
# For example,
|
269
|
+
#
|
270
|
+
# state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
|
271
|
+
# state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
|
272
|
+
def inspect
|
273
|
+
attributes = [[:name, name], [:value, @value], [:initial, initial?]]
|
274
|
+
"#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
# Should the value be cached after it's evaluated for the first time?
|
279
|
+
def cache_value?
|
280
|
+
@cache
|
281
|
+
end
|
282
|
+
|
283
|
+
# Adds a predicate method to the owner class so long as a name has
|
284
|
+
# actually been configured for the state
|
285
|
+
def add_predicate
|
286
|
+
# Checks whether the current value matches this state
|
287
|
+
machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
|
288
|
+
machine.states.matches?(object, name)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Generates the name of the method containing the actual implementation
|
293
|
+
def context_name_for(method)
|
294
|
+
:"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'state_machine/node_collection'
|
2
|
+
|
3
|
+
module StateMachine
|
4
|
+
# Represents a collection of states in a state machine
|
5
|
+
class StateCollection < NodeCollection
|
6
|
+
def initialize(machine) #:nodoc:
|
7
|
+
super(machine, :index => [:name, :qualified_name, :value])
|
8
|
+
end
|
9
|
+
|
10
|
+
# Determines whether the given object is in a specific state. If the
|
11
|
+
# object's current value doesn't match the state, then this will return
|
12
|
+
# false, otherwise true. If the given state is unknown, then an IndexError
|
13
|
+
# will be raised.
|
14
|
+
#
|
15
|
+
# == Examples
|
16
|
+
#
|
17
|
+
# class Vehicle
|
18
|
+
# state_machine :initial => :parked do
|
19
|
+
# other_states :idling
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# states = Vehicle.state_machine.states
|
24
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
25
|
+
#
|
26
|
+
# states.matches?(vehicle, :parked) # => true
|
27
|
+
# states.matches?(vehicle, :idling) # => false
|
28
|
+
# states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
|
29
|
+
def matches?(object, name)
|
30
|
+
fetch(name).matches?(machine.read(object, :state))
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determines the current state of the given object as configured by this
|
34
|
+
# state machine. This will attempt to find a known state that matches
|
35
|
+
# the value of the attribute on the object.
|
36
|
+
#
|
37
|
+
# == Examples
|
38
|
+
#
|
39
|
+
# class Vehicle
|
40
|
+
# state_machine :initial => :parked do
|
41
|
+
# other_states :idling
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# states = Vehicle.state_machine.states
|
46
|
+
#
|
47
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
48
|
+
# states.match(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
|
49
|
+
#
|
50
|
+
# vehicle.state = 'idling'
|
51
|
+
# states.match(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
|
52
|
+
#
|
53
|
+
# vehicle.state = 'invalid'
|
54
|
+
# states.match(vehicle) # => nil
|
55
|
+
def match(object)
|
56
|
+
value = machine.read(object, :state)
|
57
|
+
self[value, :value] || detect {|state| state.matches?(value)}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Determines the current state of the given object as configured by this
|
61
|
+
# state machine. If no state is found, then an ArgumentError will be
|
62
|
+
# raised.
|
63
|
+
#
|
64
|
+
# == Examples
|
65
|
+
#
|
66
|
+
# class Vehicle
|
67
|
+
# state_machine :initial => :parked do
|
68
|
+
# other_states :idling
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# states = Vehicle.state_machine.states
|
73
|
+
#
|
74
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
75
|
+
# states.match!(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
|
76
|
+
#
|
77
|
+
# vehicle.state = 'invalid'
|
78
|
+
# states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
|
79
|
+
def match!(object)
|
80
|
+
match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
|
81
|
+
end
|
82
|
+
|
83
|
+
# Gets the order in which states should be displayed based on where they
|
84
|
+
# were first referenced. This will order states in the following priority:
|
85
|
+
#
|
86
|
+
# 1. Initial state
|
87
|
+
# 2. Event transitions (:from, :except_from, :to, :except_to options)
|
88
|
+
# 3. States with behaviors
|
89
|
+
# 4. States referenced via +state+ or +other_states+
|
90
|
+
# 5. States referenced in callbacks
|
91
|
+
#
|
92
|
+
# This order will determine how the GraphViz visualizations are rendered.
|
93
|
+
def by_priority
|
94
|
+
order = select {|state| state.initial}.map {|state| state.name}
|
95
|
+
|
96
|
+
machine.events.each {|event| order += event.known_states}
|
97
|
+
order += select {|state| state.context_methods.any?}.map {|state| state.name}
|
98
|
+
order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
|
99
|
+
order += keys(:name)
|
100
|
+
|
101
|
+
order.uniq!
|
102
|
+
order.map! {|name| self[name]}
|
103
|
+
order
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
# Gets the value for the given attribute on the node
|
108
|
+
def value(node, attribute)
|
109
|
+
attribute == :value ? node.value(false) : super
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|