verborghs-state_machine 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/CHANGELOG.rdoc +360 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +635 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine/assertions.rb +36 -0
  28. data/lib/state_machine/callback.rb +241 -0
  29. data/lib/state_machine/condition_proxy.rb +106 -0
  30. data/lib/state_machine/eval_helpers.rb +83 -0
  31. data/lib/state_machine/event.rb +267 -0
  32. data/lib/state_machine/event_collection.rb +122 -0
  33. data/lib/state_machine/extensions.rb +149 -0
  34. data/lib/state_machine/guard.rb +230 -0
  35. data/lib/state_machine/initializers/merb.rb +1 -0
  36. data/lib/state_machine/initializers/rails.rb +5 -0
  37. data/lib/state_machine/initializers.rb +4 -0
  38. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  40. data/lib/state_machine/integrations/active_model.rb +445 -0
  41. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  42. data/lib/state_machine/integrations/active_record.rb +522 -0
  43. data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
  44. data/lib/state_machine/integrations/data_mapper.rb +379 -0
  45. data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
  46. data/lib/state_machine/integrations/sequel.rb +356 -0
  47. data/lib/state_machine/integrations.rb +83 -0
  48. data/lib/state_machine/machine.rb +1645 -0
  49. data/lib/state_machine/machine_collection.rb +64 -0
  50. data/lib/state_machine/matcher.rb +123 -0
  51. data/lib/state_machine/matcher_helpers.rb +54 -0
  52. data/lib/state_machine/node_collection.rb +152 -0
  53. data/lib/state_machine/state.rb +260 -0
  54. data/lib/state_machine/state_collection.rb +112 -0
  55. data/lib/state_machine/transition.rb +399 -0
  56. data/lib/state_machine/transition_collection.rb +244 -0
  57. data/lib/state_machine.rb +421 -0
  58. data/lib/tasks/state_machine.rake +1 -0
  59. data/lib/tasks/state_machine.rb +27 -0
  60. data/test/files/en.yml +9 -0
  61. data/test/files/switch.rb +11 -0
  62. data/test/functional/state_machine_test.rb +980 -0
  63. data/test/test_helper.rb +4 -0
  64. data/test/unit/assertions_test.rb +40 -0
  65. data/test/unit/callback_test.rb +728 -0
  66. data/test/unit/condition_proxy_test.rb +328 -0
  67. data/test/unit/eval_helpers_test.rb +222 -0
  68. data/test/unit/event_collection_test.rb +324 -0
  69. data/test/unit/event_test.rb +795 -0
  70. data/test/unit/guard_test.rb +909 -0
  71. data/test/unit/integrations/active_model_test.rb +956 -0
  72. data/test/unit/integrations/active_record_test.rb +1918 -0
  73. data/test/unit/integrations/data_mapper_test.rb +1814 -0
  74. data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
  75. data/test/unit/integrations/sequel_test.rb +1492 -0
  76. data/test/unit/integrations_test.rb +50 -0
  77. data/test/unit/invalid_event_test.rb +7 -0
  78. data/test/unit/invalid_transition_test.rb +7 -0
  79. data/test/unit/machine_collection_test.rb +565 -0
  80. data/test/unit/machine_test.rb +2349 -0
  81. data/test/unit/matcher_helpers_test.rb +37 -0
  82. data/test/unit/matcher_test.rb +155 -0
  83. data/test/unit/node_collection_test.rb +207 -0
  84. data/test/unit/state_collection_test.rb +280 -0
  85. data/test/unit/state_machine_test.rb +31 -0
  86. data/test/unit/state_test.rb +848 -0
  87. data/test/unit/transition_collection_test.rb +2098 -0
  88. data/test/unit/transition_test.rb +1384 -0
  89. metadata +176 -0
