{"id":13723047,"url":"https://github.com/subvisual/fsmx","last_synced_at":"2025-04-04T15:08:54.370Z","repository":{"id":38401091,"uuid":"274637672","full_name":"subvisual/fsmx","owner":"subvisual","description":"A Finite-state machine implementation in Elixir, with opt-in Ecto friendliness","archived":false,"fork":false,"pushed_at":"2023-09-08T08:46:09.000Z","size":67,"stargazers_count":158,"open_issues_count":2,"forks_count":17,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-05-09T07:31:08.805Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/subvisual.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2020-06-24T10:11:22.000Z","updated_at":"2024-04-23T04:42:57.000Z","dependencies_parsed_at":"2024-02-03T11:47:41.122Z","dependency_job_id":null,"html_url":"https://github.com/subvisual/fsmx","commit_stats":{"total_commits":41,"total_committers":7,"mean_commits":5.857142857142857,"dds":0.1707317073170732,"last_synced_commit":"a6b3e759bf0914d763a46020ab6b641b1fb3b188"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subvisual%2Ffsmx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subvisual%2Ffsmx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subvisual%2Ffsmx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/subvisual%2Ffsmx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/subvisual","download_url":"https://codeload.github.com/subvisual/fsmx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247198461,"owners_count":20900080,"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":[],"created_at":"2024-08-03T01:01:35.722Z","updated_at":"2025-04-04T15:08:54.344Z","avatar_url":"https://github.com/subvisual.png","language":"Elixir","readme":"# Fsmx\n\n[ecto-multi]: https://hexdocs.pm/ecto/Ecto.Multi.html\n[bamboo]: https://github.com/thoughtbot/bamboo\n[sage]: https://github.com/Nebo15/sage\n\nA Finite-state machine implementation in Elixir, with opt-in Ecto friendliness.\n\nHighlights:\n\n- Plays nicely with both bare Elixir structs and Ecto changesets\n- Ability to wrap transitions inside an Ecto.Multi for atomic updates\n- Guides you in the right direction when it comes to [side effects](#a-note-on-side-effects)\n\n---\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Simple state machine](#simple-state-machine)\n  - [Callbacks before transitions](#callbacks-before-transitions)\n  - [Validating transitions](#validating-transitions)\n  - [Decoupling logic from data](#decoupling-logic-from-data)\n- [Ecto support](#ecto-support)\n  - [Transition changesets](#transition-changesets)\n  - [Transition with Ecto.Multi](#transition-with-ecto-multi)\n- [A note on side effects](#a-note-on-side-effects)\n- [Contributing](#contributing)\n\n## Installation\n\nAdd fsmx to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:fsmx, \"~\u003e 0.5.0\"}\n  ]\nend\n```\n\n## Usage\n\n### Simple state machine\n\n```elixir\ndefmodule App.StateMachine do\n  defstruct [:state, :data]\n\n  use Fsmx.Struct, transitions: %{\n    \"one\" =\u003e [\"two\", \"three\"],\n    \"two\" =\u003e [\"three\", \"four\"],\n    \"three\" =\u003e \"four\",\n    \"four\" =\u003e :*, # can transition to any state\n    :* =\u003e [\"five\"] # can transition from any state to \"five\"\n  }\nend\n```\n\nUse it via the `Fsmx.transition/2` function:\n\n```elixir\nstruct = %App.StateMachine{state: \"one\", data: nil}\n\nFsmx.transition(struct, \"two\")\n# {:ok, %App.StateMachine{state: \"two\"}}\n\nFsmx.transition(struct, \"four\")\n# {:error, \"invalid transition from one to four\"}\n```\n\n### Callbacks before transitions\n\nYou can implement a `before_transition/3` callback to mutate the struct when before a transition happens.\nYou only need to pattern-match on the scenarios you want to catch. No need to add a catch-all/do-nothing function at the\nend (the library already does that for you).\n\n```elixir\ndefmodule App.StateMachine do\n  # ...\n\n  def before_transition(struct, \"two\", _destination_state) do\n    {:ok, %{struct | data: %{foo: :bar}}}\n  end\nend\n```\n\nUsage:\n\n```elixir\nstruct = %App.StateMachine{state: \"two\", data: nil}\n\nFsmx.transition(struct, \"three\")\n# {:ok, %App.StateMachine{state: \"three\", data: %{foo: :bar}}\n```\n\n### Validating transitions\n\nThe same `before_transition/3` callback can be used to add custom validation logic, by returning an `{:error, _}` tuple\nwhen needed:\n\n```elixir\ndefmodule App.StateMachine do\n  # ...\n\n\n  def before_transition(%{data: nil}, _initial_state, \"four\") do\n    {:error, \"cannot reach state four without data\"}\n  end\nend\n```\n\nUsage:\n\n```elixir\nstruct = %App.StateMachine{state: \"two\", data: nil}\n\nFsmx.transition(struct, \"four\")\n# {:error, \"cannot reach state four without data\"}\n```\n\n### Decoupling logic from data\n\nSince logic can grow a lot, and fall out of scope in your structs/schemas, it's often useful to separate\nall that business logic into a separate module:\n\n```elixir\ndefmodule App.StateMachine do\n  defstruct [:state]\n\n  use Fsmx.Struct, fsm: App.BusinessLogic\nend\n\ndefmodule App.BusinessLogic do\n  use Fsmx.Fsm, transitions: %{\n    \"one\" =\u003e [\"two\", \"three\"],\n    \"two\" =\u003e [\"three\", \"four\"],\n    \"three\" =\u003e \"four\"\n  }\n\n  # callbacks go here now\n  def before_transition(struct, \"two\", _destination_state) do\n    {:ok, %{struct | data: %{foo: :bar}}}\n  end\n\n  def before_transition(%{data: nil}, _initial_state, \"four\") do\n    {:error, \"cannot reach state four without data\"}\n  end\nend\n```\n\n### Multiple state machines in the same struct\n\nNot all structs have a single state machine, sometimes you might need more,\nusing different fields for that effect. Here's how you can do it:\n\n```elixir\ndefmodule App.StateMachine do\n  defstruct [:state, :other_state, :data]\n\n  use Fsmx.Struct, transitions: %{\n    \"one\" =\u003e [\"two\", \"three\"],\n    \"two\" =\u003e [\"three\", \"four\"],\n    \"three\" =\u003e \"four\",\n    \"four\" =\u003e :*, # can transition to any state\n    :* =\u003e [\"five\"] # can transition from any state to \"five\"\n  }\n\n  use Fsmx.Struct,\n    state_field: :other_state,\n    transitions: %{\n        \"initial\" =\u003e [\"middle\", \"middle2\"],\n        \"middle\" =\u003e \"middle2\",\n        :* =\u003e \"final\"\n    }\nend\n```\n\nUse it via the `Fsmx.transition/3` function:\n\n```elixir\nstruct = %App.StateMachine{state: \"one\", other_state: \"initial\", data: nil}\n\nFsmx.transition(struct, \"two\")\n# {:ok, %App.StateMachine{state: \"two\", other_state: \"initial\"}}\n\nFsmx.transition(struct, \"final\", field: :other_state)\n# {:ok, %App.StateMachine{state: \"one\", other_state: \"final\"}}\n```\n\n## Ecto support\n\nSupport for Ecto is built in, as long as `ecto` is in your `mix.exs` dependencies. With it, you get the ability to\ndefine state machines using Ecto schemas, and the `Fsmx.Ecto` module:\n\n```elixir\ndefmodule App.StateMachineSchema do\n  use Ecto.Schema\n\n  schema \"state_machine\" do\n    field :state, :string, default: \"one\"\n    field :data, :map\n  end\n\n  use Fsmx.Struct, transitions: %{\n    \"one\" =\u003e [\"two\", \"three\"],\n    \"two\" =\u003e [\"three\", \"four\"],\n    \"three\" =\u003e \"four\"\n  }\nend\n```\n\nYou can then mutate your state machine in one of two ways:\n\n### 1. Transition changesets\n\nReturns a changeset that mutates the `:state` field (or `{:error, _}` if the transition is invalid).\n\n```elixir\n{:ok, schema} = %App.StateMachineSchema{state: \"one\"} |\u003e Repo.insert()\n\nFsmx.transition_changeset(schema, \"two\")\n# #Ecto.Changeset\u003cchanges: %{state: \"two\"}\u003e\n```\n\nYou can customize the changeset function, and again pattern match on specific transitions, and additional params:\n\n```elixir\ndefmodule App.StateMachineSchema do\n  # ...\n\n  # only include sent data on transitions from \"one\" to \"two\"\n  def transition_changeset(changeset, \"one\", \"two\", params) do\n    # changeset already includes a :state field change\n    changeset\n    |\u003e cast(params, [:data])\n    |\u003e validate_required([:data])\n  end\n```\n\nUsage:\n\n```elixir\n{:ok, schema} = %App.StateMachineSchema{state: \"one\"} |\u003e Repo.insert()\n\nFsmx.transition_changeset(schema, \"two\", %{\"data\"=\u003e %{foo: :bar}})\n# #Ecto.Changeset\u003cchanges: %{state: \"two\", data: %{foo: :bar}\u003e\n```\n\n### 2. Transition with Ecto.Multi\n\n**Note: Please read [a note on side effects](#a-note-on-side-effects) first. Your future self will thank you.**\n\nIf a state transition is part of a larger operation, and you want to guarantee atomicity of the whole operation, you can\nplug a state transition into an [`Ecto.Multi`][ecto-multi]. The same changeset seen above will be used here:\n\n```elixir\n{:ok, schema} = %App.StateMachineSchema{state: \"one\"} |\u003e Repo.insert()\n\nEcto.Multi.new()\n|\u003e Fsmx.transition_multi(schema, \"transition-id\", \"two\", %{\"data\" =\u003e %{foo: :bar}})\n|\u003e Repo.transaction()\n```\n\nWhen using `Ecto.Multi`, you also get an additional `after_transition_multi/3` callback, where you can append additional\noperations the resulting transaction, such as dealing with side effects (but again, please know that [side effects are\ntricky](#a-note-on-side-effects))\n\n```elixir\ndefmodule App.StateMachineSchema do\n  def after_transition_multi(schema, _from, \"four\") do\n    Mailer.notify_admin(schema)\n    |\u003e Bamboo.deliver_later()\n\n    {:ok, nil}\n  end\nend\n```\n\nNote that `after_transition_multi/3` callbacks still run inside the database transaction, so be careful with expensive\noperations. In this example `Bamboo.deliver_later/1` (from the awesome [Bamboo][bamboo] package) doesn't spend time sending the actual email, it just spawns a task to do it asynchronously.\n\n## A note on side effects\n\nSide effects are tricky. Database transactions are meant to guarantee atomicity, but side effects often touch beyond the\ndatabase. Sending emails when a task is complete is a straight-forward example.\n\nWhen you run side effects within an `Ecto.Multi` you need to be aware that, should the transaction later be rolled\nback, there's no way to un-send that email.\n\nIf the side effect is the last operation within your `Ecto.Multi`, you're probably 99% fine, which works for a lot of cases.\nBut if you have more complex transactions, or if you do need 99.9999% consistency guarantees (because, let's face\nit, 100% is a pipe dream), then this simple library might not be for you.\n\nConsider looking at [`Sage`][sage], for instance.\n\n```elixir\n# this is *probably* fine\nEcto.Multi.new()\n|\u003e Fsmx.transition_multi(schema, \"transition-id\", \"two\", %{\"data\" =\u003e %{foo: :bar}})\n|\u003e Repo.transaction()\n\n# this is dangerous, because your transition callback\n# will run before the whole database transaction has run\nEcto.Multi.new()\n|\u003e Fsmx.transition_multi(schema, \"transition-id\", \"two\", %{\"data\" =\u003e %{foo: :bar}})\n|\u003e Ecto.Multi.update(:update, a_very_unreliable_changeset())\n|\u003e Repo.transaction()\n```\n\n## Contributing\n\nFeel free to contribute. Either by opening an issue, a Pull Request, or contacting the\n[team](mailto:miguel@subvisual.com) directly\n\nIf you found a bug, please open an issue. You can also open a PR for bugs or new\nfeatures. PRs will be reviewed and subject to our style guide and linters.\n\n# About\n\n`Fsmx` is maintained by [Subvisual](http://subvisual.com).\n\n[\u003cimg alt=\"Subvisual logo\" src=\"https://raw.githubusercontent.com/subvisual/guides/master/github/templates/subvisual_logo_with_name.png\" width=\"350px\" /\u003e](https://subvisual.com)\n","funding_links":[],"categories":["Elixir"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsubvisual%2Ffsmx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsubvisual%2Ffsmx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsubvisual%2Ffsmx/lists"}