class FiniteMachine::StateMachine

Base class for state machine

Public Class Methods

new(*args, &block) click to toggle source

Initialize state machine

@example

fsm = FiniteMachine::StateMachine.new(target_alias: :car) do
  initial :red

  event :go, :red => :green

  on_transition do |event|
    car.state = event.to
  end
end

@param [Hash] options

the options to create state machine with

@option options [String] :alias_target

the alias for target object

@api private

# File lib/finite_machine/state_machine.rb, line 89
def initialize(*args, &block)
  options = args.last.is_a?(::Hash) ? args.pop : {}
  @initial_state = DEFAULT_STATE
  @auto_methods  = options.fetch(:auto_methods, true)
  @subscribers   = Subscribers.new
  @observer      = Observer.new(self)
  @events_map    = EventsMap.new
  @env           = Env.new(self, [])
  @dsl           = DSL.new(self, options)

  env.target = args.pop unless args.empty?
  env.aliases << options[:alias_target] if options[:alias_target]
  dsl.call(&block) if block_given?
  trigger_init
end

Public Instance Methods

auto_methods?() click to toggle source

Check if event methods should be auto generated

@return [Boolean]

@api public

# File lib/finite_machine/state_machine.rb, line 110
def auto_methods?
  @auto_methods
end
can?(*args) click to toggle source

Checks if event can be triggered

@example

fsm.can?(:go) # => true

@example

fsm.can?(:go, "Piotr")  # checks condition with parameter "Piotr"

@param [String] event

@return [Boolean]

@api public

# File lib/finite_machine/state_machine.rb, line 205
def can?(*args)
  event_name = args.shift
  events_map.can_perform?(event_name, current, *args)
end
cannot?(*args, &block) click to toggle source

Checks if event cannot be triggered

@example

fsm.cannot?(:go) # => false

@param [String] event

@return [Boolean]

@api public

# File lib/finite_machine/state_machine.rb, line 220
def cannot?(*args, &block)
  !can?(*args, &block)
end
current() click to toggle source

Get current state

@return [String]

@api public

# File lib/finite_machine/state_machine.rb, line 146
def current
  sync_shared { state }
end
events() click to toggle source

Retireve all event names

@example

fsm.events # => [:init, :start, :stop]

@return [Array]

@api public

# File lib/finite_machine/state_machine.rb, line 188
def events
  events_map.events
end
inspect() click to toggle source

String representation of this machine

@return [String]

@api public

# File lib/finite_machine/state_machine.rb, line 392
def inspect
  sync_shared do
    "<##{self.class}:0x#{object_id.to_s(16)} " \
    "@current=#{current.inspect} " \
    "@states=#{states} " \
    "@events=#{events} " \
    "@transitions=#{events_map.state_transitions}>"
  end
end
is?(state) click to toggle source

Check if current state matches provided state

@example

fsm.is?(:green) # => true

@param [String, Array] state

@return [Boolean]

@api public

# File lib/finite_machine/state_machine.rb, line 160
def is?(state)
  if state.is_a?(Array)
    state.include? current
  else
    state == current
  end
end
notify(hook_event_type, event_name, from, *data) click to toggle source

Notify about event all the subscribers

@param [HookEvent] :hook_event_type

The hook event type.

@param [FiniteMachine::Transition] :event_transition

The event transition.

@param [Array] :data

The data associated with the hook event.

@return [nil]

@api private

# File lib/finite_machine/state_machine.rb, line 269
def notify(hook_event_type, event_name, from, *data)
  sync_shared do
    hook_event = hook_event_type.build(current, event_name, from)
    subscribers.visit(hook_event, *data)
  end
end
restore!(state) click to toggle source

Restore this machine to a known state

@param [Symbol] state

@return nil

@api public

# File lib/finite_machine/state_machine.rb, line 240
def restore!(state)
  sync_exclusive { self.state = state }
end
states() click to toggle source

Retrieve all states

@example

fsm.states # => [:yellow, :green, :red]

@return [Array]

@api public

# File lib/finite_machine/state_machine.rb, line 176
def states
  sync_shared { events_map.states }
end
subscribe(*observers) click to toggle source

Subscribe observer for event notifications

@example

machine.subscribe(Observer.new(machine))

@api public

# File lib/finite_machine/state_machine.rb, line 137
def subscribe(*observers)
  sync_exclusive { subscribers.subscribe(*observers) }
end
target() click to toggle source

Attach state machine to an object

This allows state machine to initiate events in the context of a particular object

@example

FiniteMachine.define(target: object) do
  ...
end

@return [Object|FiniteMachine::StateMachine]

@api public

# File lib/finite_machine/state_machine.rb, line 127
def target
  env.target
