Project

jsm

0.0
No commit activity in last 3 years
No release in over 3 years
Just StateMachine
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.10
>= 0
~> 10.0
>= 0
 Project Readme

Gem Version Code Climate

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 'just_state_machine'

And then execute:

$ bundle

Or install it yourself as:

$ gem install just_state_machine

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)
  • callback
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?

State

Define your state before define others(validation, event, etc). It is to prevent you define transition, validation for unwanted state. You can also define the initial state. Initial State is state value that is given when you don't set any value to state attribute in the instance on initialization. Initial State is optional.

class UserStateMachine < Jsm::Base
  attribute_name :title

  state :beginner, initial: true
  state :intermediate
  state :master
# more code here
end

class User
  include Jsm::Client
  jsm_use UserStateMachine

  attr_accessor :title
  #your code here

  def initialize(title = nil)
    @title = title
  end
end

user = User.new
user.current_state # :beginner

user = User.new(:intermediate)
user.current_state # :intermediate

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. note: Dont forget to define the state first, because if not then Jsm will raise error Jsm::InvalidStateError. This is to prevent typo when add new validation

class UserStateMachine < Jsm::Base
# many codes here

  state :intermediate
  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


  before :upgrade_title do |user|
    user.name = 'before'
  end

  after :upgrade_title do |result, user|
    if result
      user.name += ' after success'
    else
      user.name += 'after failed'
    end
  end
end

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

  attr_accessor :title, :name # 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

Transition

Transition will transitioning your state from one state to other state. Params from in transition can receive one or multiple states. However to put multiple states, you need to put it in array.

transition from: :intermediate, to: :beginner #one `from` states
transition from: [:intermediate, :master], to: :beginner #multiple `from` states

What above code means for multiple from states is the current state either :intermediate or :master, transform it into :beginner state

Callbacks

Jsm now support callbacks. It provide 2 API: before and after. basically before callbacks is run before do event. Meanwhile, after callbacks is run after event. For note, after event first argument is result of the event

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

  before :upgrade_title do |user|
    user.name = 'before'
  end

  after :upgrade_title do |result, user| # the first parameters is result of the event
    if result
      # execute if transition in event is success
      user.name += ' after success'
    else
      # execute if transition in event is failed
      user.name += 'after failed'
    end
  end
end

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

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

user = User.new
user.title # :beginner
user.name # nil
user.upgrade_title # true(it still return the original return value of the event)
user.name # before after success

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 :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 < ActiveRecord::Base
  include Jsm::Client
  include Jsm::Client::ActiveRecord
  jsm_use UserStateMachine # your state machine class here

  # attributes -> title, level
  #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.

Mongoid Integration

Mongoid is same with active record. you just need to change the client from Jsm::Client::ActiveRecord to Jsm::Client::Mongoid example:

class UserMongoid
  include Mongoid::Document
  include Jsm::Client
  include Jsm::Client::Mongoid
  jsm_use UserStateMachine # your state machine class here

  field :title, type: String # same with attribute_name in UserStateMachine
  field :level, type: String
  #your code here
end

StateMachine Visualization

When you want to visualize the state machine that has been defined. You can use the Jsm::Drawer:Graphviz. Basically it will generate an url for images of states diagram with theirs events. Basically the image is generated by google chart. They provide a really good API to generate state diagram.

class UserStateMachine < Jsm::Base
  attribute_name :relationship

  state :single, initial: true
  state :in_relationship
  state :married
  state :divorced
  state :widowed

  event :start_dating do
    transition from: :single, to: :in_relationship
  end

  event :marry do
    transition from: [:single, :in_relationship], to: :married
  end

  event :cheating do
    transition from: :in_relationship, to: :single
    transition from: :married, to: :divorced
  end

  event :divorce do
    transition from: :married, to: :divorced
  end
end

Jsm::Drawer::Graphviz.generate_url(UserStateMachine) #the arguments is class name

the result will be:

image

source

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.

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.