Class: StateMachines::StateContext

Inherits:
Module
  • Object
show all
Includes:
EvalHelpers
Defined in:
lib/state_machines/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 = StateMachines::Machine.new(Vehicle)
context = StateMachines::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

Constructor Details

#initialize(state) ⇒ StateContext

Creates a new context for the given state



63
64
65
66
67
68
69
70
# File 'lib/state_machines/state_context.rb', line 63

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



97
98
99
100
101
102
103
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
# File 'lib/state_machines/state_context.rb', line 97

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



57
58
59
# File 'lib/state_machines/state_context.rb', line 57

def machine
  @machine
end

#stateObject (readonly)

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



60
61
62
# File 'lib/state_machines/state_context.rb', line 60

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 StateMachines::Machine#transition for a description of the possible configurations for defining transitions.

Raises:

  • (ArgumentError)


88
89
90
91
92
93
94
# File 'lib/state_machines/state_context.rb', line 88

def transition(options)
  options.assert_valid_keys(: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