Class: StateMachine::StateContext

Inherits:
Module
  • Object
show all
Includes:
Assertions, EvalHelpers
Defined in:
lib/state_machine/state_context.rb

Overview

Represents a module which will get evaluated within the context of a state.

Class-level methods are proxied to the owner class, injecting a custom :if condition along with method. This assumes that the method has support for a set of configuration options, including :if. This condition will check that the object's state matches this context's state.

Instance-level methods are used to define state-driven behavior on the state's owner class.

Examples

class Vehicle
  class << self
    attr_accessor :validations

    def validate(options, &block)
      validations << options
    end
  end

  self.validations = []
  attr_accessor :state, :simulate

  def moving?
    self.class.validations.all? {|validation| validation[:if].call(self)}
  end
end

In the above class, a simple set of validation behaviors have been defined. Each validation consists of a configuration like so:

Vehicle.validate :unless => :simulate
Vehicle.validate :if => lambda {|vehicle| ...}

In order to scope validations to a particular state context, the class-level validate method can be invoked like so:

machine = StateMachine::Machine.new(Vehicle)
context = StateMachine::StateContext.new(machine.state(:first_gear))
context.validate(:unless => :simulate)

vehicle = Vehicle.new     # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
vehicle.moving?           # => false

vehicle.state = 'first_gear'
vehicle.moving?           # => true

vehicle.simulate = true
vehicle.moving?           # => false

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from EvalHelpers

#evaluate_method

Methods included from Assertions

#assert_exclusive_keys, #assert_valid_keys

Constructor Details

#initialize(state) ⇒ StateContext

Creates a new context for the given state



70
71
72
73
74
75
76
77
# File 'lib/state_machine/state_context.rb', line 70

def initialize(state)
  @state = state
  @machine = state.machine
  
  state_name = state.name
  machine_name = machine.name
  @condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(*args, &block) ⇒ Object

Hooks in condition-merging to methods that don't exist in this module



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/state_machine/state_context.rb', line 104

def method_missing(*args, &block)
  # Get the configuration
  if args.last.is_a?(Hash)
    options = args.last
  else
    args << options = {}
  end
  
  # Get any existing condition that may need to be merged
  if_condition = options.delete(:if)
  unless_condition = options.delete(:unless)
  
  # Provide scope access to configuration in case the block is evaluated
  # within the object instance
  proxy = self
  proxy_condition = @condition
  
  # Replace the configuration condition with the one configured for this
  # proxy, merging together any existing conditions
  options[:if] = lambda do |*condition_args|
    # Block may be executed within the context of the actual object, so
    # it'll either be the first argument or the executing context
    object = condition_args.first || self
    
    proxy.evaluate_method(object, proxy_condition) &&
    Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
    !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
  end
  
  # Evaluate the method on the owner class with the condition proxied
  # through
  machine.owner_class.send(*args, &block)
end

Instance Attribute Details

#machineObject (readonly)

The state machine for which this context's state is defined



64
65
66
# File 'lib/state_machine/state_context.rb', line 64

def machine
  @machine
end

#stateObject (readonly)

The state that must be present in an object for this context to be active



67
68
69
# File 'lib/state_machine/state_context.rb', line 67

def state
  @state
end

Instance Method Details

#transition(options) ⇒ Object

Creates a new transition that determines what to change the current state to when an event fires from this state.

Since this transition is being defined within a state context, you do not need to specify the :from option for the transition. For example:

state_machine do
  state :parked do
    transition :to => :idling, :on => [:ignite, :shift_up]                          # Transitions to :idling
    transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on?  # Transitions to :parked if seatbelt is off
  end
end

See StateMachine::Machine#transition for a description of the possible configurations for defining transitions.

Raises:

  • (ArgumentError)


95
96
97
98
99
100
101
# File 'lib/state_machine/state_context.rb', line 95

def transition(options)
  assert_valid_keys(options, :from, :to, :on, :if, :unless)
  raise ArgumentError, 'Must specify :on event' unless options[:on]
  raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
  
  machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
end