Class: VeryTinyStateMachine

Inherits:
Object
  • Object
show all
Defined in:
lib/very_tiny_state_machine.rb

Overview

A mini state machine object that can be used to track a state flow.

The entire state machine lives in a separate variable, and does not pollute the class or the module of the caller. The state machine has the ability to dispatch callbacks when states are switched, the callbacks are dispatched to the given object.

@automaton = VeryTinyStateMachine.new(:initialized, self)
@automaton.permit_state :processing, :closing, :closed
@automaton.permit_transition :initialized => :processing, :processing => :closing
@automaton.permit_transition :closing => :closed

# Then, lower down the code
@automaton.transition! :processing

# This switches the internal state of the machine, and dispatches the following method
# calls on the object given as the second argument to the constructor, in the following order:

# self.leaving_initialized_state
# self.entering_processing_state
# self.transitioning_from_initialized_to_processing_state
# ..the state variable is switched here
# self.after_transitioning_from_initialized_to_processing_state
# self.after_leaving_initialized_state
# self.after_entering_processing_state

@automaton.transition :initialized # Will raise TinyStateMachine::InvalidFlow
@automaton.transition :something_odd # Will raise TinyStateMachine::UnknownState

@automaton.in_state?(:processing) #=> true
@automaton.in_state?(:initialized) #=> false

Constant Summary collapse

VERSION =
'2.1.0'
InvalidFlow =

Gets raised when an impossible transition gets requested

Class.new(StandardError)
UnknownState =

Gets raised when an unknown state gets requested

Class.new(StandardError)

Instance Method Summary collapse

Constructor Details

#initialize(initial_state, object_handling_callbacks = nil) ⇒ VeryTinyStateMachine

Initialize a new TinyStateMachine, with the initial state and the object that will receive callbacks.

