SolidState

Minuscule but solid state machine for Ruby classes. The only dependency is that your model responds to a getter and setter for state.

Installation

In your Gemfile:

gem 'solidstate'

Usage


  # simplest example, using just an accessor
  class Post
    include SolidState

    attr_accessor :state
    states :draft, :published
  end

  # in its simplest form you just declare the possible states.
  # if it's a simple class you just get boolean methods for checking
  # whether the current status is X or Y.

  p = Post.new
  p.state      # => nil 
  p.state = 'draft'
  p.draft?     # true
  p.published? # => false
  p.state = 'published'
  p.published? # => true

  # you also have access to the list of possible states at Class.states, 
  # in case you need to enumerate them.
  # for instance, to populate a select field's options, you'd do something like:

  options = Post.states.map { |st| "<option value='#{st}'>#{st}</option>" }.join("\n")

  # now, if the model class responds to validates_inclusion_of, it will
  # mark the record invalid if an unknown state is set.

  # let's assume this is actually an ActiveRecord class, and the
  # table contains a column named 'state'.

  class Post < ActiveRecord::Base
    include SolidState

    states :draft, :published
  end

  p = Post.new
  p.state = 'published'
  p.valid?  # => true
  p.state = 'deleted'
  p.valid?  # => false

  # you also get scopes for free if the class responds_to the `scope` method

  Post.published.first # => #<Post ...>
  Post.draft.count # => 1

  # ok, now let's gets get fancier. we're going to declare transitions
  # which will govern the possible directions in which an object's state
  # can move to.

  class Subscriber < ActiveRecord::Base
    include SolidState

    states :inactive, :active, :unsubscribed, :disabled do
      transitions from: :inactive, to: :active
      transitions from: :active, to: [:unsubscribed, :disabled]
      transitions from: :unsubscribed, to: :active
    end
  end

  s = Subscriber.new
  s.state # => 'inactive'

  # since we declared transitions, we can now call #{state}! which
  # checks whether the instance can transition to that state and
  # if so, sets the new state and optionally saves the record.

  s.active! # => true
  s.state   # => 'active'
  s.inactive! # => raises InvalidTransitionError

  # this also works outside transition methods, of course.

  s.reload  # => true
  s.active? # => true

  s.state = 'inactive'
  s.valid? # => false

  # the last trick this library does is that it optionally lets you
  # declare callback methods that are called whenever a transition
  # method succeeds. just define a method called #once_[state] in
  # your model.

  class Subscriber
    def once_unsubscribed
      puts "Sorry to see you go!"
    end
  end

  # ...
  s.unsubscribed! # => prints "Sorry to see you go!"

That's about it. For examples check the examples directory in this repo.

Contributions

You're more than welcome. Send a pull request, including tests, and make sure you don't break anything. That's it.

Author

Tomás Pollak

Copyright

(c) Fork Limited. MIT license.