verborghs-state_machine 0.9.4

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