Parameters:

  • initial_state (#to_sym)

    the initial state of the state machine

  • object_handling_callbacks (#send, #respond_to?) (defaults to: nil)

    the callback handler that will receive transition notifications



44
45
46
47
48
49
50
# File 'lib/very_tiny_state_machine.rb', line 44

def initialize(initial_state, object_handling_callbacks = nil)
  @state = initial_state.to_sym
  @flow = [@state]
  @permitted_states = Set.new([initial_state])
  @permitted_transitions = Set.new
  @callbacks_via = object_handling_callbacks
end

Instance Method Details

#expect!(requisite_state) ⇒ TrueClass

Ensure the machine is in a given state, and if it isn’t raise an InvalidFlow

Parameters:

  • requisite_state (#to_sym)

    the state to verify

Returns:

  • (TrueClass)

    true if the machine is in the requisite state

Raises:

  • InvalidFlow



114
115
116
117
118
119
# File 'lib/very_tiny_state_machine.rb', line 114

def expect!(requisite_state)
  unless requisite_state.to_sym == @state
    raise InvalidFlow, "Must be in #{requisite_state.inspect} state, but was in #{@state.inspect}"
  end
  true
end

#flow_so_farArray

Returns the flow of the transitions the machine went through so far

Returns:

  • (Array)

    the array of states



178
179
180
# File 'lib/very_tiny_state_machine.rb', line 178

def flow_so_far
  @flow.dup
end

#in_state?(requisite_state) ⇒ Boolean

Tells whether the state machine is in a given state at the moment

Parameters:

  • requisite_state (Symbol, String)

    name of the state to check for

Returns:

  • (Boolean)

    whether the machine is in that state currently



105
106
107
# File 'lib/very_tiny_state_machine.rb', line 105

def in_state?(requisite_state)
  @state == requisite_state.to_sym
end

#known?(state) ⇒ Boolean

Tells whether the state is known to this state machine

Parameters:

  • state (Symbol, String)

    the state to check for

Returns:

  • (Boolean)

    whether the state is known



87
88
89
# File 'lib/very_tiny_state_machine.rb', line 87

def known?(state)
  @permitted_states.include?(state.to_sym)
end

#may_transition_to?(to_state) ⇒ Boolean

Tells whether a transition is permitted to the given state.

Parameters:

  • to_state (Symbol, String)

    state to transition to

Returns:

  • (Boolean)

    whether the state can be transitioned to



95
96
97
98
99
# File 'lib/very_tiny_state_machine.rb', line 95

def may_transition_to?(to_state)
  to_state = to_state.to_sym
  transition = {@state => to_state.to_sym}
  @permitted_states.include?(to_state) && @permitted_transitions.include?(transition)
end

#permit_state(*states) ⇒ Set

Permit a single state or multiple states

Parameters:

  • states (Array)

    states to permit

Returns:

  • (Set)

    the Set of states added to permitted states as the result of the call



56
57
58
59
60
61
# File 'lib/very_tiny_state_machine.rb', line 56

def permit_state(*states)
  states_to_permit = Set.new(states.map(&:to_sym))
  will_be_added = states_to_permit - @permitted_states
  @permitted_states += states_to_permit
  will_be_added
end

#permit_transition(from_to_hash) ⇒ Array

Permit a transition from one state to another. If you need to add multiple transitions from the same state, just call the method multiple times:

@machine.permit_transition :initialized => :failed, :running => :closed
@machine.permit_transition :initialized => :running

Parameters:

  • from_to_hash (Hash)

    the transitions to allow

Returns:

  • (Array)

    the list of states added to permitted states



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/very_tiny_state_machine.rb', line 71

def permit_transition(from_to_hash)
  transitions_to_permit = Set.new
  from_to_hash.each_pair do | from_state, to_state |
    raise UnknownState, from_state unless @permitted_states.include?(from_state.to_sym)
    raise UnknownState, to_state unless @permitted_states.include?(to_state.to_sym)
    transitions_to_permit << {from_state.to_sym => to_state.to_sym}
  end
  additions = transitions_to_permit - @permitted_transitions
  @permitted_transitions += transitions_to_permit
  additions
end

#transition!(new_state) ⇒ Symbol

Transition to a given state. Will raise an InvalidFlow exception if the transition is impossible. Additionally, if you want to transition to a state that is already activated, an InvalidFlow will be raised if you did not permit this transition explicitly. If you want to transition to a state OR stay in it if it is already active use TinyStateMachine#transition_or_maintain!

During transitions the before callbacks will be called on the @callbacks_via instance variable. If you are transitioning from “initialized” to “processing” for instance, the following callbacks will be dispatched:

  • leaving_initialized_state

  • entering_processing_state

  • transitioning_from_initialized_to_processing_state

..the state variable is switched here

  • after_transitioning_from_initialized_to_processing_state

  • after_leaving_initialized_state

  • after_entering_processing_state

The return value of the callbacks does not matter.

Parameters:

  • new_state (#to_sym)

    the state to transition to.

Returns:

  • (Symbol)

    the state that the machine has just left

Raises:

  • InvalidFlow



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/very_tiny_state_machine.rb', line 143

def transition!(new_state)
  new_state = new_state.to_sym
  
  raise UnknownState, new_state.inspect unless known?(new_state)
  if may_transition_to?(new_state)
    dispatch_callbacks_before_transition(new_state) if @callbacks_via
    
    previous = @state
    @state = new_state.to_sym
    @flow << new_state.to_sym
    
    dispatch_callbacks_after_transition(previous) if @callbacks_via
    previous
  else
    raise InvalidFlow, 
      "Cannot change states from #{@state} to #{new_state} (flow so far: #{@flow.join(' > ')})"
  end
end

#transition_or_maintain!(new_state) ⇒ void

This method returns an undefined value.

Transition to a given state. If the machine already is in that state, do nothing. If the transition has to happen (the requested state is different than the current) transition! will be called instead.

Parameters:

  • new_state (Symbol, String)

    the state to transition to.

Raises:

  • InvalidFlow

See Also:

  • TinyStateMachine#transition!


170
171
172
173
# File 'lib/very_tiny_state_machine.rb', line 170

def transition_or_maintain!(new_state)
  return if in_state?(new_state)
  transition! new_state
end