{"id":20481161,"url":"https://github.com/wendy0402/jsm","last_synced_at":"2025-04-13T14:10:57.752Z","repository":{"id":56879466,"uuid":"44398217","full_name":"wendy0402/jsm","owner":"wendy0402","description":"Just State Machine","archived":false,"fork":false,"pushed_at":"2018-06-14T05:53:28.000Z","size":106,"stargazers_count":3,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-27T05:12:11.634Z","etag":null,"topics":["just-state-machine","ruby","state-diagram","state-machine"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wendy0402.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-10-16T16:44:20.000Z","updated_at":"2019-07-09T18:23:36.000Z","dependencies_parsed_at":"2022-08-20T11:40:37.381Z","dependency_job_id":null,"html_url":"https://github.com/wendy0402/jsm","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wendy0402%2Fjsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wendy0402%2Fjsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wendy0402%2Fjsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wendy0402%2Fjsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wendy0402","download_url":"https://codeload.github.com/wendy0402/jsm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248724629,"owners_count":21151561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["just-state-machine","ruby","state-diagram","state-machine"],"created_at":"2024-11-15T16:07:04.797Z","updated_at":"2025-04-13T14:10:57.724Z","avatar_url":"https://github.com/wendy0402.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Gem Version](https://badge.fury.io/rb/just_state_machine.svg)](https://badge.fury.io/rb/just_state_machine)\n[![Code Climate](https://codeclimate.com/github/wendy0402/jsm/badges/gpa.svg)](https://codeclimate.com/github/wendy0402/jsm)\n# Jsm\n\nJSM 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`.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'just_state_machine'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install just_state_machine\n\n## Usage\n\n### State Machine Definition\nTo use `JSM`, a state machine class need to be created. Define your state machine here, such as:\n* state\n* event\n* transition\n* state validation\n* attribute(the client state value)\n* callback\n\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :title\n\n  state :beginner\n  state :intermediate\n  state :master\n\n  validate :intermediate do |user|\n\t  (20..50).include?(user.current_level)\n  end\n\n  validate :master do |user|\n\t  user.current_level \u003e 50\n  end\n\n  event :upgrade_title do\n    transition from: [:beginner], to: :intermediate\n    transition from: [:intermediate], to: :master\n  end\n\n  event :downgrade_title do\n    transition from: :intermediate, to: :beginner\n    transition from: :master, to: :intermediate\n  end\nend\n```\n\n### Client\nTo use the state machine definition specify it as below\n```ruby\nclass User\n  include Jsm::Client\n  jsm_use UserStateMachine # your state machine class here\n\n  attr_accessor :title # same with attribute_name in UserStateMachine\n  #your code here\nend\n```\n**note**: Client class should have instance variable same with `attribute_name` specified in state machine class.\n\nThis also provides you with a couple of public methods(based on event) for instances of the class `User`:\n```ruby\nuser = User.new\nuser.upgrade_title # run event confirm, return true/ false\nuser.upgrade_title! # run event confirm, raise error Jsm::IllegalTransitionError if failed\nuser.can_upgrade_title? # check if can run event successfully, return true/false\n\nuser.downgrade_title\nuser.downgrade_title!\nuser.can_downgrade_title?\n```\n### State\nDefine your state **before** define others(validation, event, etc). It is to prevent you define transition, validation for unwanted state.\nYou 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.\n\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :title\n\n  state :beginner, initial: true\n  state :intermediate\n  state :master\n# more code here\nend\n\nclass User\n  include Jsm::Client\n  jsm_use UserStateMachine\n\n  attr_accessor :title\n  #your code here\n\n  def initialize(title = nil)\n    @title = title\n  end\nend\n\nuser = User.new\nuser.current_state # :beginner\n\nuser = User.new(:intermediate)\nuser.current_state # :intermediate\n```\n### Validation\nThis 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.\n**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\n``` ruby\nclass UserStateMachine \u003c Jsm::Base\n# many codes here\n\n  state :intermediate\n  validate :intermediate do |user|\n    (20..50).include?(user.current_level)\n  end\n# many codes here\nend\n```\n\n### Event\nwhen 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.\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :level\n\n  state :beginner\n  state :intermediate\n  state :master\n\n  event :upgrade_title do\n    transition from: [:beginner], to: :intermediate\n    transition from: [:intermediate], to: :master\n  end\n\n  event :downgrade_title do\n    transition from: :intermediate, to: :beginner\n    transition from: :master, to: :intermediate\n  end\n\n\n  before :upgrade_title do |user|\n    user.name = 'before'\n  end\n\n  after :upgrade_title do |result, user|\n    if result\n      user.name += ' after success'\n    else\n      user.name += 'after failed'\n    end\n  end\nend\n\n# Client Class\nclass User\n  include Jsm::Client\n  jsm_use UserStateMachine # your state machine class here\n\n  attr_accessor :title, :name # same with attribute_name in UserStateMachine\n  def initialize\n\t  @title = :beginner\n\t  @level = 1\n  end\n  #your code here\nend\n\nuser = User.new\nuser.title # :beginner\nuser.upgrade_title # true\nuser.title # :intermediate\n```\n### Transition\nTransition will transitioning your state from one state to other state. Params `from` in transition can receive one or multiple states.\nHowever to put multiple states, you need to put it in array.\n\n```ruby\ntransition from: :intermediate, to: :beginner #one `from` states\ntransition from: [:intermediate, :master], to: :beginner #multiple `from` states\n```\nWhat above code means for multiple `from` states is the current state either `:intermediate` or `:master`, transform it into `:beginner` state\n\n### Callbacks\n`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\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :level\n\n  state :beginner\n  state :intermediate\n  state :master\n\n  event :upgrade_title do\n    transition from: [:beginner], to: :intermediate\n    transition from: [:intermediate], to: :master\n  end\n\n  event :downgrade_title do\n    transition from: :intermediate, to: :beginner\n    transition from: :master, to: :intermediate\n  end\n\n  before :upgrade_title do |user|\n    user.name = 'before'\n  end\n\n  after :upgrade_title do |result, user| # the first parameters is result of the event\n    if result\n      # execute if transition in event is success\n      user.name += ' after success'\n    else\n      # execute if transition in event is failed\n      user.name += 'after failed'\n    end\n  end\nend\n\n# Client Class\nclass User\n  include Jsm::Client\n  jsm_use UserStateMachine # your state machine class here\n\n  attr_accessor :title, :name # same with attribute_name in UserStateMachine\n  def initialize\n\t  @title = :beginner\n\t  @level = 1\n  end\n  #your code here\nend\n\nuser = User.new\nuser.title # :beginner\nuser.name # nil\nuser.upgrade_title # true(it still return the original return value of the event)\nuser.name # before after success\n```\n\n## Active Model Integration\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :title\n\n  state :unconfirmed\n  state :beginner\n  state :intermediate\n  state :master\n\n  validate :intermediate do |user|\n    unless (20..50).include?(user.current_level)\n      user.errors.add(:title, 'is not between 20 and 50')\n    end\n  end\n\n  validate :master do |user|\n    unless user.current_level \u003e 50\n     user.errors.add(:title, 'have not reached 50')\n    end\n  end\n\n  event :upgrade_title do\n    transition from: [:beginner], to: :intermediate\n    transition from: [:intermediate], to: :master\n  end\n\n  event :downgrade_title do\n    transition from: :intermediate, to: :beginner\n    transition from: :master, to: :intermediate\n  end\nend\n\n# Client Class\nclass User\n  include ActiveModel::Model\n  include Jsm::Client\n  include Jsm::Client::ActiveModel\n  jsm_use UserStateMachine # your state machine class here\n\n  attr_accessor :title # same with attribute_name in UserStateMachine\n  attr_accessor :level\n  def initialize\n\t@title = :beginner\n\t@level = 1\n  end\n  #your code here\nend\n```\n\n`Jsm`  support `ActiveModel`. In the `client` class include the `Jsm::Client::ActiveModel`.  when run an event. It will auto saved the object.\n\n### Validation\nIt 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\n```ruby\nuser = User.new\nuser.level # 1\nuser.level = 18\nuser.upgrade_title # false\nuser.errors[:title] # [\"is not between 20 and 50\"]\n```\n## ActiveRecord Integration\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :title\n\n  state :beginner\n  state :intermediate\n  state :master\n\n  validate :intermediate do |user|\n    unless (20..50).include?(user.current_level)\n      user.errors.add(:title, 'is not between 20 and 50')\n    end\n  end\n\n  validate :master do |user|\n    unless user.current_level \u003e 50\n     user.errors.add(:title, 'have not reached 50')\n    end\n  end\n\n  event :upgrade_title do\n    transition from: [:beginner], to: :intermediate\n    transition from: [:intermediate], to: :master\n  end\n\n  event :downgrade_title do\n    transition from: :intermediate, to: :beginner\n    transition from: :master, to: :intermediate\n  end\nend\n\n# Client Class\nclass User \u003c ActiveRecord::Base\n  include Jsm::Client\n  include Jsm::Client::ActiveRecord\n  jsm_use UserStateMachine # your state machine class here\n\n  # attributes -\u003e title, level\n  #your code here\nend\n```\n`Jsm`  support `ActiveRecord`. In the `client` class include the `Jsm::Client::ActiveRecord`.  It also support `ActiveRecord` Validation. The behavior is same with `ActiveModel` client.\n\n\n## Mongoid Integration\n\nMongoid is same with active record. you just need to change the client from `Jsm::Client::ActiveRecord` to `Jsm::Client::Mongoid`\nexample:\n```ruby\nclass UserMongoid\n  include Mongoid::Document\n  include Jsm::Client\n  include Jsm::Client::Mongoid\n  jsm_use UserStateMachine # your state machine class here\n\n  field :title, type: String # same with attribute_name in UserStateMachine\n  field :level, type: String\n  #your code here\nend\n```\n\n## StateMachine Visualization\nWhen 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.\n\n```ruby\nclass UserStateMachine \u003c Jsm::Base\n  attribute_name :relationship\n\n  state :single, initial: true\n  state :in_relationship\n  state :married\n  state :divorced\n  state :widowed\n\n  event :start_dating do\n    transition from: :single, to: :in_relationship\n  end\n\n  event :marry do\n    transition from: [:single, :in_relationship], to: :married\n  end\n\n  event :cheating do\n    transition from: :in_relationship, to: :single\n    transition from: :married, to: :divorced\n  end\n\n  event :divorce do\n    transition from: :married, to: :divorced\n  end\nend\n\nJsm::Drawer::Graphviz.generate_url(UserStateMachine) #the arguments is class name\n```\nthe result will be:\n\n![image](https://chart.googleapis.com/chart?cht=gv\u0026chl=digraph{single-%3Ein_relationship[label=start_dating];single-%3Emarried[label=marry];in_relationship-%3Emarried[label=marry];in_relationship-%3Esingle[label=cheating];married-%3Edivorced[label=cheating];married-%3Edivorced[label=divorce]})\n\n[source]( https://chart.googleapis.com/chart?cht=gv\u0026chl=digraph{single-%3Ein_relationship[label=start_dating];single-%3Emarried[label=marry];in_relationship-%3Emarried[label=marry];in_relationship-%3Esingle[label=cheating];married-%3Edivorced[label=cheating];married-%3Edivorced[label=divorce]}Â)\n## Development\n\nAfter 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.\n\n## Contributing\n\nBug 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](contributor-covenant.org) code of conduct.\n\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwendy0402%2Fjsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwendy0402%2Fjsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwendy0402%2Fjsm/lists"}