{"id":13507294,"url":"https://github.com/joaomdmoura/machinery","last_synced_at":"2025-05-15T07:07:04.321Z","repository":{"id":13182374,"uuid":"73817045","full_name":"joaomdmoura/machinery","owner":"joaomdmoura","description":"Elixir State machine thin layer for structs","archived":false,"fork":false,"pushed_at":"2024-05-03T08:18:53.000Z","size":534,"stargazers_count":544,"open_issues_count":12,"forks_count":53,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-05-12T09:13:53.023Z","etag":null,"topics":["dashboard","ecto","elixir","elixir-lang","machine","machinery","phoenix","state","state-machine","state-management","statemachine"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joaomdmoura.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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}},"created_at":"2016-11-15T13:38:45.000Z","updated_at":"2025-04-05T17:54:41.000Z","dependencies_parsed_at":"2024-06-21T16:50:32.949Z","dependency_job_id":null,"html_url":"https://github.com/joaomdmoura/machinery","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaomdmoura%2Fmachinery","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaomdmoura%2Fmachinery/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaomdmoura%2Fmachinery/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joaomdmoura%2Fmachinery/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joaomdmoura","download_url":"https://codeload.github.com/joaomdmoura/machinery/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254292042,"owners_count":22046426,"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":["dashboard","ecto","elixir","elixir-lang","machine","machinery","phoenix","state","state-machine","state-management","statemachine"],"created_at":"2024-08-01T02:00:30.262Z","updated_at":"2025-05-15T07:06:59.295Z","avatar_url":"https://github.com/joaomdmoura.png","language":"Elixir","funding_links":[],"categories":["Algorithms and Data structures"],"sub_categories":[],"readme":"# Machinery\n\n[![Build Status](https://circleci.com/gh/joaomdmoura/machinery.svg?style=svg)](https://circleci.com/gh/circleci/circleci-docs)\n[![Module Version](https://img.shields.io/hexpm/v/machinery.svg)](https://hex.pm/packages/machinery)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/machinery/)\n[![Total Download](https://img.shields.io/hexpm/dt/machinery.svg)](https://hex.pm/packages/machinery)\n[![License](https://img.shields.io/hexpm/l/machinery.svg)](https://github.com/joaomdmoura/machinery/blob/master/LICENSE)\n\n![Machinery](./assets/logo.png)\n\nMachinery is a lightweight State Machine library for Elixir with built-in \nPhoenix integration. \nIt provides a simple DSL for declaring states and includes support for guard \nclauses and callbacks.\n\n## Table of Contents\n- [Installing](#installing)\n- [Declaring States](#declaring-states)\n- [Changing States](#changing-states)\n- [Persist State](#persist-state)\n- [Logging Transitions](#logging-transitions)\n- [Guard Functions](#guard-functions)\n- [Before and After Callbacks](#before-and-after-callbacks)\n\n## Installing\n\nAdd `:machinery` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:machinery, \"~\u003e 1.1.0\"}\n  ]\nend\n```\n\nCreate a `state` field (or a custom name) for the module you want to apply a \nstate machine to, and ensure it's declared as part of your defstruct.\n\nIf using a Phoenix model, add it to the schema as a `string` and include it in \nthe `changeset/2` function:\n\n```elixir\ndefmodule YourProject.User do\n  schema \"users\" do\n    # ...\n    field :state, :string\n    # ...\n  end\n\n  def changeset(%User{} = user, attrs) do\n    #...\n    |\u003e cast(attrs, [:state])\n    #...\n  end\nend\n```\n\n## Declaring States\n\nCreate a separate module for your State Machine logic.\nFor example, if you want to add a state machine to your `User` model, create a\n`UserStateMachine` module.\n\nThen import `Machinery` in this new module and declare states as arguments.\n\nMachinery expects a `Keyword` as an argument with the keys `field`, `states` \nand `transitions`.\n\n- `field`: An atom representing your state field name (defaults to `state`)\n- `states`: A `List` of strings representing each state.\n- `transitions`: A Map for each state and its allowed next state(s).\n\n### Example\n\n```elixir\ndefmodule YourProject.UserStateMachine do\n  use Machinery,\n    field: :custom_state_name, # Optional, default value is `:field`\n    states: [\"created\", \"partial\", \"completed\", \"canceled\"],\n    transitions: %{\n      \"created\" =\u003e  [\"partial\", \"completed\"],\n      \"partial\" =\u003e \"completed\",\n      \"*\" =\u003e \"canceled\"\n    }\nend\n```\n\nYou can use wildcards `\"*\"` to declare a transition that can happen from any \nstate to a specific one.\n\n## Changing States\n\nTo transition a struct to another state, call `Machinery.transition_to/3` or `Machinery.transition_to/4`.\n\n### `Machinery.transition_to/3` or ``Machinery.transition_to/4`\n\nIt takes the following arguments:\n\n- `struct`: The `struct` you want to transition to another state.\n- `state_machine_module`: The module that holds the state machine logic, where Machinery is imported.\n- `next_event`: `string` of the next state you want the struct to transition to.\n- *(optional)* `extra_metadata`: `map` with any extra data you might want to access on any of the sate machine functions triggered by the state change\n\n```elixir\nMachinery.transition_to(your_struct, YourStateMachine, \"next_state\")\n# {:ok, updated_struct}\n\n# OR\n\nMachinery.transition_to(your_struct, YourStateMachine, \"next_state\", %{extra: \"metadata\"})\n# {:ok, updated_struct}\n```\n\n### Example\n\n```elixir\nuser = Accounts.get_user!(1)\n{:ok, updated_user} = Machinery.transition_to(user, UserStateMachine, \"completed\")\n```\n\n## Persist State\n\nTo persist the struct and state transition, you declare a `persist/2` or `/3` *(in case you wanna access metadata passed on `transition_to/4`)*\nfunction in the state machine module. \n\nThis function will receive the unchanged `struct` as the first argument and a \n`string` of the next state as the second one.\n\n**your `persist/2` or `persist/3` should always return the updated struct.**\n\n### Example\n\n```elixir\ndefmodule YourProject.UserStateMachine do\n  alias YourProject.Accounts\n\n  use Machinery,\n    states: [\"created\", \"completed\"],\n    transitions: %{\"created\" =\u003e \"completed\"}\n  \n  # You can add an optional third argument for the extra metadata.\n  def persist(struct, next_state) do\n    # Updating a user on the database with the new state.\n    {:ok, user} = Accounts.update_user(struct, %{state: next_stated})\n    # `persist` should always return the updated struct\n    user\n  end\nend\n```\n\n## Logging Transitions\n\nTo log transitions, Machinery provides a `log_transition/2` or `/3` *(in case you wanna access metadata passed on `transition_to/4`)*\ncallback that is called on every transition, after the `persist` function is executed.\n\nThis function receives the unchanged `struct` as the first \nargument and a `string` of the next state as the second one. \n\n**`log_transition/2` or `log_transition/3` should always return the struct.**\n\n### Example\n\n```elixir\ndefmodule YourProject.UserStateMachine do\n  alias YourProject.Accounts\n\n  use Machinery,\n    states: [\"created\", \"completed\"],\n    transitions: %{\"created\" =\u003e \"completed\"}\n\n  # You can add an optional third argument for the extra metadata.\n  def log_transition(struct, _next_state) do\n    # Log transition here.\n    # ...\n    # `log_transition` should always return the struct\n    struct\n  end\nend\n```\n\n## Guard functions\n\nCreate guard conditions by adding `guard_transition/2` or `/3` *(in case you wanna access metadata passed on `transition_to/4`)* \nfunction signatures to the state machine module.\nThis function receives two arguments: the `struct` and a `string` of the state it \nwill transition to. \n\nUse the second argument for pattern matching the desired state you want to guard.\n\n```elixir\n# The second argument is used to pattern match into the state\n# and guard the transition to it.\n#\n# You can add an optional third argument for the extra metadata.\ndef guard_transition(struct, \"guarded_state\") do\n # Your guard logic here\nend\n```\n\nGuard conditions will allow the transition if it returns anything other than a tuple with `{:error, \"cause\"}`:\n  - `{:error, \"cause\"}`: Transition won't be allowed.\n  - `_` *(anything else)*: Guard clause will allow the transition.\n\n### Example\n\n```elixir\ndefmodule YourProject.UserStateMachine do\n  use Machinery,\n    states: [\"created\", \"completed\"],\n    transitions: %{\"created\" =\u003e \"completed\"}\n\n  # Guard the transition to the \"completed\" state.\n  def guard_transition(struct, \"completed\") do\n    if Map.get(struct, :missing_fields) == true do\n      {:error, \"There are missing fields\"}\n    end\n  end\nend\n```\n\nWhen trying to transition a struct that is blocked by its guard clause, \nyou will have the following return:\n\n```elixir\nblocked_struct = %TestStruct{state: \"created\", missing_fields: true}\nMachinery.transition_to(blocked_struct, TestStateMachineWithGuard, \"completed\")\n\n# {:error, \"There are missing fields\"}\n```\n\n## Before and After callbacks\n\nYou can also use before and after callbacks to handle desired side effects and \nreactions to a specific state transition.\n\nYou can declare `before_transition/2` or `/3` *(in case you wanna access metadata passed on `transition_to/4`)* \nand `after_transition/2` or `/3` *(in case you wanna access metadata passed on `transition_to/4`)*, \npattern matching the desired state you want to.\n\n**Before and After callbacks should return the struct.**\n\n```elixir\n# Before and After callbacks should return the struct.\n# You can add an optional third argument for the extra metadata.\ndef before_transition(struct, \"state\"), do: struct\ndef after_transition(struct, \"state\"), do: struct\n```\n\n### Example\n\n```elixir\ndefmodule YourProject.UserStateMachine do\n  use Machinery,\n    states: [\"created\", \"partial\", \"completed\"],\n    transitions: %{\n      \"created\" =\u003e  [\"partial\", \"completed\"],\n      \"partial\" =\u003e \"completed\"\n    }\n\n    def before_transition(struct, \"partial\") do\n      # ... overall desired side effects\n      struct\n    end\n\n    def after_transition(struct, \"completed\") do\n      # ... overall desired side effects\n      struct\n    end\nend\n```\n\n## Copyright and License\n\nCopyright (c) 2016 João M. D. Moura\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoaomdmoura%2Fmachinery","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoaomdmoura%2Fmachinery","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoaomdmoura%2Fmachinery/lists"}