class EnumStateMachine::State
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 EnumStateMachine::Machine#state
for more information about how state-driven behavior can be utilized.
Attributes
Whether this state's value should be cached after being evaluated
The human-readable name for the state
Whether or not this state is the initial state to use for new objects
Whether or not this state is the initial state to use for new objects
The state machine for which this state is defined
A custom lambda block for determining whether a given value matches this state
The unique identifier for the state used in event and callback definitions
The fully-qualified identifier for the state, scoped by the machine's namespace
The value that is written to a machine's attribute when an object transitions into this state
Public Instance Methods
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.
# File lib/enum_state_machine/state.rb 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
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.
# File lib/enum_state_machine/state.rb 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
The list of methods that have been defined in this state's context
# File lib/enum_state_machine/state.rb 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
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 (*)
Configuration options:
-
:human_name
- Whether to use this state's human name in the description or just the internal name
# File lib/enum_state_machine/state.rb 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
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”.
Configuration options:
-
:human_name
- Whether to use the state's human name for the node's label that gets drawn on the graph
# File lib/enum_state_machine/state.rb 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
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.
# File lib/enum_state_machine/state.rb 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
Transforms the state name into a more human-readable format, such as “first gear” instead of “first_gear”
# File lib/enum_state_machine/state.rb 109 def human_name(klass = @machine.owner_class) 110 @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name 111 end
Generates a nicely formatted description of this state's contents.
For example,
state = EnumStateMachine::State.new(machine, :parked, :value => 1, :initial => true) state # => #<EnumStateMachine::State name=:parked value=1 initial=true context=[]>
# File lib/enum_state_machine/state.rb 272 def inspect 273 attributes = [[:name, name], [:value, @value], [:initial, initial?]] 274 "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>" 275 end
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
# File lib/enum_state_machine/state.rb 171 def matches?(other_value) 172 matcher ? matcher.call(other_value) : other_value == value 173 end
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@...>
# File lib/enum_state_machine/state.rb 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
Private Instance Methods
Adds a predicate method to the owner class so long as a name has actually been configured for the state
# File lib/enum_state_machine/state.rb 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
Should the value be cached after it's evaluated for the first time?
# File lib/enum_state_machine/state.rb 279 def cache_value? 280 @cache 281 end
Generates the name of the method containing the actual implementation
# File lib/enum_state_machine/state.rb 293 def context_name_for(method) 294 :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__" 295 end