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



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/finite_machine/state_machine.rb', line 88

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)


422
423
424
425
426
427
428
429
430
# File 'lib/finite_machine/state_machine.rb', line 422

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)


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

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)


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

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)


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

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

#currentString

Get current state

Returns:

  • (String)


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

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])


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

def events
  events_map.events
end

#inspectString

String representation of this machine

Returns:

  • (String)


391
392
393
394
395
396
397
# File 'lib/finite_machine/state_machine.rb', line 391

def inspect
  sync_shared do
    "<##{self.class}:0x#{object_id.to_s(16)} @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)


159
160
161
162
163
164
165
# File 'lib/finite_machine/state_machine.rb', line 159

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)


268
269
270
271
272
273
# File 'lib/finite_machine/state_machine.rb', line 268

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



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

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])


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

def states
  sync_shared { events_map.states }
end

#subscribe(*observers) ⇒ Object

Subscribe observer for event notifications

Examples:

machine.subscribe(Observer.new(machine))


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

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:



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

def target
  env.target
end

#terminated?Boolean

Checks if terminal state has been reached

Returns:

  • (Boolean)


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

def terminated?
  is?(final_state)
end

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



364
365
366
367
368
# File 'lib/finite_machine/state_machine.rb', line 364

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)


351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/finite_machine/state_machine.rb', line 351

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:



377
378
379
380
381
382
383
384
# File 'lib/finite_machine/state_machine.rb', line 377

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



340
341
342
343
344
# File 'lib/finite_machine/state_machine.rb', line 340

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



303
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
# File 'lib/finite_machine/state_machine.rb', line 303

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



281
282
283
284
285
286
287
288
289
290
291
# File 'lib/finite_machine/state_machine.rb', line 281

def try_trigger(event_name)
  if valid_state?(event_name)
    yield
  else
    exception = InvalidStateError
    catch_error(exception) ||
      fail(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)


251
252
253
254
# File 'lib/finite_machine/state_machine.rb', line 251

def valid_state?(event_name)
  current_states = events_map.states_for(event_name)
  current_states.any? { |state| state == current || state == ANY_STATE }
end