class FiniteMachine::StateMachine
Base class for state machine
Public Class Methods
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
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
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
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
Get current state
@return [String]
@api public
# File lib/finite_machine/state_machine.rb, line 146 def current sync_shared { state } end
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
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
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 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 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
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 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
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
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
# 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
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
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 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 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
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
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
Forward the message to observer or self
@param [String] method_name
@param [Array] args
@return [self]
@api private
# 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 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
Test if a message can be handled by state machine
@param [String] method_name
@param [Boolean] include_private
@return [Boolean]
@api private
# 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