{"id":13484595,"url":"https://github.com/state-machines/state_machines","last_synced_at":"2025-05-13T15:12:34.380Z","repository":{"id":16467117,"uuid":"19219215","full_name":"state-machines/state_machines","owner":"state-machines","description":"Adds support for creating state machines for attributes on any Ruby class","archived":false,"fork":false,"pushed_at":"2025-04-14T22:21:21.000Z","size":550,"stargazers_count":827,"open_issues_count":6,"forks_count":95,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-05-08T10:01:53.338Z","etag":null,"topics":["ruby","state-machine"],"latest_commit_sha":null,"homepage":"https://github.com/state-machines/state_machines","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/state-machines.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2014-04-27T22:05:49.000Z","updated_at":"2025-05-07T02:04:31.000Z","dependencies_parsed_at":"2024-06-18T11:20:29.732Z","dependency_job_id":"f18c5d1d-ec5c-403c-805a-1a22e1ee9cdc","html_url":"https://github.com/state-machines/state_machines","commit_stats":{"total_commits":83,"total_committers":28,"mean_commits":"2.9642857142857144","dds":0.5662650602409638,"last_synced_commit":"e9f212ab0342494d2741008603be344d21c462ef"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/state-machines%2Fstate_machines","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/state-machines%2Fstate_machines/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/state-machines%2Fstate_machines/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/state-machines%2Fstate_machines/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/state-machines","download_url":"https://codeload.github.com/state-machines/state_machines/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253233939,"owners_count":21875524,"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":["ruby","state-machine"],"created_at":"2024-07-31T17:01:26.822Z","updated_at":"2025-05-13T15:12:34.333Z","avatar_url":"https://github.com/state-machines.png","language":"Ruby","funding_links":[],"categories":["Ruby","State Machines","Libraries"],"sub_categories":["Ruby"],"readme":"![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg)\n[![Code Climate](https://codeclimate.com/github/state-machines/state_machines.svg)](https://codeclimate.com/github/state-machines/state_machines)\n# State Machines\n\nState Machines adds support for creating state machines for attributes on any Ruby class.\n\n*Please note that multiple integrations are available for [Active Model](https://github.com/state-machines/state_machines-activemodel), [Active Record](https://github.com/state-machines/state_machines-activerecord), [Mongoid](https://github.com/state-machines/state_machines-mongoid) and more in the [State Machines organisation](https://github.com/state-machines).*  If you want to save state in your database, **you need one of these additional integrations**.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'state_machines'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install state_machines\n\n## Usage\n\n### Example\n\nBelow is an example of many of the features offered by this plugin, including:\n\n* Initial states\n* Namespaced states\n* Transition callbacks\n* Conditional transitions\n* State-driven instance behavior\n* Customized state values\n* Parallel events\n* Path analysis\n\nClass definition:\n\n```ruby\nclass Vehicle\n  attr_accessor :seatbelt_on, :time_used, :auto_shop_busy\n\n  state_machine :state, initial: :parked do\n    before_transition parked: any - :parked, do: :put_on_seatbelt\n    \n    after_transition on: :crash, do: :tow\n    after_transition on: :repair, do: :fix\n    after_transition any =\u003e :parked do |vehicle, transition|\n      vehicle.seatbelt_on = false\n    end\n\n    after_failure on: :ignite, do: :log_start_failure\n\n    around_transition do |vehicle, transition, block|\n      start = Time.now\n      block.call\n      vehicle.time_used += Time.now - start\n    end\n\n    event :park do\n      transition [:idling, :first_gear] =\u003e :parked\n    end\n\n    event :ignite do\n      transition stalled: same, parked: :idling\n    end\n\n    event :idle do\n      transition first_gear: :idling\n    end\n\n    event :shift_up do\n      transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear\n    end\n\n    event :shift_down do\n      transition third_gear: :second_gear, second_gear: :first_gear\n    end\n\n    event :crash do\n      transition all - [:parked, :stalled] =\u003e :stalled, if: -\u003e(vehicle) {!vehicle.passed_inspection?}\n    end\n\n    event :repair do\n      # The first transition that matches the state and passes its conditions\n      # will be used\n      transition stalled: :parked, unless: :auto_shop_busy\n      transition stalled: same\n    end\n\n    state :parked do\n      def speed\n        0\n      end\n    end\n\n    state :idling, :first_gear do\n      def speed\n        10\n      end\n    end\n\n    state all - [:parked, :stalled, :idling] do\n      def moving?\n        true\n      end\n    end\n\n    state :parked, :stalled, :idling do\n      def moving?\n        false\n      end\n    end\n  end\n\n  state_machine :alarm_state, initial: :active, namespace: :'alarm' do\n    event :enable do\n      transition all =\u003e :active\n    end\n\n    event :disable do\n      transition all =\u003e :off\n    end\n\n    state :active, :value =\u003e 1\n    state :off, :value =\u003e 0\n  end\n\n  def initialize\n    @seatbelt_on = false\n    @time_used = 0\n    @auto_shop_busy = true\n    super() # NOTE: This *must* be called, otherwise states won't get initialized\n  end\n\n  def put_on_seatbelt\n    @seatbelt_on = true\n  end\n\n  def passed_inspection?\n    false\n  end\n\n  def tow\n    # tow the vehicle\n  end\n\n  def fix\n    # get the vehicle fixed by a mechanic\n  end\n\n  def log_start_failure\n    # log a failed attempt to start the vehicle\n  end\nend\n```\n\n**Note** the comment made on the `initialize` method in the class.  In order for\nstate machine attributes to be properly initialized, `super()` must be called.\nSee `StateMachines:MacroMethods` for more information about this.\n\nUsing the above class as an example, you can interact with the state machine\nlike so:\n\n```ruby\nvehicle = Vehicle.new           # =\u003e #\u003cVehicle:0xb7cf4eac @state=\"parked\", @seatbelt_on=false\u003e\nvehicle.state                   # =\u003e \"parked\"\nvehicle.state_name              # =\u003e :parked\nvehicle.human_state_name        # =\u003e \"parked\"\nvehicle.parked?                 # =\u003e true\nvehicle.can_ignite?             # =\u003e true\nvehicle.ignite_transition       # =\u003e #\u003cStateMachines:Transition attribute=:state event=:ignite from=\"parked\" from_name=:parked to=\"idling\" to_name=:idling\u003e\nvehicle.state_events            # =\u003e [:ignite]\nvehicle.state_transitions       # =\u003e [#\u003cStateMachines:Transition attribute=:state event=:ignite from=\"parked\" from_name=:parked to=\"idling\" to_name=:idling\u003e]\nvehicle.speed                   # =\u003e 0\nvehicle.moving?                 # =\u003e false\n\nvehicle.ignite                  # =\u003e true\nvehicle.parked?                 # =\u003e false\nvehicle.idling?                 # =\u003e true\nvehicle.speed                   # =\u003e 10\nvehicle                         # =\u003e #\u003cVehicle:0xb7cf4eac @state=\"idling\", @seatbelt_on=true\u003e\n\nvehicle.shift_up                # =\u003e true\nvehicle.speed                   # =\u003e 10\nvehicle.moving?                 # =\u003e true\nvehicle                         # =\u003e #\u003cVehicle:0xb7cf4eac @state=\"first_gear\", @seatbelt_on=true\u003e\n\n# A generic event helper is available to fire without going through the event's instance method\nvehicle.fire_state_event(:shift_up) # =\u003e true\n\n# Call state-driven behavior that's undefined for the state raises a NoMethodError\nvehicle.speed                   # =\u003e NoMethodError: super: no superclass method `speed' for #\u003cVehicle:0xb7cf4eac\u003e\nvehicle                         # =\u003e #\u003cVehicle:0xb7cf4eac @state=\"second_gear\", @seatbelt_on=true\u003e\n\n# The bang (!) operator can raise exceptions if the event fails\nvehicle.park!                   # =\u003e StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear\n\n# Generic state predicates can raise exceptions if the value does not exist\nvehicle.state?(:parked)         # =\u003e false\nvehicle.state?(:invalid)        # =\u003e IndexError: :invalid is an invalid name\n\n# Namespaced machines have uniquely-generated methods\nvehicle.alarm_state             # =\u003e 1\nvehicle.alarm_state_name        # =\u003e :active\n\nvehicle.can_disable_alarm?      # =\u003e true\nvehicle.disable_alarm           # =\u003e true\nvehicle.alarm_state             # =\u003e 0\nvehicle.alarm_state_name        # =\u003e :off\nvehicle.can_enable_alarm?       # =\u003e true\n\nvehicle.alarm_off?              # =\u003e true\nvehicle.alarm_active?           # =\u003e false\n\n# Events can be fired in parallel\nvehicle.fire_events(:shift_down, :enable_alarm) # =\u003e true\nvehicle.state_name                              # =\u003e :first_gear\nvehicle.alarm_state_name                        # =\u003e :active\n\nvehicle.fire_events!(:ignite, :enable_alarm)    # =\u003e StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm\n\n# Human-friendly names can be accessed for states/events\nVehicle.human_state_name(:first_gear)               # =\u003e \"first gear\"\nVehicle.human_alarm_state_name(:active)             # =\u003e \"active\"\n\nVehicle.human_state_event_name(:shift_down)         # =\u003e \"shift down\"\nVehicle.human_alarm_state_event_name(:enable)       # =\u003e \"enable\"\n\n# States / events can also be references by the string version of their name\nVehicle.human_state_name('first_gear')              # =\u003e \"first gear\"\nVehicle.human_state_event_name('shift_down')        # =\u003e \"shift down\"\n\n# Available transition paths can be analyzed for an object\nvehicle.state_paths                                       # =\u003e [[#\u003cStateMachines:Transition ...], [#\u003cStateMachines:Transition ...], ...]\nvehicle.state_paths.to_states                             # =\u003e [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]\nvehicle.state_paths.events                                # =\u003e [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]\n\n# Possible states can be analyzed for a class\nVehicle.state_machine.states.to_a                   # [#\u003cStateMachines::State name=:parked value=\"parked\" initial=true\u003e, #\u003cStateMachines::State name=:idling value=\"idling\" initial=false\u003e, ...]\nVehicle.state_machines[:state].states.to_a          # [#\u003cStateMachines::State name=:parked value=\"parked\" initial=true\u003e, #\u003cStateMachines::State name=:idling value=\"idling\" initial=false\u003e, ...]\n\n# Find all paths that start and end on certain states\nvehicle.state_paths(:from =\u003e :parked, :to =\u003e :first_gear) # =\u003e [[\n                                                          #       #\u003cStateMachines:Transition attribute=:state event=:ignite from=\"parked\" ...\u003e,\n                                                          #       #\u003cStateMachines:Transition attribute=:state event=:shift_up from=\"idling\" ...\u003e\n                                                          #    ]]\n# Skipping state_machine and writing to attributes directly\nvehicle.state = \"parked\"\nvehicle.state                   # =\u003e \"parked\"\nvehicle.state_name              # =\u003e :parked\n\n# *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine):\n# vehicle.state = :parked\n```\n\n## Additional Topics\n\n### Explicit vs. Implicit Event Transitions\n\nEvery event defined for a state machine generates an instance method on the\nclass that allows the event to be explicitly triggered.  Most of the examples in\nthe state_machine documentation use this technique.  However, with some types of\nintegrations, like ActiveRecord, you can also *implicitly* fire events by\nsetting a special attribute on the instance.\n\nSuppose you're using the ActiveRecord integration and the following model is\ndefined:\n\n```ruby\nclass Vehicle \u003c ActiveRecord::Base\n  state_machine initial: :parked do\n    event :ignite do\n      transition parked: :idling\n    end\n  end\nend\n```\n\nTo trigger the `ignite` event, you would typically call the `Vehicle#ignite`\nmethod like so:\n\n```ruby\nvehicle = Vehicle.create    # =\u003e #\u003cVehicle id=1 state=\"parked\"\u003e\nvehicle.ignite              # =\u003e true\nvehicle.state               # =\u003e \"idling\"\n```\n\nThis is referred to as an *explicit* event transition.  The same behavior can\nalso be achieved *implicitly* by setting the state event attribute and invoking\nthe action associated with the state machine.  For example:\n\n```ruby\nvehicle = Vehicle.create        # =\u003e #\u003cVehicle id=1 state=\"parked\"\u003e\nvehicle.state_event = 'ignite'  # =\u003e 'ignite'\nvehicle.save                    # =\u003e true\nvehicle.state                   # =\u003e 'idling'\nvehicle.state_event             # =\u003e nil\n```\n\nAs you can see, the `ignite` event was automatically triggered when the `save`\naction was called.  This is particularly useful if you want to allow users to\ndrive the state transitions from a web API.\n\nSee each integration's API documentation for more information on the implicit\napproach.\n\n### Symbols vs. Strings\n\nIn all of the examples used throughout the documentation, you'll notice that\nstates and events are almost always referenced as symbols.  This isn't a\nrequirement, but rather a suggested best practice.\n\nYou can very well define your state machine with Strings like so:\n\n```ruby\nclass Vehicle\n  state_machine initial: 'parked' do\n    event 'ignite' do\n      transition 'parked' =\u003e 'idling'\n    end\n\n    # ...\n  end\nend\n```\n\nYou could even use numbers as your state / event names.  The **important** thing\nto keep in mind is that the type being used for referencing states / events in\nyour machine definition must be **consistent**.  If you're using Symbols, then\nall states / events must use Symbols.  Otherwise you'll encounter the following\nerror:\n\n```ruby\nclass Vehicle\n  state_machine do\n    event :ignite do\n      transition parked: 'idling'\n    end\n  end\nend\n\n# =\u003e ArgumentError: \"idling\" state defined as String, :parked defined as Symbol; all states must be consistent\n```\n\nThere **is** an exception to this rule.  The consistency is only required within\nthe definition itself.  However, when the machine's helper methods are called\nwith input from external sources, such as a web form, state_machine will map\nthat input to a String / Symbol.  For example:\n\n```ruby\nclass Vehicle\n  state_machine initial: :parked do\n    event :ignite do\n      transition parked: :idling\n    end\n  end\nend\n\nv = Vehicle.new     # =\u003e #\u003cVehicle:0xb71da5f8 @state=\"parked\"\u003e\nv.state?('parked')  # =\u003e true\nv.state?(:parked)   # =\u003e true\n```\n\n**Note** that none of this actually has to do with the type of the value that\ngets stored.  By default, all state values are assumed to be string -- regardless\nof whether the state names are symbols or strings.  If you want to store states\nas symbols instead you'll have to be explicit about it:\n\n```ruby\nclass Vehicle\n  state_machine initial: :parked do\n    event :ignite do\n      transition parked: :idling\n    end\n\n    states.each do |state|\n      self.state(state.name, :value =\u003e state.name.to_sym)\n    end\n  end\nend\n\nv = Vehicle.new     # =\u003e #\u003cVehicle:0xb71da5f8 @state=:parked\u003e\nv.state?('parked')  # =\u003e true\nv.state?(:parked)   # =\u003e true\n```\n\n### Syntax flexibility\n\nAlthough state_machine introduces a simplified syntax, it still remains\nbackwards compatible with previous versions and other state-related libraries by\nproviding some flexibility around how transitions are defined.  See below for an\noverview of these syntaxes.\n\n#### Verbose syntax\n\nIn general, it's recommended that state machines use the implicit syntax for\ntransitions.  However, you can be a little more explicit and verbose about\ntransitions by using the `:from`, `:except_from`, `:to`,\nand `:except_to` options.\n\nFor example, transitions and callbacks can be defined like so:\n\n```ruby\nclass Vehicle\n  state_machine initial: :parked do\n    before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt\n    after_transition to: :parked do |vehicle, transition|\n      vehicle.seatbelt = 'off'\n    end\n\n    event :ignite do\n      transition from: :parked, to: :idling\n    end\n  end\nend\n```\n\n#### Transition context\n\nSome flexibility is provided around the context in which transitions can be\ndefined.  In almost all examples throughout the documentation, transitions are\ndefined within the context of an event.  If you prefer to have state machines\ndefined in the context of a **state** either out of preference or in order to\neasily migrate from a different library, you can do so as shown below:\n\n```ruby\nclass Vehicle\n  state_machine initial: :parked do\n    # ...\n\n    state :parked do\n      transition to: :idling, :on =\u003e [:ignite, :shift_up], if: :seatbelt_on?\n\n      def speed\n        0\n      end\n    end\n\n    state :first_gear do\n      transition to: :second_gear, on: :shift_up\n\n      def speed\n        10\n      end\n    end\n\n    state :idling, :first_gear do\n      transition to: :parked, on: :park\n    end\n  end\nend\n```\n\nIn the above example, there's no need to specify the `from` state for each\ntransition since it's inferred from the context.\n\nYou can also define transitions completely outside the context of a particular\nstate / event.  This may be useful in cases where you're building a state\nmachine from a data store instead of part of the class definition.  See the\nexample below:\n\n```ruby\nclass Vehicle\n  state_machine initial: :parked do\n    # ...\n\n    transition parked: :idling, :on =\u003e [:ignite, :shift_up]\n    transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up\n    transition [:idling, :first_gear] =\u003e :parked, on: :park\n    transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy?\n  end\nend\n```\n\nNotice that in these alternative syntaxes:\n\n* You can continue to configure `:if` and `:unless` conditions\n* You can continue to define `from` states (when in the machine context) using\nthe `all`, `any`, and `same` helper methods\n\n### Static / Dynamic definitions\n\nIn most cases, the definition of a state machine is **static**.  That is to say,\nthe states, events and possible transitions are known ahead of time even though\nthey may depend on data that's only known at runtime.  For example, certain\ntransitions may only be available depending on an attribute on that object it's\nbeing run on.  All of the documentation in this library define static machines\nlike so:\n\n```ruby\nclass Vehicle\n  state_machine :state, initial: :parked do\n    event :park do\n      transition [:idling, :first_gear] =\u003e :parked\n    end\n\n    # ...\n  end\nend\n```\n\n#### Draw state machines\n\nState machines includes a default STDIORenderer for debugging state machines without external dependencies.\nThis renderer can be used to visualize the state machine in the console.\n\nTo use the renderer, simply call the `draw` method on the state machine:\n\n```ruby\nVehicle.state_machine.draw # Outputs the state machine diagram to the console\n```\n\nYou can customize the output by passing in options to the `draw` method, such as the output stream:\n\n```ruby\nVehicle.state_machine.draw(io: $stderr) # Outputs the state machine diagram to stderr\n```\n\n#### Dynamic definitions\n\nThere may be cases where the definition of a state machine is **dynamic**.\nThis means that you don't know the possible states or events for a machine until\nruntime.  For example, you may allow users in your application to manage the\nstate machine of a project or task in your system.  This means that the list of\ntransitions (and their associated states / events) could be stored externally,\nsuch as in a database.  In a case like this, you can define dynamically-generated\nstate machines like so:\n\n```ruby\nclass Vehicle\n  attr_accessor :state\n\n  # Make sure the machine gets initialized so the initial state gets set properly\n  def initialize(*)\n    super\n    machine\n  end\n\n  # Replace this with an external source (like a db)\n  def transitions\n    [\n      {parked: :idling, on: :ignite},\n      {idling: :first_gear, first_gear: :second_gear, on: :shift_up}\n      # ...\n    ]\n  end\n\n  # Create a state machine for this vehicle instance dynamically based on the\n  # transitions defined from the source above\n  def machine\n    vehicle = self\n    @machine ||= Machine.new(vehicle, initial: :parked, action: :save) do\n      vehicle.transitions.each {|attrs| transition(attrs)}\n    end\n  end\n\n  def save\n    # Save the state change...\n    true\n  end\nend\n\n# Generic class for building machines\nclass Machine\n  def self.new(object, *args, \u0026block)\n    machine_class = Class.new\n    machine = machine_class.state_machine(*args, \u0026block)\n    attribute = machine.attribute\n    action = machine.action\n\n    # Delegate attributes\n    machine_class.class_eval do\n      define_method(:definition) { machine }\n      define_method(attribute) { object.send(attribute) }\n      define_method(\"#{attribute}=\") {|value| object.send(\"#{attribute}=\", value) }\n      define_method(action) { object.send(action) } if action\n    end\n\n    machine_class.new\n  end\nend\n\nvehicle = Vehicle.new                   # =\u003e #\u003cVehicle:0xb708412c @state=\"parked\" ...\u003e\nvehicle.state                           # =\u003e \"parked\"\nvehicle.machine.ignite                  # =\u003e true\nvehicle.machine.state                   # =\u003e \"idling\"\nvehicle.state                           # =\u003e \"idling\"\nvehicle.machine.state_transitions       # =\u003e [#\u003cStateMachines:Transition ...\u003e]\nvehicle.machine.definition.states.keys  # =\u003e :first_gear, :second_gear, :parked, :idling\n```\n\nAs you can see, state_machine provides enough flexibility for you to be able\nto create new machine definitions on the fly based on an external source of\ntransitions.\n\n## Dependencies\n\nRuby versions officially supported and tested:\n\n* Ruby (MRI) 3.0.0+\n\nFor graphing state machine:\n\n* [state_machines-graphviz](https://github.com/state-machines/state_machines-graphviz)\n\nFor documenting state machines:\n\n* [state_machines-yard](https://github.com/state-machines/state_machines-yard)\n\nFor RSpec testing, use the custom RSpec matchers:\n\n* [state_machines-rspec](https://github.com/state-machines/state_machines-rspec)\n\n## Contributing\n\n1. Fork it ( https://github.com/state-machines/state_machines/fork )\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstate-machines%2Fstate_machines","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstate-machines%2Fstate_machines","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstate-machines%2Fstate_machines/lists"}