Jsm

JSM is abbreviation of Just State Machine. The purpose is to simplify and increase the clarity of code related with state. JSM support validations before do transition. It help you to prevent unwanted transition. It also support integration with ActiveModel and ActiveRecord.

Installation

Add this line to your application's Gemfile:

gem 'jsm'

And then execute:

$ bundle

Or install it yourself as:

$ gem install jsm

Usage

State Machine Definition

To use JSM, a state machine class need to be created. Define your state machine here, such as:

  • state
  • event
  • transition
  • state validation
  • attribute(the client state value)
class UserStateMachine < Jsm::Base
  attribute_name :title

  state :beginner
  state :intermediate
  state :master

  validate :intermediate do |user|
      (20..50).include?(user.current_level)
  end

  validate :master do |user|
      user.current_level > 50
  end

  event :upgrade_title do
    transition from: [:beginner], to: :intermediate
    transition from: [:intermediate], to: :master
  end

  event :downgrade_title do
    transition from: :intermediate, to: :beginner
    transition from: :master, to: :intermediate
  end
end

Client

To use the state machine definition specify it as below

class User
  include Jsm::Client
  jsm_use UserStateMachine # your state machine class here

  attr_accessor :title # same with attribute_name in UserStateMachine
  #your code here
end

note: Client class should have instance variable same with attribute_name specified in state machine class.

This also provides you with a couple of public methods(based on event) for instances of the class User:

user = User.new
user.upgrade_title # run event confirm, return true/ false
user.upgrade_title! # run event confirm, raise error Jsm::IllegalTransitionError if failed
user.can_upgrade_title? # check if can run event successfully, return true/false

user.downgrade_title
user.downgrade_title!
user.can_downgrade_title?

Validation

This is useful, when you want to allow transition to a specified state allowed when it pass the validation. Validation should return true if passed validation and false if failed.

class UserStateMachine < Jsm::Base
# many codes here
  validate :intermediate do |user|
    (20..50).include?(user.current_level)
  end
# many codes here
end

Event

when an event is triggered, it run validate. If passed, then it run transition. In the event of having multiple transitions, the first transition that successfully completes will stop other transitions to be executed.

class UserStateMachine < Jsm::Base
  attribute_name :level

  state :beginner
  state :intermediate
  state :master

  event :upgrade_title do
    transition from: [:beginner], to: :intermediate
    transition from: [:intermediate], to: :master
  end

  event :downgrade_title do
    transition from: :intermediate, to: :beginner
    transition from: :master, to: :intermediate
  end
end

# Client Class
class User
  include Jsm::Client
  jsm_use UserStateMachine # your state machine class here

  attr_accessor :title # same with attribute_name in UserStateMachine
  def initialize
      @title = :beginner
      @level = 1
  end
  #your code here
end

user = User.new
user.title # :beginner
user.upgrade_title # true
user.title # :intermediate

Active Model Integration

class UserStateMachine < Jsm::Base
  attribute_name :title

  state :unconfirmed
  state :beginner
  state :intermediate
  state :master

  validate :intermediate do |user|
    unless (20..50).include?(user.current_level)
      user.errors.add(:title, 'is not between 20 and 50')
    end
  end

  validate :master do |user|
    unless user.current_level > 50
     user.errors.add(:title, 'have not reached 50')
    end
  end

  event :upgrade_title do
    transition from: [:beginner], to: :intermediate
    transition from: [:intermediate], to: :master
  end

  event :downgrade_title do
    transition from: :intermediate, to: :beginner
    transition from: :master, to: :intermediate
  end
end

# Client Class
class User
  include ActiveModel::Model
  include Jsm::Client
  include Jsm::Client::ActiveModel
  jsm_use UserStateMachine # your state machine class here

  attr_accessor :title # same with attribute_name in UserStateMachine
  attr_accessor :level
  def initialize
    @title = :beginner
    @level = 1
  end
  #your code here
end

Jsm support ActiveModel. In the client class include the Jsm::Client::ActiveModel. when run an event. It will auto saved the object.

Validation

It also support validation from ActiveModel . Validation checked based on errors value in the instance. you can add an error to the errors object. This will prevent the state from being changed

user = User.new
user.level # 1
user.level = 18
user.upgrade_title # false
user.errors[:title] # ["is not between 20 and 50"]

ActiveRecord Integration

class UserStateMachine < Jsm::Base
  attribute_name :title

  state :unconfirmed
  state :beginner
  state :intermediate
  state :master

  validate :intermediate do |user|
    unless (20..50).include?(user.current_level)
      user.errors.add(:title, 'is not between 20 and 50')
    end
  end

  validate :master do |user|
    unless user.current_level > 50
     errors.add(:title, 'have not reached 50')
    end
  end

  event :upgrade_title do
    transition from: [:beginner], to: :intermediate
    transition from: [:intermediate], to: :master
  end

  event :downgrade_title do
    transition from: :intermediate, to: :beginner
    transition from: :master, to: :intermediate
  end
end

# Client Class
class User
  include ActiveModel::Model
  include Jsm::Client
  include Jsm::Client::ActiveModel
  jsm_use UserStateMachine # your state machine class here

  attr_accessor :title # same with attribute_name in UserStateMachine
  attr_accessor :level
  def initialize
    @title = :beginner
    @level = 1
  end
  #your code here
end

Jsm support ActiveRecord. In the client class include the Jsm::Client::ActiveRecord. It also support ActiveRecord Validation. The behavior is same with ActiveModel client.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/wendy0402/jsm. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.