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

cache[RW]

Whether this state's value should be cached after being evaluated

human_name[W]

The human-readable name for the state

initial[RW]

Whether or not this state is the initial state to use for new objects

initial?[RW]

Whether or not this state is the initial state to use for new objects

machine[RW]

The state machine for which this state is defined

matcher[RW]

A custom lambda block for determining whether a given value matches this state

name[R]

The unique identifier for the state used in event and callback definitions

qualified_name[R]

The fully-qualified identifier for the state, scoped by the machine's namespace

value[W]

The value that is written to a machine's attribute when an object transitions into this state

Public Instance Methods

call(object, method, *args, &block) click to toggle source

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
context(&block) click to toggle source

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
context_methods() click to toggle source

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
description(options = {}) click to toggle source

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
draw(graph, options = {}) click to toggle source

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
final?() click to toggle source

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
human_name(klass = @machine.owner_class) click to toggle source

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
inspect() click to toggle source

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
matches?(other_value) click to toggle source

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
value(eval = true) click to toggle source

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

add_predicate() click to toggle source

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
cache_value?() click to toggle source

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
context_name_for(method) click to toggle source

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