end
terminated?() click to toggle source

Checks if terminal state has been reached

@return [Boolean]

@api public

# File lib/finite_machine/state_machine.rb, line 229
def terminated?
  is?(terminal_states)
end
transition(event_name, *data, &block) click to toggle source
# File lib/finite_machine/state_machine.rb, line 365
def transition(event_name, *data, &block)
  transition!(event_name, *data, &block)
rescue InvalidStateError, TransitionError
  false
end
transition!(event_name, *data, &block) click to toggle source

Find available state to transition to and transition

@param [Symbol] event_name

@api private

# File lib/finite_machine/state_machine.rb, line 352
def transition!(event_name, *data, &block)
  from_state = current
  to_state   = events_map.move_to(event_name, from_state, *data)

  block.call(from_state, to_state) if block

  if log_transitions
    Logger.report_transition(event_name, from_state, to_state, *data)
  end

  try_trigger(event_name) { transition_to!(to_state) }
end
transition_to!(new_state) click to toggle source

Update this state machine state to new one

@param [Symbol] new_state

@raise [TransitionError]

@api private

# File lib/finite_machine/state_machine.rb, line 378
def transition_to!(new_state)
  from_state = current
  self.state = new_state
  self.initial_state = new_state if from_state == DEFAULT_STATE
  true
rescue Exception => e
  catch_error(e) || raise_transition_error(e)
end
trigger(event_name, *data, &block) click to toggle source

Trigger transition event without raising any errors

@param [Symbol] event_name

@return [Boolean]

true on successful transition, false otherwise

@api public

# File lib/finite_machine/state_machine.rb, line 341
def trigger(event_name, *data, &block)
  trigger!(event_name, *data, &block)
rescue InvalidStateError, TransitionError, CallbackError
  false
end
trigger!(event_name, *data, &block) click to toggle source

Trigger transition event with data

@param [Symbol] event_name

the event name

@param [Array] data

@return [Boolean]

true when transition is successful, false otherwise

@api public

# File lib/finite_machine/state_machine.rb, line 304
def trigger!(event_name, *data, &block)
  from = current # Save away current state

  sync_exclusive do
    notify HookEvent::Before, event_name, from, *data

    status = try_trigger(event_name) do
      if can?(event_name, *data)
        notify HookEvent::Exit, event_name, from, *data

        stat = transition!(event_name, *data, &block)

        notify HookEvent::Transition, event_name, from, *data
        notify HookEvent::Enter, event_name, from, *data
      else
        stat = false
      end
      stat
    end

    notify HookEvent::After, event_name, from, *data

    status
  end
rescue Exception => err
  self.state = from # rollback transition
  raise err
end
try_trigger(event_name) { || ... } click to toggle source

Attempt performing event trigger for valid state

@return [Boolean]

true is trigger successful, false otherwise

@api private

# File lib/finite_machine/state_machine.rb, line 282
def try_trigger(event_name)
  if valid_state?(event_name)
    yield
  else
    exception = InvalidStateError
    catch_error(exception) ||
      raise(exception, "inappropriate current state '#{current}'")

    false
  end
end
valid_state?(event_name) click to toggle source

Check if state is reachable

@param [Symbol] event_name

the event name for all transitions

@return [Boolean]

@api private

# File lib/finite_machine/state_machine.rb, line 252
def valid_state?(event_name)
  current_states = events_map.states_for(event_name)
  current_states.any? { |state| state == current || state == ANY_STATE }
end

Private Instance Methods

method_missing(method_name, *args, &block) click to toggle source

Forward the message to observer or self

@param [String] method_name

@param [Array] args

@return [self]

@api private

Calls superclass method
# File lib/finite_machine/state_machine.rb, line 425
def method_missing(method_name, *args, &block)
  if observer.respond_to?(method_name.to_sym)
    observer.public_send(method_name.to_sym, *args, &block)
  elsif env.aliases.include?(method_name.to_sym)
    env.send(:target, *args, &block)
  else
    super
  end
end
raise_transition_error(error) click to toggle source

Raise when failed to transition between states

@param [Exception] error

the error to describe

@raise [TransitionError]

@api private

# File lib/finite_machine/state_machine.rb, line 412
def raise_transition_error(error)
  raise TransitionError, Logger.format_error(error)
end
respond_to_missing?(method_name, include_private = false) click to toggle source

Test if a message can be handled by state machine

@param [String] method_name

@param [Boolean] include_private

@return [Boolean]

@api private

Calls superclass method
# File lib/finite_machine/state_machine.rb, line 444
def respond_to_missing?(method_name, include_private = false)
  observer.respond_to?(method_name.to_sym) ||
    env.aliases.include?(method_name.to_sym) || super
end