Class: FiniteMachine::StateMachine

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Catchable, Threadable
Defined in:
lib/finite_machine/state_machine.rb

Overview

Base class for state machine

Instance Method Summary collapse

Methods included from Catchable

#catch_error, #handle, included

Constructor Details

#initialize(*args, &block) ⇒ StateMachine

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initialize state machine

Examples:

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

  event :go, :red => :green

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

Parameters:

  • options (Hash)

    the options to create state machine with



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ self (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Forward the message to observer or self

Parameters:

  • method_name (String)
  • args (Array)

Returns:

  • (self)


425
426
427
428
429
430
431
432
433
# 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

Instance Method Details

#auto_methods?Boolean

Check if event methods should be auto generated

Returns:

  • (Boolean)


110
111
112
# File 'lib/finite_machine/state_machine.rb', line 110

def auto_methods?
  @auto_methods
end

#can?(*args) ⇒ Boolean

Checks if event can be triggered

Examples:

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

Parameters:

  • event (String)

Returns:

  • (Boolean)


205
206
207
208
# 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) ⇒ Boolean

Checks if event cannot be triggered

Examples:

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

Parameters:

  • event (String)

Returns:

  • (Boolean)


220
221
222
# File 'lib/finite_machine/state_machine.rb', line 220

def cannot?(*args, &block)
  !can?(*args, &block)
end

#currentString

Get current state

Returns:

  • (String)


146
147
148
# File 'lib/finite_machine/state_machine.rb', line 146

def current
  sync_shared { state }
end

#dslDSL

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Machine dsl

Returns:



48
# File 'lib/finite_machine/state_machine.rb', line 48

attr_threadsafe :dsl

#eventsArray[Symbol]

Retireve all event names

Examples:

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

Returns:

  • (Array[Symbol])


188
189
190
# File 'lib/finite_machine/state_machine.rb', line 188

def events
  events_map.events
end

#inspectString

String representation of this machine

Returns:

  • (String)


392
393
394
395
396
397
398
399
400
# 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) ⇒ Boolean

Check if current state matches provided state

Examples:

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

Parameters:

  • state (String, Array[String])

Returns:

  • (Boolean)


160
161
162
163
164
165
166
# 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) ⇒ nil

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Notify about event all the subscribers

Parameters:

  • :hook_event_type (HookEvent)

    The hook event type.

  • :event_transition (FiniteMachine::Transition)

    The event transition.

  • :data (Array[Object])

    The data associated with the hook event.

Returns:

  • (nil)


269
270
271
272
273
274
# 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

#observerObserver

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The state machine observer

Returns:



55
# File 'lib/finite_machine/state_machine.rb', line 55

attr_threadsafe :observer

#restore!(state) ⇒ Object

Restore this machine to a known state

Parameters:

  • state (Symbol)

Returns:

  • nil



240
241
242
# File 'lib/finite_machine/state_machine.rb', line 240

def restore!(state)
  sync_exclusive { self.state = state }
end

#stateSymbol

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Current state

Returns:

  • (Symbol)


26
# File 'lib/finite_machine/state_machine.rb', line 26

attr_threadsafe :state

#statesArray[Symbol]

Retrieve all states

Examples:

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

Returns:

  • (Array[Symbol])


176
177
178
# File 'lib/finite_machine/state_machine.rb', line 176

def states
  sync_shared { events_map.states }
end

#subscribe(*observers) ⇒ Object

Subscribe observer for event notifications

Examples:

machine.subscribe(Observer.new(machine))


137
138
139
# File 'lib/finite_machine/state_machine.rb', line 137

def subscribe(*observers)
  sync_exclusive { subscribers.subscribe(*observers) }
end

#subscribersSubscribers

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The state machine subscribers

Returns:



62
# File 'lib/finite_machine/state_machine.rb', line 62

attr_threadsafe :subscribers

#targetObject|FiniteMachine::StateMachine

Attach state machine to an object

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

Examples:

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

Returns:



127
128
129
# File 'lib/finite_machine/state_machine.rb', line 127

def target
  env.target
end

#terminated?Boolean

Checks if terminal state has been reached

Returns:

  • (Boolean)


229
230
231
# File 'lib/finite_machine/state_machine.rb', line 229

def terminated?
  is?(terminal_states)
end

#transition(event_name, *data, &block) ⇒ Object



365
366
367
368
369
# 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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Find available state to transition to and transition

Parameters:

  • event_name (Symbol)


352
353
354
355
356
357
358
359
360
361
362
363
# 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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Update this state machine state to new one

Parameters:

  • new_state (Symbol)

Raises:



378
379
380
381
382
383
384
385
# 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) ⇒ Boolean

Trigger transition event without raising any errors

Parameters:

  • event_name (Symbol)

Returns:

  • (Boolean)

    true on successful transition, false otherwise



341
342
343
344
345
# 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) ⇒ Boolean

Trigger transition event with data

Parameters:

  • event_name (Symbol)

    the event name

  • data (Array)

Returns:

  • (Boolean)

    true when transition is successful, false otherwise



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# 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) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Attempt performing event trigger for valid state

Returns:

  • (Boolean)

    true is trigger successful, false otherwise



282
283
284
285
286
287
288
289
290
291
292
# 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) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check if state is reachable

Parameters:

  • event_name (Symbol)

    the event name for all transitions

Returns:

  • (Boolean)


252
253
254
255
# 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