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.1.0"

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

Yields:

  • (self)

    yields the machine for easy definition of states

Yield Parameters:

Raises:


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

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

Instance Attribute Details

#stateString (readonly)


64
65
66
# File 'lib/nanomachine.rb', line 64

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"}>}

72
73
74
# File 'lib/nanomachine.rb', line 72

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

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


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/nanomachine.rb', line 118

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

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

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

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

83
84
85
# File 'lib/nanomachine.rb', line 83

def transition(from, to)
  transitions[to_state(from)] = Set.new(to).map! { |state| to_state(state) }
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

149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/nanomachine.rb', line 149

def transition_to(other_state, *args, &block)
  if transition_to?(other_state)
    other_state = to_state(other_state)
    previous_state, @state = @state, other_state
    [[nil, nil], [previous_state, nil], [nil, other_state], [previous_state, other_state]].each do |combo|
      @callbacks[combo].each do |callback|
        callback.call([previous_state, other_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

Raises:


172
173
174
175
176
177
178
# File 'lib/nanomachine.rb', line 172

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

#transition_to?(other_state) ⇒ Boolean

Query to see if it's possible to transition to the given state.

Examples:

fsm.transition_to?("state") # => true

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

def transition_to?(other_state)
  transitions[state].include?(to_state(other_state))
end