Class: StateMachine::State
- Inherits:
-
Object
- Object
- StateMachine::State
- Includes:
- Assertions
- Defined in:
- lib/state_machine/state.rb
Overview
A state defines a value that an attribute can be in after being transitioned 0 or more times. States can represent a value of any type in Ruby, though the most common (and default) type is String.
In addition to defining the machine’s value, a state can also define a behavioral context for an object when that object is in the state. See StateMachine::Machine#state for more information about how state-driven behavior can be utilized.
Instance Attribute Summary collapse
-
#cache ⇒ Object
Whether this state’s value should be cached after being evaluated.
-
#initial ⇒ Object
(also: #initial?)
Whether or not this state is the initial state to use for new objects.
-
#machine ⇒ Object
The state machine for which this state is defined.
-
#matcher ⇒ Object
A custom lambda block for determining whether a given value matches this state.
-
#methods ⇒ Object
readonly
Tracks all of the methods that have been defined for the machine’s owner class when objects are in this state.
-
#name ⇒ Object
readonly
The unique identifier for the state used in event and callback definitions.
-
#qualified_name ⇒ Object
readonly
The fully-qualified identifier for the state, scoped by the machine’s namespace.
-
#value(eval = true) ⇒ Object
The value that represents this state.
Instance Method Summary collapse
-
#call(object, method, *args, &block) ⇒ Object
Calls a method defined in this state’s context on the given object.
-
#context(&block) ⇒ Object
Defines a context for the state which will be enabled on instances of the owner class when the machine is in this state.
-
#description ⇒ Object
Generates a human-readable description of this state’s name / value:.
-
#draw(graph) ⇒ Object
Draws a representation of this state on the given machine.
-
#final? ⇒ Boolean
Determines whether there are any states that can be transitioned to from this state.
-
#initialize(machine, name, options = {}) ⇒ State
constructor
Creates a new state within the context of the given machine.
-
#initialize_copy(orig) ⇒ Object
Creates a copy of this state in addition to the list of associated methods to prevent conflicts across different states.
-
#inspect ⇒ Object
Generates a nicely formatted description of this state’s contents.
-
#matches?(other_value) ⇒ Boolean
Determines whether this state matches the given value.
Methods included from Assertions
#assert_exclusive_keys, #assert_valid_keys
Constructor Details
#initialize(machine, name, options = {}) ⇒ State
Creates a new state within the context of the given machine.
Configuration options:
-
:initial- Whether this state is the beginning state for the machine. Default is false. -
:value- The value to store when an object transitions to this state. Default is the name (stringified). -
:cache- If a dynamic value (via a lambda block) is being used, then setting this to true will cache the evaluated result -
:if- Determines whether a value matches this state (e.g. :value => lambda Time.now, :if => lambda {|state| !state.nil?}). By default, the configured value is matched.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/state_machine/state.rb', line 59 def initialize(machine, name, = {}) #:nodoc: assert_valid_keys(, :initial, :value, :cache, :if) @machine = machine @name = name @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name @value = .include?(:value) ? [:value] : name && name.to_s @cache = [:cache] @matcher = [:if] @methods = {} @initial = [:initial] == true add_predicate end |
Instance Attribute Details
#cache ⇒ Object
Whether this state’s value should be cached after being evaluated
31 32 33 |
# File 'lib/state_machine/state.rb', line 31 def cache @cache end |
#initial ⇒ Object Also known as: initial?
Whether or not this state is the initial state to use for new objects
34 35 36 |
# File 'lib/state_machine/state.rb', line 34 def initial @initial end |
#machine ⇒ Object
The state machine for which this state is defined
17 18 19 |
# File 'lib/state_machine/state.rb', line 17 def machine @machine end |
#matcher ⇒ Object
A custom lambda block for determining whether a given value matches this state
39 40 41 |
# File 'lib/state_machine/state.rb', line 39 def matcher @matcher end |
#methods ⇒ Object (readonly)
Tracks all of the methods that have been defined for the machine’s owner class when objects are in this state.
Maps :method_name => UnboundMethod
45 46 47 |
# File 'lib/state_machine/state.rb', line 45 def methods @methods end |
#name ⇒ Object (readonly)
The unique identifier for the state used in event and callback definitions
20 21 22 |
# File 'lib/state_machine/state.rb', line 20 def name @name end |
#qualified_name ⇒ Object (readonly)
The fully-qualified identifier for the state, scoped by the machine’s namespace
24 25 26 |
# File 'lib/state_machine/state.rb', line 24 def qualified_name @qualified_name end |
#value(eval = true) ⇒ Object
The value that represents this state. This will optionally evaluate the original block if it’s a lambda block. Otherwise, the static value is returned.
For example,
State.new(machine, :parked, :value => 1).value # => 1
State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/state_machine/state.rb', line 119 def value(eval = true) if @value.is_a?(Proc) && eval if cache_value? @value = @value.call machine.states.update(self) @value else @value.call end else @value end end |
Instance Method Details
#call(object, method, *args, &block) ⇒ Object
Calls a method defined in this state’s context on the given object. All arguments and any block will be passed into the method defined.
If the method has never been defined for this state, then a NoMethodError will be raised.
188 189 190 191 192 193 194 195 196 |
# File 'lib/state_machine/state.rb', line 188 def call(object, method, *args, &block) if context_method = methods[method.to_sym] # Method is defined by the state: proxy it through context_method.bind(object).call(*args, &block) else # Raise exception as if the method never existed on the original object raise NoMethodError, "undefined method '#{method}' for #{object} with #{name || 'nil'} #{machine.name}" end end |
#context(&block) ⇒ Object
Defines a context for the state which will be enabled on instances of the owner class when the machine is in this state.
This can be called multiple times. Each time a new context is created, a new module will be included in the owner class.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/state_machine/state.rb', line 157 def context(&block) owner_class = machine.owner_class machine_name = machine.name name = self.name # Evaluate the method definitions context = ConditionProxy.new(owner_class, lambda {|object| object.class.state_machine(machine_name).states.matches?(object, name)}) context.class_eval(&block) context.instance_methods.each do |method| methods[method.to_sym] = context.instance_method(method) # Calls the method defined by the current state of the machine context.class_eval " def \#{method}(*args, &block)\n self.class.state_machine(\#{machine_name.inspect}).states.match!(self).call(self, \#{method.inspect}, *args, &block)\n end\n end_eval\n end\n \n # Include the context so that it can be bound to the owner class (the\n # context is considered an ancestor, so it's allowed to be bound)\n owner_class.class_eval { include context }\n \n context\nend\n", __FILE__, __LINE__ |
#description ⇒ Object
Generates a human-readable description of this state’s name / value:
For example,
State.new(machine, :parked).description # => "parked"
State.new(machine, :parked, :value => :parked).description # => "parked"
State.new(machine, :parked, :value => nil).description # => "parked (nil)"
State.new(machine, :parked, :value => 1).description # => "parked (1)"
State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
104 105 106 107 108 |
# File 'lib/state_machine/state.rb', line 104 def description description = name ? name.to_s : name.inspect description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s description end |
#draw(graph) ⇒ Object
Draws a representation of this state on the given machine. This will create a new node on the graph with the following properties:
-
label- The human-friendly description of the state. -
width- The width of the node. Always 1. -
height- The height of the node. Always 1. -
shape- The actual shape of the node. If the state is a final state, then “doublecircle”, otherwise “ellipse”.
The actual node generated on the graph will be returned.
207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/state_machine/state.rb', line 207 def draw(graph) node = graph.add_node(name ? name.to_s : 'nil', :label => description, :width => '1', :height => '1', :shape => final? ? 'doublecircle' : 'ellipse' ) # Add open arrow for initial state graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial? node end |
#final? ⇒ Boolean
Determines whether there are any states that can be transitioned to from this state. If there are none, then this state is considered final. Any objects in a final state will remain so forever given the current machine’s definition.
85 86 87 88 89 90 91 92 93 |
# File 'lib/state_machine/state.rb', line 85 def final? !machine.events.any? do |event| event.guards.any? do |guard| guard.state_requirements.any? do |requirement| requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name) end end end end |
#initialize_copy(orig) ⇒ Object
Creates a copy of this state in addition to the list of associated methods to prevent conflicts across different states.
76 77 78 79 |
# File 'lib/state_machine/state.rb', line 76 def initialize_copy(orig) #:nodoc: super @methods = methods.dup end |
#inspect ⇒ Object
Generates a nicely formatted description of this state’s contents.
For example,
state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
227 228 229 230 |
# File 'lib/state_machine/state.rb', line 227 def inspect attributes = [[:name, name], [:value, @value], [:initial, initial?], [:context, methods.keys]] "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>" end |
#matches?(other_value) ⇒ Boolean
Determines whether this state matches the given value. If no matcher is configured, then this will check whether the values are equivalent. Otherwise, the matcher will determine the result.
For example,
# Without a matcher
state = State.new(machine, :parked, :value => 1)
state.matches?(1) # => true
state.matches?(2) # => false
# With a matcher
state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
state.matches?(nil) # => false
state.matches?(Time.now) # => true
148 149 150 |
# File 'lib/state_machine/state.rb', line 148 def matches?(other_value) matcher ? matcher.call(other_value) : other_value == value end |