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,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
|