Class: Nanomachine

Inherits:
Object
  • Object
show all
Defined in:
lib/nanomachine.rb,
lib/nanomachine/version.rb

Overview

A minimal state machine where you transition between states, instead of transition by input symbols or events.

Examples:

state_machine = Nanomachine.new("unpublished") do |fsm|
  fsm.transition("published", %w[unpublished processing removed])
  fsm.transition("unpublished", %w[published processing removed])
  fsm.transition("processing", %w[published unpublished])
  fsm.transition("removed", []) # defined for being explicit

  fsm.on_transition do |(from_state, to_state)|
    update_column(:state, to_state)
  end
end

if state_machine.transition_to("published")
  puts "Publish success!"
else
  puts "Publish failure! We’re in #{state_machine.state}."
end

Constant Summary collapse

InvalidTransitionError =

Raised when a transition cannot be performed.

Class.new(StandardError)
InvalidStateError =

Raised when a given state cannot be accepted.

Class.new(StandardError)
VERSION =

See Also:

"1.0.1"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(initial_state) {|self| ... } ⇒ Nanomachine

Construct a Nanomachine with an initial state.

Examples:

initialization with a block

machine = Nanomachine.new("initial") do |fsm|
  fsm.transition("initial", %w[green orange])
  fsm.transition("green", %w[orange error])
  fsm.transition("orange", %w[green error])
  # error is a dead state, no transition out of it
  # so not necessary to define the transitions for it

  fsm.on_transition(to: "error") do |(from_state, to_state), message|
    notifier.notify_error(message)
  end

  fsm.on_transition do |(from_state, to_state)|
    object.update_state(to_state)
  end
end

Parameters:

  • initial_state (#to_s)

    state the machine is in after initialization

Yields:

  • (self)

    yields the machine for easy definition of states

Yield Parameters:

Raises:



56
57
58
59
60
61
62
63
64
65
# File 'lib/nanomachine.rb', line 56

def initialize(initial_state)
  if initial_state.nil?
    raise InvalidStateError, "initial state cannot be nil"
  end

  @state = initial_state.to_s
  @transitions = Hash.new(Set.new)
  @callbacks = Hash.new { |h, k| h[k] = [] }
  yield self if block_given?
end

Instance Attribute Details

#stateString (readonly)

Returns current state of the state machine.

Returns:

  • (String)

    current state of the state machine.



68
69
70
# File 'lib/nanomachine.rb', line 68

def state
  @state
end

#transitionsHash<String, Set> (readonly)

Returns mapping of state to possible transition targets.

Examples:

{"initial"=>#<Set: {"green", "orange"}>,
 "green"=>#<Set: {"orange", "error"}>,
 "orange"=>#<Set: {"green", "error"}>}

Returns:

  • (Hash<String, Set>)

    mapping of state to possible transition targets



76
77
78
# File 'lib/nanomachine.rb', line 76

def transitions
  @transitions
end

Instance Method Details

#on_transition(options = {}) {|transition, *args, &block| ... } ⇒ Object

Define a callback to be executed on transition.

Examples:

callback executed on any transition

fsm.on_transition do |(from_state, to_state), *args, &block|
  # executed on any transition
end

callback executed on transition from a given state only

fsm.on_transition(from: "green") do |(from_state, to_state), *args, &block|
  # executed only on transitions *from* green state
end

callback executed on transition to a given state only

fsm.on_transition(to: "green") do |(from_state, to_state), *args, &block|
  # executed only on transitions *to* green state
end

callback executed on transition between two states only

fsm.on_transition(from: "green", to: "red") do |(from_state, to_state), *args, &block|
  # executed only on transitions between green and red
end

Parameters:

  • options (Hash) (defaults to: {})

    constraint on when callback is to be executed

Options Hash (options):

  • :from (#to_s, nil) — default: nil

    only match when transitioning from the given state, nil for any

  • :to (#to_s, nil) — default: nil

    only match when transitioning to the given state, nil for any

Yields:

Yield Parameters:

Raises:

  • (ArgumentError)

    when given unknown options

  • (LocalJumpError)

    when no callback block is supplied



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/nanomachine.rb', line 122

def on_transition(options = {}, &block)
  unless block_given?
    raise LocalJumpError, "no block given"
  end

  from = options.delete(:from)
  from &&= from.to_s

  to = options.delete(:to)
  to &&= to.to_s

  unless options.empty?
    raise ArgumentError, "unknown options: #{options.keys.join(", ")}"
  end

  @callbacks[[from, to]] << block
end

#transition(from, to) ⇒ Object

Define possible state transitions from the source state.

Examples:

fsm.transition("green", %w[orange red])
fsm.transition("orange", %w[red])
fsm.transition(:error, [:nowhere])

Parameters:

  • from (#to_s)
  • to (#each)

    each target state must respond to #to_s



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

def transition(from, to)
  transitions[from.to_s] = Set.new(to).map!(&:to_s)
end

#transition_to(other_state, *args, &block) ⇒ String, false

Transition the state machine from the current state to a target state.

Examples:

transition to error state with a message given to any callbacks

if previous_state = fsm.transition_to("error", "something went really wrong")
  puts "Transition from #{previous_state} to #{fsm.state} successful!"
else
  puts "Transition failed."
end

Parameters:

  • other_state (#to_s)

    new state to transition to

  • args

    any number of arguments, passed to callbacks defined with #on_transition

  • block

    passed to callbacks defined with #on_transition

Returns:

  • (String, false)

    state the machine was in before transition, or false if transition is not allowed



153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/nanomachine.rb', line 153

def transition_to(other_state, *args, &block)
  other_state &&= other_state.to_s
  if transitions[state].include?(other_state)
    previous_state, @state = @state, other_state
    [[nil, nil], [previous_state, nil], [nil, @state], [previous_state, @state]].each do |combo|
      @callbacks[combo].each do |callback|
        callback.call([previous_state, @state], *args, &block)
      end
    end
    previous_state
  else
    false
  end
end

#transition_to!(other_state) ⇒ String

Same as #transition_to, but raises an error if the transition is not allowed.

Examples:

fsm.transition_to!("bogus state") # => InvalidTransitionError

Parameters:

  • other_state (#to_s)

    new state to transition to

  • args

    any number of arguments, passed to callbacks defined with #on_transition

  • block

    passed to callbacks defined with #on_transition

Returns:

  • (String)

    the state the state machine was in before transition

Raises:



176
177
178
179
180
181
182
# File 'lib/nanomachine.rb', line 176

def transition_to!(other_state)
  if previous_state = transition_to(other_state)
    previous_state
  else
    raise InvalidTransitionError, "cannot transition from #{state.inspect} to #{other_state.inspect}"
  end
end