{"id":13463325,"url":"https://github.com/gocardless/statesman","last_synced_at":"2026-07-30T11:00:18.241Z","repository":{"id":687001,"uuid":"12614368","full_name":"gocardless/statesman","owner":"gocardless","description":"A statesmanlike state machine library.","archived":false,"fork":false,"pushed_at":"2026-07-23T09:08:51.000Z","size":1057,"stargazers_count":1905,"open_issues_count":17,"forks_count":158,"subscribers_count":119,"default_branch":"master","last_synced_at":"2026-07-23T16:01:41.502Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://gocardless.com/blog/statesman/","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/gocardless.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2013-09-05T09:42:17.000Z","updated_at":"2026-07-23T09:50:53.000Z","dependencies_parsed_at":"2024-04-22T07:53:43.615Z","dependency_job_id":"9982a542-9225-4674-b622-7ce55989da0f","html_url":"https://github.com/gocardless/statesman","commit_stats":{"total_commits":600,"total_committers":85,"mean_commits":"7.0588235294117645","dds":0.76,"last_synced_commit":"455a21dd74bb7d3b7555de0dd66e2ece9461c22d"},"previous_names":[],"tags_count":62,"template":false,"template_full_name":null,"purl":"pkg:github/gocardless/statesman","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fstatesman","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fstatesman/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fstatesman/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fstatesman/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gocardless","download_url":"https://codeload.github.com/gocardless/statesman/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fstatesman/sbom","scorecard":{"id":433129,"data":{"date":"2025-08-11","repo":{"name":"github.com/gocardless/statesman","commit":"eac9e4e25ce562eda853d82ad169376a980e4278"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5.5,"checks":[{"name":"Code-Review","score":10,"reason":"all changesets reviewed","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/tests.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:58: update your workflow using https://app.stepsecurity.io/secureworkflow/gocardless/statesman/tests.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/tests.yml:60: update your workflow using https://app.stepsecurity.io/secureworkflow/gocardless/statesman/tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:104: update your workflow using https://app.stepsecurity.io/secureworkflow/gocardless/statesman/tests.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/tests.yml:106: update your workflow using https://app.stepsecurity.io/secureworkflow/gocardless/statesman/tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/gocardless/statesman/tests.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/tests.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/gocardless/statesman/tests.yml/master?enable=pin","Info:   0 out of   3 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   3 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/gocardless/.github/SECURITY.md:1","Info: Found linked content: github.com/gocardless/.github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/gocardless/.github/SECURITY.md:1","Info: Found text in security policy: github.com/gocardless/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-19T03:51:40.745Z","repository_id":687001,"created_at":"2025-08-19T03:51:40.746Z","updated_at":"2025-08-19T03:51:40.746Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35898043,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-26T02:00:06.503Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2024-07-31T13:00:50.987Z","updated_at":"2026-07-30T11:00:18.203Z","avatar_url":"https://github.com/gocardless.png","language":"Ruby","funding_links":[],"categories":["Rails Plugins","Web 后端","Ruby","Gems","State Machines","Libraries"],"sub_categories":["State Machines","Ruby"],"readme":"\u003c!-- markdownlint-disable no-inline-html first-line-heading --\u003e\n\u003cp align=\"center\"\u003e\u003cimg src=\"https://user-images.githubusercontent.com/110275/106792848-96e4ee80-664e-11eb-8fd1-16ff24b41eb2.png\" alt=\"Statesman\" width=\"512\"\u003e\u003c/p\u003e\n\u003c!-- markdownlint-enable no-inline-html first-line-heading --\u003e\n\nA statesmanlike state machine library.\n\nFor our policy on compatibility with Ruby and Rails versions, see [COMPATIBILITY.md](docs/COMPATIBILITY.md).\n\n[![Gem Version](https://badge.fury.io/rb/statesman.svg)](http://badge.fury.io/rb/statesman)\n[![CircleCI](https://circleci.com/gh/gocardless/statesman.svg?style=shield)](https://circleci.com/gh/gocardless/statesman)\n[![Code Climate](https://codeclimate.com/github/gocardless/statesman.svg)](https://codeclimate.com/github/gocardless/statesman)\n[![Gitter](https://badges.gitter.im/join.svg)](https://gitter.im/gocardless/statesman?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n[![SemVer](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=statesman\u0026package-manager=bundler\u0026version-scheme=semver\u0026previous-version=11.0.0\u0026new-version=12.0.0)](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=statesman\u0026package-manager=bundler\u0026version-scheme=semver\u0026previous-version=11.0.0\u0026new-version=12.0.0)\n\nStatesman is an opinionated state machine library designed to provide a robust\naudit trail and data integrity. It decouples the state machine logic from the\nunderlying model and allows for easy composition with one or more model classes.\n\nAs such, the design of statesman is a little different from other state machine\nlibraries:\n\n- State behaviour is defined in a separate, \"state machine\" class, rather than\nadded directly onto a model. State machines are then instantiated with the model\nto which they should apply.\n- State transitions are also modelled as a class, which can optionally be\npersisted to the database for a full audit history. This audit history can\ninclude JSON metadata set during a transition.\n- Database indices are used to offer database-level transaction duplication\nprotection.\n\n## Installation\n\nTo get started, just add Statesman to your `Gemfile`, and then run `bundle`:\n\n```ruby\ngem 'statesman', '~\u003e 13.0.0'\n```\n\n## Usage\n\nFirst, create a state machine based on `Statesman::Machine`:\n\n```ruby\nclass OrderStateMachine\n  include Statesman::Machine\n\n  state :pending, initial: true\n  state :checking_out\n  state :purchased\n  state :shipped\n  state :cancelled\n  state :failed\n  state :refunded\n\n  transition from: :pending,      to: [:checking_out, :cancelled]\n  transition from: :checking_out, to: [:purchased, :cancelled]\n  transition from: :purchased,    to: [:shipped, :failed]\n  transition from: :shipped,      to: :refunded\n\n  guard_transition(to: :checking_out) do |order|\n    order.products_in_stock?\n  end\n\n  before_transition(from: :checking_out, to: :cancelled) do |order, transition|\n    order.reallocate_stock\n  end\n\n  before_transition(to: :purchased) do |order, transition|\n    PaymentService.new(order).submit\n  end\n\n  after_transition(to: :purchased) do |order, transition|\n    MailerService.order_confirmation(order).deliver\n  end\nend\n```\n\nThen, link it to your model:\n\n```ruby\nclass Order \u003c ActiveRecord::Base\n  has_many :order_transitions, autosave: false\n\n  include Statesman::Adapters::ActiveRecordQueries[\n    transition_class: OrderTransition,\n    initial_state: :pending\n  ]\n\n  def state_machine\n    @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)\n  end\nend\n```\n\nNext, you'll need to create a further model to represent state transitions:\n\n```ruby\nclass OrderTransition \u003c ActiveRecord::Base\n  include Statesman::Adapters::ActiveRecordTransition\n\n  validates :to_state, inclusion: { in: OrderStateMachine.states }\n\n  belongs_to :order, inverse_of: :order_transitions\nend\n```\n\nNow, you can start working with your state machine:\n\n```ruby\nOrder.first.state_machine.current_state # =\u003e \"pending\"\nOrder.first.state_machine.allowed_transitions # =\u003e [\"checking_out\", \"cancelled\"]\nOrder.first.state_machine.can_transition_to?(:cancelled) # =\u003e true/false\nOrder.first.state_machine.transition_to(:cancelled, optional: :metadata) # =\u003e true/false\nOrder.first.state_machine.transition_to!(:cancelled) # =\u003e true/exception\nOrder.first.state_machine.last_transition # =\u003e transition model or nil\nOrder.first.state_machine.last_transition_to(:pending) # =\u003e transition model or nil\n\nOrder.in_state(:cancelled) # =\u003e [#\u003cOrder id: \"123\"\u003e]\nOrder.not_in_state(:checking_out) # =\u003e [#\u003cOrder id: \"123\"\u003e]\n```\n\nIf you'd like, you can also define a template for a generic state machine, then alter classes which extend it as required:\n\n```ruby\nmodule Template\n  def define_states\n    state :a, initial: true\n    state :b\n    state :c\n  end\n\n  def define_transitions\n    transition from: :a, to: :b\n    transition from: :b, to: :c\n    transition from: :c, to: :a\n  end\nend\n\nclass Circular\n  include Statesman::Machine\n  extend Template\n\n  define_states\n  define_transitions\nend\n\nclass Linear\n  include Statesman::Machine\n  extend Template\n\n  define_states\n  define_transitions\n\n  remove_transitions from: :c, to: :a\nend\n\nclass Shorter\n  include Statesman::Machine\n  extend Template\n\n  define_states\n  define_transitions\n\n  remove_state :c\nend\n```\n\n## Persistence\n\nBy default Statesman stores transition history in memory only. It can be\npersisted by configuring Statesman to use a different adapter. For example,\nfor ActiveRecord within Rails:\n\n`config/initializers/statesman.rb`:\n\n```ruby\nStatesman.configure do\n  storage_adapter(Statesman::Adapters::ActiveRecord)\nend\n```\n\nGenerate the transition model:\n\n```bash\nrails g statesman:active_record_transition Order OrderTransition\n```\n\nYour transition class should\n`include Statesman::Adapters::ActiveRecordTransition` if you're using the\nActiveRecord adapter.\n\nIf you're using the ActiveRecord adapter and decide not to include the default\n`updated_at` column in your transition table, you'll need to configure the\n`updated_timestamp_column` option on the transition class, setting it to another column\nname (e.g. `:updated_on`) or `nil`.\n\nAnd add an association from the parent model:\n\n`app/models/order.rb`:\n\n```ruby\nclass Order \u003c ActiveRecord::Base\n  has_many :transitions, class_name: \"OrderTransition\", autosave: false\n\n  # Initialize the state machine\n  def state_machine\n    @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition,\n                                                   association_name: :transitions)\n  end\n\n  # Optionally delegate some methods\n\n  delegate :can_transition_to?,\n           :current_state, :history, :last_transition, :last_transition_to,\n           :transition_to!, :transition_to, :in_state?, to: :state_machine\nend\n```\n\n### Using PostgreSQL JSON column\n\nBy default, Statesman uses `serialize` to store the metadata in JSON format.\nIt is also possible to use the PostgreSQL JSON column if you are using Rails 4\nor 5. To do that\n\n- Change `metadata` column type in the transition model migration to `json` or `jsonb`\n\n  ```ruby\n  # Before\n  t.text :metadata, default: \"{}\"\n  # After (Rails 4)\n  t.json :metadata, default: \"{}\"\n  # After (Rails 5)\n  t.json :metadata, default: {}\n  ```\n\n* Remove the `include Statesman::Adapters::ActiveRecordTransition` statement from\n  your transition model, which would've instructed ActiveRecord to serialize the\n  metadata.\n* The module that you just removed enables customizing the updatated timestamp column\n  as described above. Having removed it, if you want to customise your transition class's\n  \"updated timestamp column\", you should define a `.updated_timestamp_column` method on\n  your class and return the name of the column as a symbol, or `nil` if you don't want\n  to record an updated timestamp on transitions.\n\n## Configuration\n\n### `storage_adapter`\n\n```ruby\nStatesman.configure do\n  storage_adapter(Statesman::Adapters::ActiveRecord)\nend\n```\n\nStatesman defaults to storing transitions in memory. If you're using rails, you\ncan instead configure it to persist transitions to the database by using the\nActiveRecord adapter.\n\nStatesman will fallback to memory unless you specify a transition_class when instantiating your state machine. This allows you to only persist transitions on certain state machines in your app.\n\n### `initial_transition`\n\n```ruby\ndef state_machine\n  @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition,\n                                                 association_name: :transitions,\n                                                 initial_transition: true)\nend\n```\n\nBy default Statesman does not record a transition to the initial state of the state machine.\n\nYou can configure Statesman to record a transition to the initial state, this will allow you to:\n- Keep an accurate record of the initial state even if configuration changes\n- Keep a record of how long the state machine spent in the initial state\n- Utilise a transition hook for the transition to the initial state\n\n## Class methods\n\n### `Machine.state`\n\n```ruby\nMachine.state(:some_state, initial: true)\nMachine.state(:another_state)\n```\n\nDefine a new state and optionally mark as the initial state.\n\n### `Machine.transition`\n\n```ruby\nMachine.transition(from: :some_state, to: :another_state)\n```\n\nDefine a transition rule. Both method parameters are required, `to` can also be\nan array of states (`.transition(from: :some_state, to: [:another_state, :some_other_state])`).\n\n### `Machine.guard_transition`\n\n```ruby\nMachine.guard_transition(from: :some_state, to: :another_state) do |object|\n  object.some_boolean?\nend\n```\n\nDefine a guard. `to` and `from` parameters are optional, a nil parameter means\nguard all transitions. The passed block should evaluate to a boolean and must\nbe idempotent as it could be called many times. The guard will pass when it\nevaluates to a truthy value and fail when it evaluates to a falsey value (`nil` or `false`).\n\n### `Machine.before_transition`\n\n```ruby\nMachine.before_transition(from: :some_state, to: :another_state) do |object|\n  object.side_effect\nend\n```\n\nDefine a callback to run before a transition. `to` and `from` parameters are\noptional, a nil parameter means run before all transitions. This callback can\nhave side-effects as it will only be run once immediately before the transition.\n\n### `Machine.after_transition`\n\n```ruby\nMachine.after_transition(from: :some_state, to: :another_state) do |object, transition|\n  object.side_effect\nend\n```\n\nDefine a callback to run after a successful transition. `to` and `from`\nparameters are optional, a nil parameter means run after all transitions. The\nmodel object and transition object are passed as arguments to the callback.\nThis callback can have side-effects as it will only be run once immediately\nafter the transition.\n\nIf you specify `after_commit: true`, the callback will be executed once the\ntransition has been committed to the database.\n\n### `Machine.after_transition_failure`\n\n```ruby\nMachine.after_transition_failure(from: :some_state, to: :another_state) do |object, exception|\n  Logger.info(\"transition to #{exception.to} failed for #{object.id}\")\nend\n```\n\nDefine a callback to run if `Statesman::TransitionFailedError` is raised\nduring the execution of transition callbacks. `to` and `from`\nparameters are optional, a nil parameter means run after all transitions.\nThe model object, and exception are passed as arguments to the callback.\nThis is executed outside of the transaction wrapping other callbacks.\nIf using `transition!` the exception is re-raised after these callbacks are\nexecuted.\n\n### `Machine.after_guard_failure`\n\n```ruby\nMachine.after_guard_failure(from: :some_state, to: :another_state) do |object, exception|\n  Logger.info(\"guard failed during transition to #{exception.to} for #{object.id}\")\nend\n```\n\nDefine a callback to run if `Statesman::GuardFailedError` is raised\nduring the execution of guard callbacks. `to` and `from`\nparameters are optional, a nil parameter means run after all transitions.\nThe model object, and exception are passed as arguments to the callback.\nThis is executed outside of the transaction wrapping other callbacks.\nIf using `transition!` the exception is re-raised after these callbacks are\nexecuted.\n\n### `Machine.new`\n\n```ruby\nmy_machine = Machine.new(my_model, transition_class: MyTransitionModel)\n```\n\nInitialize a new state machine instance. `my_model` is required. If using the\nActiveRecord adapter `my_model` should have a `has_many` association with\n`MyTransitionModel`.\n\n### `Machine.retry_conflicts`\n\n```ruby\nMachine.retry_conflicts { instance.transition_to(:new_state) }\n```\n\nAutomatically retry the given block if a `TransitionConflictError` is raised.\nIf you know you want to retry a transition if it fails due to a race condition\ncall it from within this block. Takes an (optional) argument for the maximum\nnumber of retry attempts (defaults to 1).\n\n### `Machine.states`\n\nReturns an array of all possible state names as strings.\n\n### `Machine.successors`\n\nReturns a hash of states and the states it is valid for them to transition to.\n\n```ruby\nMachine.successors\n\n{\n  \"pending\" =\u003e [\"checking_out\", \"cancelled\"],\n  \"checking_out\" =\u003e [\"purchased\", \"cancelled\"],\n  \"purchased\" =\u003e [\"shipped\", \"failed\"],\n  \"shipped\" =\u003e [\"refunded\"]\n}\n```\n\n## Class constants\nAdding a state to a state machine will automatically create a constant for the value, for example:\n```ruby\nclass OrderStateMachine\n  include Statesman::Machine\n\n  state :pending, initial: true\n  state :checking_out\n  state :cancelled\n\n  # Constants created as a side effect of adding state\n  transition from: PENDING, to: [CHECKING_OUT, CANCELLED]\nend\n\nOrderStateMachine::PENDING #=\u003e \"pending\"\nOrderStateMachine::CHECKING_OUT # =\u003e \"checking_out\"\n```\n\n## Instance methods\n\n### `Machine#current_state`\n\nReturns the current state based on existing transition objects.\n\nTakes an optional keyword argument to force a reload of data from the\ndatabase.\ne.g `current_state(force_reload: true)`\n\n### `Machine#in_state?(:state_1, :state_2, ...)`\n\nReturns true if the machine is in any of the given states.\n\n### `Machine#history`\n\nReturns a sorted array of all transition objects.\n\n### `Machine#last_transition`\n\nReturns the most recent transition object.\n\n### `Machine#last_transition_to(:state)`\n\nReturns the most recent transition object to a given state.\n\n### `Machine#allowed_transitions`\n\nReturns an array of states you can `transition_to` from current state.\n\n### `Machine#can_transition_to?(:state)`\n\nReturns true if the current state can transition to the passed state and all\napplicable guards pass.\n\n### `Machine#transition_to!(:state)`\n\nTransition to the passed state, returning `true` on success. Raises\n`Statesman::GuardFailedError` or `Statesman::TransitionFailedError` on failure.\n\n### `Machine#transition_to(:state)`\n\nTransition to the passed state, returning `true` on success. Swallows all\nStatesman exceptions and returns false on failure. (NB. if your guard or\ncallback code throws an exception, it will not be caught.)\n\n## Errors\n\n### Initialization errors\n\nThese errors are raised when the Machine and/or Model is initialized. A simple spec like\n\n```ruby\nexpect { OrderStateMachine.new(Order.new, transition_class: OrderTransition) }.to_not raise_error\n```\n\nwill expose these errors as part of your test suite\n\n#### InvalidStateError\n\nRaised if:\n\n- Attempting to define a transition without a `to` state.\n- Attempting to define a transition with a non-existent state.\n- Attempting to define multiple states as `initial`.\n\n#### InvalidTransitionError\n\nRaised if:\n\n- Attempting to define a callback `from` a state that has no valid transitions (A terminal state).\n- Attempting to define a callback `to` the `initial` state if that state has no transitions to it.\n- Attempting to define a callback with `from` and `to` where any of the pairs have no transition between them.\n\n#### InvalidCallbackError\n\nRaised if:\n\n- Attempting to define a callback without a block.\n\n#### UnserializedMetadataError\n\nRaised if:\n\n- ActiveRecord is configured to not serialize the `metadata` attribute into\n  to Database column backing it. See the `Using PostgreSQL JSON column` section.\n\n#### IncompatibleSerializationError\n\nRaised if:\n\n- There is a mismatch between the column type of the `metadata` in the\n  Database and the model. See the `Using PostgreSQL JSON column` section.\n\n#### MissingTransitionAssociation\n\nRaised if:\n\n- The model that `Statesman::Adapters::ActiveRecordQueries` is included in\n  does not have a `has_many` association to the `transition_class`.\n\n### Runtime errors\n\nThese errors are raised by `transition_to!`. Using `transition_to` will\nsuppress `GuardFailedError` and `TransitionFailedError` and return `false` instead.\n\n#### GuardFailedError\n\nRaised if:\n\n- A guard callback between `from` and `to` state returned a falsey value.\n\nThe model object that failed to transition is available via `object`:\n\n```ruby\nbegin\n  my_model.transition_to!(:some_state)\nrescue Statesman::GuardFailedError =\u003e e\n  logger.error(\"Guard failed for #{e.object.class}##{e.object.id}: #{e.message}\")\nend\n```\n\n#### TransitionFailedError\n\nRaised if:\n\n- A transition is attempted but `current_state -\u003e new_state` is not a valid pair.\n\n#### TransitionConflictError\n\nRaised if:\n\n- A database conflict affecting the `sort_key` or `most_recent` columns occurs\n  when attempting a transition.\n  Retried automatically if it occurs wrapped in `retry_conflicts`.\n\n## Model scopes\n\nA mixin is provided for the ActiveRecord adapter which adds scopes to easily\nfind all models currently in (or not in) a given state. Include it into your\nmodel and passing in `transition_class` and `initial_state` as options.\n\nIn 4.1.2 and below, these two options had to be defined as methods on the model,\nbut 5.0.0 and above allow this style of configuration as well.\nThe old way pollutes the model with extra class methods, and is deprecated,\nto be removed in the future.\n\n```ruby\nclass Order \u003c ActiveRecord::Base\n  has_many :order_transitions, autosave: false\n  include Statesman::Adapters::ActiveRecordQueries[\n    transition_class: OrderTransition,\n    initial_state: OrderStateMachine.initial_state\n  ]\nend\n```\n\nIf the transition class-name differs from the association name, you will also\nneed to pass `transition_name` as an option:\n\n```ruby\nclass Order \u003c ActiveRecord::Base\n  has_many :transitions, class_name: \"OrderTransition\", autosave: false\n  include Statesman::Adapters::ActiveRecordQueries[\n    transition_class: OrderTransition,\n    initial_state: OrderStateMachine.initial_state,\n    transition_name: :transitions\n  ]\nend\n```\n\n### `Model.in_state(:state_1, :state_2, etc)`\n\nReturns all models currently in any of the supplied states.\n\n### `Model.not_in_state(:state_1, :state_2, etc)`\n\nReturns all models not currently in any of the supplied states.\n\n### `Model.most_recent_transition_join`\n\nThis joins the model to its most recent transition whatever that may be.\nWe expose this method to ease use of ActiveRecord's `or` e.g\n\n```ruby\nModel.in_state(:state_1).or(\n  Model.most_recent_transition_join.where(model_field: 123)\n)\n```\n\n## Frequently Asked Questions\n\n### Storing the state on the model object\n\nIf you wish to store the model state on the model directly, you can keep it up\nto date using an `after_transition` hook.\nCombine it with the `after_commit` option to ensure the model state will only be\nsaved once the transition has made it irreversibly to the database:\n\n```ruby\nafter_transition(after_commit: true) do |model, transition|\n  model.state = transition.to_state\n  model.save!\nend\n```\n\nYou could also use a calculated column or view in your database.\n\n### Accessing metadata from the last transition\n\nGiven a field `foo` that was stored in the metadata, you can access it like so:\n\n```ruby\nmodel_instance.state_machine.last_transition.metadata[\"foo\"]\n```\n\n### Deleting records\n\nIf you need to delete the Parent model regularly you will need to change\neither the association deletion behaviour or add a `DELETE CASCADE` condition\nto foreign key in your database.\n\nE.g\n\n```ruby\nhas_many :order_transitions, autosave: false, dependent: :destroy\n```\n\nor when migrating the transition model\n\n```ruby\nadd_foreign_key :order_transitions, :orders, on_delete: :cascade\n```\n\n## Testing Statesman Implementations\n\nThis answer was abstracted from [this issue](https://github.com/gocardless/statesman/issues/77).\n\nAt GoCardless we focus on testing that:\n\n- guards correctly prevent / allow transitions\n- callbacks execute when expected and perform the expected actions\n\n### Testing Guards\n\nGuards can be tested by asserting that `transition_to!` does or does not raise a `Statesman::GuardFailedError`:\n\n```ruby\ndescribe \"guards\" do\n  it \"cannot transition from state foo to state bar\" do\n    expect { some_model.transition_to!(:bar) }.to raise_error(Statesman::GuardFailedError)\n  end\n\n  it \"can transition from state foo to state baz\" do\n    expect { some_model.transition_to!(:baz) }.to_not raise_error\n  end\nend\n```\n\n### Testing Callbacks\n\nCallbacks are tested by asserting that the action they perform occurs:\n\n```ruby\ndescribe \"some callback\" do\n  it \"adds one to the count property on the model\" do\n    expect { some_model.transition_to!(:some_state) }.\n      to change { some_model.reload.count }.\n      by(1)\n  end\nend\n```\n\n## Compatibility with type checkers\n\nIncluding ActiveRecordQueries to your model can cause issues with type checkers\nsuch as Sorbet, this is because this technically is using a dynamic include,\nwhich is not supported by Sorbet.\n\nTo avoid these issues you can instead include the TypeSafeActiveRecordQueries\nmodule and pass in configuration.\n\n```ruby\nclass Order \u003c ActiveRecord::Base\n  has_many :order_transitions, autosave: false\n\n  include Statesman::Adapters::TypeSafeActiveRecordQueries\n\n  configure_state_machine transition_class: OrderTransition,\n                          initial_state: :pending\n\n  def state_machine\n    @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)\n  end\nend\n```\n\n# Third-party extensions\n\n[statesman-sequel](https://github.com/badosu/statesman-sequel) - An adapter to make Statesman work with [Sequel](https://github.com/jeremyevans/sequel)\n\n---\n\nGoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/careers/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgocardless%2Fstatesman","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgocardless%2Fstatesman","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgocardless%2Fstatesman/lists"}