Class: StateMachine::Event

Inherits:
Object
  • Object
show all
Includes:
Assertions, MatcherHelpers
Defined in:
lib/state_machine/event.rb

Overview

An event defines an action that transitions an attribute from one state to another. The state that an attribute is transitioned to depends on the guards configured for the event.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MatcherHelpers

#all, #same

Methods included from Assertions

#assert_exclusive_keys, #assert_valid_keys

Constructor Details

#initialize(machine, name) ⇒ Event

Creates a new event within the context of the given machine



36
37
38
39
40
41
42
43
44
# File 'lib/state_machine/event.rb', line 36

def initialize(machine, name) #:nodoc:
  @machine = machine
  @name = name
  @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
  @guards = []
  @known_states = []
  
  add_actions
end

Instance Attribute Details

#guardsObject (readonly)

The list of guards that determine what state this event transitions objects to when fired



29
30
31
# File 'lib/state_machine/event.rb', line 29

def guards
  @guards
end

#known_statesObject (readonly)

A list of all of the states known to this event using the configured guards/transitions as the source



33
34
35
# File 'lib/state_machine/event.rb', line 33

def known_states
  @known_states
end

#machineObject

The state machine for which this event is defined



19
20
21
# File 'lib/state_machine/event.rb', line 19

def machine
  @machine
end

#nameObject (readonly)

The name of the event



22
23
24
# File 'lib/state_machine/event.rb', line 22

def name
  @name
end

#qualified_nameObject (readonly)

The fully-qualified name of the event, scoped by the machine’s namespace



25
26
27
# File 'lib/state_machine/event.rb', line 25

def qualified_name
  @qualified_name
end

Instance Method Details

#can_fire?(object) ⇒ Boolean

Determines whether any transitions can be performed for this event based on the current state of the given object.

If the event can’t be fired, then this will return false, otherwise true.



159
160
161
# File 'lib/state_machine/event.rb', line 159

def can_fire?(object)
  !transition_for(object).nil?
end

#draw(graph) ⇒ Object

Draws a representation of this event on the given graph. This will create 1 or more edges on the graph for each guard (i.e. transition) configured.

A collection of the generated edges will be returned.



204
205
206
207
# File 'lib/state_machine/event.rb', line 204

def draw(graph)
  valid_states = machine.states.by_priority.map {|state| state.name}
  guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
end

#fire(object, *args) ⇒ Object

Attempts to perform the next available transition on the given object. If no transitions can be made, then this will return false, otherwise true.

Any additional arguments are passed to the StateMachine::Transition#perform instance method.



188
189
190
191
192
193
194
195
196
197
# File 'lib/state_machine/event.rb', line 188

def fire(object, *args)
  machine.reset(object)
  
  if transition = transition_for(object)
    transition.perform(*args)
  else
    machine.invalidate(object, :state, :invalid_transition, [[:event, name]])
    false
  end
end

#initialize_copy(orig) ⇒ Object

Creates a copy of this event in addition to the list of associated guards to prevent conflicts across events within a class hierarchy.



48
49
50
51
52
# File 'lib/state_machine/event.rb', line 48

def initialize_copy(orig) #:nodoc:
  super
  @guards = @guards.dup
  @known_states = @known_states.dup
end

#inspectObject

Generates a nicely formatted description of this event’s contents.

For example,

event = StateMachine::Event.new(machine, :park)
event.transition all - :idling => :parked, :idling => same
event   # => #<StateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>


216
217
218
219
220
221
222
223
224
# File 'lib/state_machine/event.rb', line 216

def inspect
  transitions = guards.map do |guard|
    guard.state_requirements.map do |state_requirement|
      "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
    end * ', '
  end
  
  "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
end

#transition(options) ⇒ Object

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

Defining transitions

The options for a new transition uses the Hash syntax to map beginning states to ending states. For example,

transition :parked => :idling, :idling => :first_gear

In this case, when the event is fired, this transition will cause the state to be idling if it’s current state is parked or first_gear if it’s current state is idling.

To help define these implicit transitions, a set of helpers are available for slightly more complex matching:

  • all - Matches every state in the machine

  • all - [:parked, :idling, ...] - Matches every state except those specified

  • any - An alias for all (matches every state in the machine)

  • same - Matches the same state being transitioned from

See StateMachine::MatcherHelpers for more information.

Examples:

transition all => nil                               # Transitions to nil regardless of the current state
transition all => :idling                           # Transitions to :idling regardless of the current state
transition all - [:idling, :first_gear] => :idling  # Transitions every state but :idling and :first_gear to :idling
transition nil => :idling                           # Transitions to :idling from the nil state
transition :parked => :idling                       # Transitions to :idling if :parked
transition [:parked, :stalled] => :idling           # Transitions to :idling if :parked or :stalled

transition :parked => same                          # Loops :parked back to :parked
transition [:parked, :stalled] => same              # Loops either :parked or :stalled back to the same state
transition all - :parked => same                    # Loops every state but :parked back to the same state

Verbose transitions

Transitions can also be defined use an explicit set of deprecated configuration options:

  • :from - A state or array of states that can be transitioned from. If not specified, then the transition can occur for any state.

  • :to - The state that’s being transitioned to. If not specified, then the transition will simply loop back (i.e. the state will not change).

  • :except_from - A state or array of states that cannot be transitioned from.

Examples:

transition :to => nil
transition :to => :idling
transition :except_from => [:idling, :first_gear], :to => :idling
transition :from => nil, :to => :idling
transition :from => [:parked, :stalled], :to => :idling

transition :from => :parked
transition :from => [:parked, :stalled]
transition :except_from => :parked

Notice that the above examples are the verbose equivalent of the examples described initially.

Conditions

In addition to the state requirements for each transition, a condition can also be defined to help determine whether that transition is available. These options will work on both the normal and verbose syntax.

Configuration options:

  • :if - A method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}). The condition should return or evaluate to true or false.

  • :unless - A method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}). The condition should return or evaluate to true or false.

Examples:

transition :parked => :idling, :if => :moving?
transition :parked => :idling, :unless => :stopped?

transition :from => :parked, :to => :idling, :if => :moving?
transition :from => :parked, :to => :idling, :unless => :stopped?

Order of operations

Transitions are evaluated in the order in which they’re defined. As a result, if more than one transition applies to a given object, then the first transition that matches will be performed.

Raises:

  • (ArgumentError)


143
144
145
146
147
148
149
150
151
152
153
# File 'lib/state_machine/event.rb', line 143

def transition(options)
  raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
  
  # Only a certain subset of explicit options are allowed for transition
  # requirements
  assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
  
  guards << guard = Guard.new(options.merge(:on => name))
  @known_states |= guard.known_states
  guard
end

#transition_for(object, requirements = {}) ⇒ Object

Finds and builds the next transition that can be performed on the given object. If no transitions can be made, then this will return nil.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/state_machine/event.rb', line 165

def transition_for(object, requirements = {})
  requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
  
  guards.each do |guard|
    if match = guard.match(object, requirements)
      # Guard allows for the transition to occur
      from = requirements[:from]
      to = match[:to].values.empty? ? from : match[:to].values.first
      
      return Transition.new(object, machine, name, from, to, !custom_from_state)
    end
  end
  
  # No transition matched
  nil
end