Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

tiny_state

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tiny_state

  • 0.2.0
  • Rubygems
  • Socket score

Version published
Maintainers
1
Created
Source

Tiny State

CI

[!WARNING] Tiny State is currently in early development. Use at your own risk!

Tiny State is a library to add a state machine to any Ruby class. It has a few design goals:

  • It is very small on purpose, with only a small DSL to define the states and events.
  • Each event is handled by a simple Ruby class, which only defines the transitions it allows and modifies the state's attribute. These can also just be executed as Plain Old Ruby Objects outside of the state machine's context.
  • No other logic (callbacks, guards, persistence, error handling) is supplied. If you need them you can implement it inside the event classes using regular Ruby code in any way you prefer.
  • This prevents your main class from being poluted with a lot of state machine related logic, methods, callbacks, conditions, etc. It only defines the state machine.

Getting Started

Tiny State is published on Rubygems, so just add it to your Gemfile:

gem "tiny_state"

Usage

This example implements a state machine with three states: new, published and rejected. It has two events: publish and reject. The publish event can only be triggered from the new and rejected states, and the reject event can only be triggered from the new state, so you can't reject a post that is already published.

class Post
  # include the TinyState module in your class.
  include TinyState

  # set up a state attribute, usually this will come from something like your  Rails model.
  attr_accessor :state

  def initialize(state: :initial)
    @state = state
  end

  # define the state machine with the `tiny_state` method.
  tiny_state do
    state :new # define one state at a time
    state [:approved, :published] # or multiple at once

    # define each event with the class that handles it.
    event :publish, PublishPost
    event :reject, RejectPost
  end
end

# define the event classes that handle the transitions, they need to inherit from `TinyState::Event`.
class PublishPost < TinyState::Event
  # define the transitions this event allows. It can transition from multiple states to a single state.
  transitions from: %i[new rejected], to: :published
end

class RejectPost < TinyState::Event
  transitions from: :new, to: :rejected
end

tiny_state takes a block and only defines the possible states and events. The state methods defines possible states, and the event method defines possible events. The event class is passed as a second argument.

Note that the state attribute itself is not defined by Tiny State. You need to define it yourself in your class or model. There is also no default value set by Tiny State.

This will then add the following event methods to a Post instance.

  • #publish? and #publish!
  • #reject? and #reject!

The #question_mark? methods will return true or false depending on whether the transition is allowed. These methods are also used internally to check if we can transition the state.

The #bang! methods will raise an exception if the transition is not allowed, if it is they will change the state attribute on the instance to the new value. This will not trigger any database update or other side effects, that is left up to you to implement.

Defining your states

You can define states with the state method, eiter individually or multiple states at once. States are deduplicated automatically.

class Post
  include TinyState

  tiny_state do
    state :new
    state [:approved, :rejected]
  end
end

You can define multiple state machines on a single class, each with their own attribute. If you redefine a state machine, the previous one is overwritten.

Defining your events

You define events with the event method. Each event is defined with a class that handles that event.

class Post
  include TinyState

  tiny_state do
    # ...
    event :publish, PublishPost
  end
end

This class should inherit from TinyState::Event. The Event class takes one configuration that defines which transitions it allows. This is checked before transitioning the state. If the transition is not allowed, an TinyState::InvalidTransitionError exception is raised.

You can define multiple transitions with the transitions method. This takes a from and to keyword argument. The from argument can be a single state or an array of states, and the to argument is the (single) state the event transitions to.

class PublishPost < TinyState::Event
  transitions from: [:new, :rejected], to: :published
end

Exposed state machines

To be able to peek inside the state machines, the #tiny_state_machines and #tiny_state_machine methods are added to the instance of your class.

#tiny_state_machines returns a hash with the field as the key and a TinyState::Machine instance as the value, which contains the defined states and events.

As a shortcut the singular #tiny_state_machine is also defined, which returns the machine for the first state machine you define or the machine for a specific field if you give it an argument.

post = Post.new # from the example above

post.tiny_state_machine.states
# => #<TinyState::StateMachine:0x00000000000ff0 :state, states: [:initial, :approved, :rejected], events: [:approve, :reject]>

post.tiny_state_machine(:state)
# => #<TinyState::StateMachine:0x00000000000ff0 :state, states: [:initial, :approved, :rejected], events: [:approve, :reject]>

The TinyState::StateMachine instance has the following methods:

  • #attribute returns the attribute the state machine is defined on.
  • #states returns an array of the defined states.
  • #events returns a hash with the event names as keys and the event classes as values.
post = Post.new # from the example above

post.tiny_state_machine.states
# => [:new, :published, :rejected]

post.tiny_state_machine.events
# => [:publish, :reject]

post.tiny_state_machine.attribute
# => :state

Using a different field for state

By default Tiny State uses the state field on your resource. If you want to use a different field, you can supply that as an option when defining the state machine:

class Post
  # ...

  tiny_state :status do
    # ...
  end
end

Adding extra transition logic

If you want to extend the logic that allows a transition, you can do so by overriding the #transition? method in the event class. Be sure to always call super to ensure the transition state checks defined in the event itself are also performed.

class PublishPost < TinyState::Event
  # ...

  def transition?
    # Add your own logic here, for example:
    super && some_other_condition?
  end

  private

  def some_other_condition?
    # ...
  end
end

Implementing side effects

If you want to implement side effects before or after a transition, you can do so by overriding the #transition! method in the event class. Be sure to always call super to ensure the transition is allowed and the state is changed.

class PublishPost < TinyState::Event
  # ...

  def transition!
    super
    # Add your own logic here, for example:
    log_publication!
  end

  private

  def log_publication!
    # ...
  end
end

If you don't call super in your transition! method, it won't check if the transition is allowed and won't change the state attribute. If you are fine with that, you can still call change_state! to change the state at the moment you want.

Note: if you plan to have any asynchronous side effects like sending emails or queueing jobs, you should make sure they are fired after the resource is saved and the transaction is committed. This is because the state change is not persisted until the transaction is committed. I recommend to use the after_commit_everywhere gem for this.

Using the object or resource itself

The object itself is always referenceable through resource. This can be used if you want to access the object in the event class.

class PublishPost < TinyState::Event
  # ...

  def transition!
    # ...
    resource.published_at = Time.now
    # ...
  end
end

If you want to use a different name for the resource, for example to match the type of resource you pass in, you can alias the method or define a simple one line method that refers to resource.

class PublishPost < TinyState::Event
  # ...

  alias_method :post, :resource
  # or
  def post = resource
end

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/djfpaagman/tiny_state.

FAQs

Package last updated on 30 May 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc