Class: StateMachine::State

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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, options = {}) #:nodoc:
  assert_valid_keys(options, :initial, :value, :cache, :if)
  
  @machine = machine
  @name = name
  @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
  @value = options.include?(:value) ? options[:value] : name && name.to_s
  @cache = options[:cache]
  @matcher = options[:if]
  @methods = {}
  @initial = options[:initial] == true
  
  add_predicate
end

Instance Attribute Details

#cacheObject

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

#initialObject 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

#machineObject

The state machine for which this state is defined



17
18
19
# File 'lib/state_machine/state.rb', line 17

def machine
  @machine
end

#matcherObject

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

#methodsObject (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

#nameObject (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_nameObject (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__

#descriptionObject

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

#inspectObject

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