@@ -0,0 +1,64 @@
1
+ module StateMachine
2
+ # Represents a collection of state machines for a class
3
+ class MachineCollection < Hash
4
+ # Initializes the state of each machine in the given object. Initial
5
+ # values are only set if the machine's attribute doesn't already exist
6
+ # (which must mean the defaults are being skipped)
7
+ def initialize_states(object, options = {})
8
+ if ignore = options[:ignore]
9
+ ignore = ignore.map {|attribute| attribute.to_sym}
10
+ end
11
+
12
+ each_value do |machine|
13
+ if (!ignore || !ignore.include?(machine.attribute)) && (!options.include?(:dynamic) || machine.dynamic_initial_state? == options[:dynamic])
14
+ value = machine.read(object, :state)
15
+ machine.initialize_state(object) if ignore || value.nil? || value.respond_to?(:empty?) && value.empty?
16
+ end
17
+ end
18
+ end
19
+
20
+ # Runs one or more events in parallel on the given object. See
21
+ # StateMachine::InstanceMethods#fire_events for more information.
22
+ def fire_events(object, *events)
23
+ run_action = [true, false].include?(events.last) ? events.pop : true
24
+
25
+ # Generate the transitions to run for each event
26
+ transitions = events.collect do |event_name|
27
+ # Find the actual event being run
28
+ event = nil
29
+ detect {|name, machine| event = machine.events[event_name, :qualified_name]}
30
+
31
+ raise InvalidEvent, "#{event_name.inspect} is an unknown state machine event" unless event
32
+
33
+ # Get the transition that will be performed for the event
34
+ unless transition = event.transition_for(object)
35
+ machine = event.machine
36
+ machine.invalidate(object, :state, :invalid_transition, [[:event, event.human_name]])
37
+ end
38
+
39
+ transition
40
+ end.compact
41
+
42
+ # Run the events in parallel only if valid transitions were found for
43
+ # all of them
44
+ if events.length == transitions.length
45
+ TransitionCollection.new(transitions, :actions => run_action).perform
46
+ else
47
+ false
48
+ end
49
+ end
50
+
51
+ # Builds the collection of transitions for all event attributes defined on
52
+ # the given object. This will only include events whose machine actions
53
+ # match the one specified.
54
+ #
55
+ # These should only be fired as a result of the action being run.
56
+ def transitions(object, action, options = {})
57
+ transitions = map do |name, machine|
58
+ machine.events.attribute_transition_for(object, true) if machine.action == action
59
+ end
60
+
61
+ AttributeTransitionCollection.new(transitions.compact, options)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,123 @@
1
+ require 'singleton'
2
+
3
+ module StateMachine
4
+ # Provides a general strategy pattern for determining whether a match is found
5
+ # for a value. The algorithm that actually determines the match depends on
6
+ # the matcher in use.
7
+ class Matcher
8
+ # The list of values against which queries are matched
9
+ attr_reader :values
10
+
11
+ # Creates a new matcher for querying against the given set of values
12
+ def initialize(values = [])
13
+ @values = values.is_a?(Array) ? values : [values]
14
+ end
15
+
16
+ # Generates a subset of values that exists in both the set of values being
17
+ # filtered and the values configured for the matcher
18
+ def filter(values)
19
+ self.values & values
20
+ end
21
+ end
22
+
23
+ # Matches any given value. Since there is no configuration for this type of
24
+ # matcher, it must be used as a singleton.
25
+ class AllMatcher < Matcher
26
+ include Singleton
27
+
28
+ # Generates a blacklist matcher based on the given set of values
29
+ #
30
+ # == Examples
31
+ #
32
+ # matcher = StateMachine::AllMatcher.instance - [:parked, :idling]
33
+ # matcher.matches?(:parked) # => false
34
+ # matcher.matches?(:first_gear) # => true
35
+ def -(blacklist)
36
+ BlacklistMatcher.new(blacklist)
37
+ end
38
+
39
+ # Always returns true
40
+ def matches?(value, context = {})
41
+ true
42
+ end
43
+
44
+ # Always returns the given set of values
45
+ def filter(values)
46
+ values
47
+ end
48
+
49
+ # A human-readable description of this matcher. Always "all".
50
+ def description
51
+ 'all'
52
+ end
53
+ end
54
+
55
+ # Matches a specific set of values
56
+ class WhitelistMatcher < Matcher
57
+ # Checks whether the given value exists within the whitelist configured
58
+ # for this matcher.
59
+ #
60
+ # == Examples
61
+ #
62
+ # matcher = StateMachine::WhitelistMatcher.new([:parked, :idling])
63
+ # matcher.matches?(:parked) # => true
64
+ # matcher.matches?(:first_gear) # => false
65
+ def matches?(value, context = {})
66
+ values.include?(value)
67
+ end
68
+
69
+ # A human-readable description of this matcher
70
+ def description
71
+ values.length == 1 ? values.first.inspect : values.inspect
72
+ end
73
+ end
74
+
75
+ # Matches everything but a specific set of values
76
+ class BlacklistMatcher < Matcher
77
+ # Checks whether the given value exists outside the blacklist configured
78
+ # for this matcher.
79
+ #
80
+ # == Examples
81
+ #
82
+ # matcher = StateMachine::BlacklistMatcher.new([:parked, :idling])
83
+ # matcher.matches?(:parked) # => false
84
+ # matcher.matches?(:first_gear) # => true
85
+ def matches?(value, context = {})
86
+ !values.include?(value)
87
+ end
88
+
89
+ # Finds all values that are *not* within the blacklist configured for this
90
+ # matcher
91
+ def filter(values)
92
+ values - self.values
93
+ end
94
+
95
+ # A human-readable description of this matcher
96
+ def description
97
+ "all - #{values.length == 1 ? values.first.inspect : values.inspect}"
98
+ end
99
+ end
100
+
101
+ # Matches a loopback of two values within a context. Since there is no
102
+ # configuration for this type of matcher, it must be used as a singleton.
103
+ class LoopbackMatcher < Matcher
104
+ include Singleton
105
+
106
+ # Checks whether the given value matches what the value originally was.
107
+ # This value should be defined in the context.
108
+ #
109
+ # == Examples
110
+ #
111
+ # matcher = StateMachine::LoopbackMatcher.instance
112
+ # matcher.matches?(:parked, :from => :parked) # => true
113
+ # matcher.matches?(:parked, :from => :idling) # => false
114
+ def matches?(value, context)
115
+ context[:from] == value
116
+ end
117
+
118
+ # A human-readable description of this matcher. Always "same".
119
+ def description
120
+ 'same'
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,54 @@
1
+ module StateMachine
2
+ # Provides a set of helper methods for generating matchers
3
+ module MatcherHelpers
4
+ # Represents a state that matches all known states in a machine.
5
+ #
6
+ # == Examples
7
+ #
8
+ # class Vehicle
9
+ # state_machine do
10
+ # before_transition any => :parked, :do => lambda {...}
11
+ # before_transition all - :parked => all - :idling, :do => lambda {}
12
+ #
13
+ # event :park
14
+ # transition all => :parked
15
+ # end
16
+ #
17
+ # event :crash
18
+ # transition all - :parked => :stalled
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # In the above example, +all+ will match the following states since they
24
+ # are known:
25
+ # * +parked+
26
+ # * +stalled+
27
+ # * +idling+
28
+ def all
29
+ AllMatcher.instance
30
+ end
31
+ alias_method :any, :all
32
+
33
+ # Represents a state that matches the original +from+ state. This is useful
34
+ # for defining transitions which are loopbacks.
35
+ #
36
+ # == Examples
37
+ #
38
+ # class Vehicle
39
+ # state_machine do
40
+ # event :ignite
41
+ # transition [:idling, :first_gear] => same
42
+ # end
43
+ # end
44
+ # end
45
+ #
46
+ # In the above example, +same+ will match whichever the from state is. In
47
+ # the case of the +ignite+ event, it is essential the same as the following:
48
+ #
49
+ # transition :parked => :parked, :first_gear => :first_gear
50
+ def same
51
+ LoopbackMatcher.instance
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,152 @@
1
+ require 'state_machine/assertions'
2
+
3
+ module StateMachine
4
+ # Represents a collection of nodes in a state machine, be it events or states.
5
+ class NodeCollection
6
+ include Enumerable
7
+ include Assertions
8
+
9
+ # The machine associated with the nodes
10
+ attr_reader :machine
11
+
12
+ # Creates a new collection of nodes for the given state machine. By default,
13
+ # the collection is empty.
14
+ #
15
+ # Configuration options:
16
+ # * <tt>:index</tt> - One or more attributes to automatically generate
17
+ # hashed indices for in order to perform quick lookups. Default is to
18
+ # index by the :name attribute
19
+ def initialize(machine, options = {})
20
+ assert_valid_keys(options, :index)
21
+ options = {:index => :name}.merge(options)
22
+
23
+ @machine = machine
24
+ @nodes = []
25
+ @indices = Array(options[:index]).inject({}) {|indices, attribute| indices[attribute] = {}; indices}
26
+ @default_index = Array(options[:index]).first
27
+ end
28
+
29
+ # Creates a copy of this collection such that modifications don't affect
30
+ # the original collection
31
+ def initialize_copy(orig) #:nodoc:
32
+ super
33
+
34
+ nodes = @nodes
35
+ @nodes = []
36
+ @indices = @indices.inject({}) {|indices, (name, index)| indices[name] = {}; indices}
37
+ nodes.each {|node| self << node.dup}
38
+ end
39
+
40
+ # Changes the current machine associated with the collection. In turn, this
41
+ # will change the state machine associated with each node in the collection.
42
+ def machine=(new_machine)
43
+ @machine = new_machine
44
+ each {|node| node.machine = new_machine}
45
+ end
46
+
47
+ # Gets the number of nodes in this collection
48
+ def length
49
+ @nodes.length
50
+ end
51
+
52
+ # Gets the set of unique keys for the given index
53
+ def keys(index_name = @default_index)
54
+ index(index_name).keys
55
+ end
56
+
57
+ # Adds a new node to the collection. By doing so, this will also add it to
58
+ # the configured indices.
59
+ def <<(node)
60
+ @nodes << node
61
+ @indices.each {|attribute, index| index[value(node, attribute)] = node}
62
+ self
63
+ end
64
+
65
+ # Updates the indexed keys for the given node. If the node's attribute
66
+ # has changed since it was added to the collection, the old indexed keys
67
+ # will be replaced with the updated ones.
68
+ def update(node)
69
+ @indices.each do |attribute, index|
70
+ old_key = RUBY_VERSION < '1.9' ? index.index(node) : index.key(node)
71
+ new_key = value(node, attribute)
72
+
73
+ # Only replace the key if it's changed
74
+ if old_key != new_key
75
+ index.delete(old_key)
76
+ index[new_key] = node
77
+ end
78
+ end
79
+ end
80
+
81
+ # Calls the block once for each element in self, passing that element as a
82
+ # parameter.
83
+ #
84
+ # states = StateMachine::NodeCollection.new
85
+ # states << StateMachine::State.new(machine, :parked)
86
+ # states << StateMachine::State.new(machine, :idling)
87
+ # states.each {|state| puts state.name, ' -- '}
88
+ #
89
+ # ...produces:
90
+ #
91
+ # parked -- idling --
92
+ def each
93
+ @nodes.each {|node| yield node}
94
+ self
95
+ end
96
+
97
+ # Gets the node at the given index.
98
+ #
99
+ # states = StateMachine::NodeCollection.new
100
+ # states << StateMachine::State.new(machine, :parked)
101
+ # states << StateMachine::State.new(machine, :idling)
102
+ #
103
+ # states.at(0).name # => :parked
104
+ # states.at(1).name # => :idling
105
+ def at(index)
106
+ @nodes[index]
107
+ end
108
+
109
+ # Gets the node indexed by the given key. By default, this will look up the
110
+ # key in the first index configured for the collection. A custom index can
111
+ # be specified like so:
112
+ #
113
+ # collection['parked', :value]
114
+ #
115
+ # The above will look up the "parked" key in a hash indexed by each node's
116
+ # +value+ attribute.
117
+ #
118
+ # If the key cannot be found, then nil will be returned.
119
+ def [](key, index_name = @default_index)
120
+ index(index_name)[key]
121
+ end
122
+
123
+ # Gets the node indexed by the given key. By default, this will look up the
124
+ # key in the first index configured for the collection. A custom index can
125
+ # be specified like so:
126
+ #
127
+ # collection['parked', :value]
128
+ #
129
+ # The above will look up the "parked" key in a hash indexed by each node's
130
+ # +value+ attribute.
131
+ #
132
+ # If the key cannot be found, then an IndexError exception will be raised:
133
+ #
134
+ # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
135
+ def fetch(key, index_name = @default_index)
136
+ self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
137
+ end
138
+
139
+ private
140
+ # Gets the given index. If the index does not exist, then an ArgumentError
141
+ # is raised.
142
+ def index(name)
143
+ raise ArgumentError, 'No indices configured' unless @indices.any?
144
+ @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
145
+ end
146
+
147
+ # Gets the value for the given attribute on the node
148
+ def value(node, attribute)
149
+ node.send(attribute)
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,260 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/condition_proxy'
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
+ # Tracks all of the methods that have been defined for the machine's owner
45
+ # class when objects are in this state.
46
+ #
47
+ # Maps :method_name => UnboundMethod
48
+ attr_reader :methods
49
+
50
+ # Creates a new state within the context of the given machine.
51
+ #
52
+ # Configuration options:
53
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
54
+ # machine. Default is false.
55
+ # * <tt>:value</tt> - The value to store when an object transitions to this
56
+ # state. Default is the name (stringified).
57
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
58
+ # then setting this to true will cache the evaluated result
59
+ # * <tt>:if</tt> - Determines whether a value matches this state
60
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
61
+ # By default, the configured value is matched.
62
+ # * <tt>:human_name</tt> - The human-readable version of this state's name
63
+ def initialize(machine, name, options = {}) #:nodoc:
64
+ assert_valid_keys(options, :initial, :value, :cache, :if, :human_name)
65
+
66
+ @machine = machine
67
+ @name = name
68
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
69
+ @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
70
+ @value = options.include?(:value) ? options[:value] : name && name.to_s
71
+ @cache = options[:cache]
72
+ @matcher = options[:if]
73
+ @methods = {}
74
+ @initial = options[:initial] == true
75
+
76
+ add_predicate
77
+ end
78
+
79
+ # Creates a copy of this state in addition to the list of associated
80
+ # methods to prevent conflicts across different states.
81
+ def initialize_copy(orig) #:nodoc:
82
+ super
83
+ @methods = methods.dup
84
+ end
85
+
86
+ # Determines whether there are any states that can be transitioned to from
87
+ # this state. If there are none, then this state is considered *final*.
88
+ # Any objects in a final state will remain so forever given the current
89
+ # machine's definition.
90
+ def final?
91
+ !machine.events.any? do |event|
92
+ event.guards.any? do |guard|
93
+ guard.state_requirements.any? do |requirement|
94
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ # Transforms the state name into a more human-readable format, such as
101
+ # "first gear" instead of "first_gear"
102
+ def human_name(klass = @machine.owner_class)
103
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
104
+ end
105
+
106
+ # Generates a human-readable description of this state's name / value:
107
+ #
108
+ # For example,
109
+ #
110
+ # State.new(machine, :parked).description # => "parked"
111
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
112
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
113
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
114
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
115
+ def description
116
+ description = name ? name.to_s : name.inspect
117
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
118
+ description
119
+ end
120
+
121
+ # The value that represents this state. This will optionally evaluate the
122
+ # original block if it's a lambda block. Otherwise, the static value is
123
+ # returned.
124
+ #
125
+ # For example,
126
+ #
127
+ # State.new(machine, :parked, :value => 1).value # => 1
128
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
129
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
130
+ def value(eval = true)
131
+ if @value.is_a?(Proc) && eval
132
+ if cache_value?
133
+ @value = @value.call
134
+ machine.states.update(self)
135
+ @value
136
+ else
137
+ @value.call
138
+ end
139
+ else
140
+ @value
141
+ end
142
+ end
143
+
144
+ # Determines whether this state matches the given value. If no matcher is
145
+ # configured, then this will check whether the values are equivalent.
146
+ # Otherwise, the matcher will determine the result.
147
+ #
148
+ # For example,
149
+ #
150
+ # # Without a matcher
151
+ # state = State.new(machine, :parked, :value => 1)
152
+ # state.matches?(1) # => true
153
+ # state.matches?(2) # => false
154
+ #
155
+ # # With a matcher
156
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
157
+ # state.matches?(nil) # => false
158
+ # state.matches?(Time.now) # => true
159
+ def matches?(other_value)
160
+ matcher ? matcher.call(other_value) : other_value == value
161
+ end
162
+
163
+ # Defines a context for the state which will be enabled on instances of
164
+ # the owner class when the machine is in this state.
165
+ #
166
+ # This can be called multiple times. Each time a new context is created,
167
+ # a new module will be included in the owner class.
168
+ def context(&block)
169
+ owner_class = machine.owner_class
170
+ machine_name = machine.name
171
+ name = self.name
172
+
173
+ # Evaluate the method definitions
174
+ context = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(machine_name).states.matches?(object, name)})
175
+ context.class_eval(&block)
176
+ context.instance_methods.each do |method|
177
+ methods[method.to_sym] = context.instance_method(method)
178
+
179
+ # Calls the method defined by the current state of the machine
180
+ context.class_eval <<-end_eval, __FILE__, __LINE__
181
+ def #{method}(*args, &block)
182
+ self.class.state_machine(#{machine_name.inspect}).states.match!(self).call(self, #{method.inspect}, lambda {super}, *args, &block)
183
+ end
184
+ end_eval
185
+ end
186
+
187
+ # Include the context so that it can be bound to the owner class (the
188
+ # context is considered an ancestor, so it's allowed to be bound)
189
+ owner_class.class_eval { include context }
190
+
191
+ context
192
+ end
193
+
194
+ # Calls a method defined in this state's context on the given object. All
195
+ # arguments and any block will be passed into the method defined.
196
+ #
197
+ # If the method has never been defined for this state, then a NoMethodError
198
+ # will be raised.
199
+ def call(object, method, method_missing = nil, *args, &block)
200
+ if context_method = methods[method.to_sym]
201
+ # Method is defined by the state: proxy it through
202
+ context_method.bind(object).call(*args, &block)
203
+ else
204
+ # Dispatch to the superclass since this state doesn't handle the method
205
+ method_missing.call if method_missing
206
+ end
207
+ end
208
+
209
+ # Draws a representation of this state on the given machine. This will
210
+ # create a new node on the graph with the following properties:
211
+ # * +label+ - The human-friendly description of the state.
212
+ # * +width+ - The width of the node. Always 1.
213
+ # * +height+ - The height of the node. Always 1.
214
+ # * +shape+ - The actual shape of the node. If the state is a final
215
+ # state, then "doublecircle", otherwise "ellipse".
216
+ #
217
+ # The actual node generated on the graph will be returned.
218
+ def draw(graph)
219
+ node = graph.add_node(name ? name.to_s : 'nil',
220
+ :label => description,
221
+ :width => '1',
222
+ :height => '1',
223
+ :shape => final? ? 'doublecircle' : 'ellipse'
224
+ )
225
+
226
+ # Add open arrow for initial state
227
+ graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
228
+
229
+ node
230
+ end
231
+
232
+ # Generates a nicely formatted description of this state's contents.
233
+ #
234
+ # For example,
235
+ #
236
+ # state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
237
+ # state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
238
+ def inspect
239
+ attributes = [[:name, name], [:value, @value], [:initial, initial?], [:context, methods.keys]]
240
+ "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
241
+ end
242
+
243
+ private
244
+ # Should the value be cached after it's evaluated for the first time?
245
+ def cache_value?
246
+ @cache
247
+ end
248
+
249
+ # Adds a predicate method to the owner class so long as a name has
250
+ # actually been configured for the state
251
+ def add_predicate
252
+ return unless name
253
+
254
+ # Checks whether the current value matches this state
255
+ machine.define_instance_method("#{qualified_name}?") do |machine, object|
256
+ machine.states.matches?(object, name)
257
+ end
258
+ end
259
+ end
260
+ end