{"id":18030587,"url":"https://github.com/edisonywh/gearbox","last_synced_at":"2025-04-05T00:07:31.652Z","repository":{"id":35117273,"uuid":"208860673","full_name":"edisonywh/gearbox","owner":"edisonywh","description":"⚙️ Gearbox is a functional state machine with an easy-to-use API, inspired by both Fsm and Machinery","archived":false,"fork":false,"pushed_at":"2022-08-17T22:30:36.000Z","size":38,"stargazers_count":178,"open_issues_count":2,"forks_count":9,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-04-20T12:26:14.977Z","etag":null,"topics":["elixir","gearbox","state-machine","state-transitions"],"latest_commit_sha":null,"homepage":"","language":"Elixir","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/edisonywh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-09-16T17:42:03.000Z","updated_at":"2024-03-21T14:01:17.000Z","dependencies_parsed_at":"2022-08-08T05:01:41.421Z","dependency_job_id":null,"html_url":"https://github.com/edisonywh/gearbox","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edisonywh%2Fgearbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edisonywh%2Fgearbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edisonywh%2Fgearbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edisonywh%2Fgearbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edisonywh","download_url":"https://codeload.github.com/edisonywh/gearbox/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247266563,"owners_count":20910836,"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":["elixir","gearbox","state-machine","state-transitions"],"created_at":"2024-10-30T09:14:39.371Z","updated_at":"2025-04-05T00:07:31.634Z","avatar_url":"https://github.com/edisonywh.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gearbox\n\nGearbox is a functional state machine with an easy-to-use API, inspired by both\n[Fsm](https://github.com/sasa1977/fsm) and [Machinery](https://github.com/joaomdmoura/machinery).\n\nGearbox does not run in a process, so there's no potential for a GenServer bottleneck.\nThis way there's also less overhead as you won't need to setup a supervision tree/manage your state machine processes.\n\n\u003e Note: Gearbox is heavily inspired by [Machinery](https://github.com/joaomdmoura/machinery),\n\u003e and also took inspiration from [Fsm](https://github.com/sasa1977/fsm).\n\nGearbox is **very** similar to [Machinery](https://github.com/joaomdmoura/machinery) in term of\nthe API usage, however it differs in the ways below:\n\n- **Gearbox does not use a GenServer as a backing process**.\n  Since GenServer can be a potential bottleneck in a system, for that reason I think it's best\n  to leave process management to users of the library.\n- **No before/after callbacks**. Callback allow you to add side effects, but side effects violate\n  Single Responsibility Principle, and that can bring surprises to your codebase\n  (e.g: \"How come everytime this transition happens, X happens?\"). Gearbox nudges you to keep domain-logic\n  callbacks close to your contexts/domain events. Gearbox still ships with a `guard_transition/3` callback,\n  as that is intrinsic to state machines.\n- Gearbox does not ship with a `Phoenix Dashboard` view.\n  A really cool and great concept, but more often than not it is not needed and the added dependency\n  can prove more trouble than worth.\n\nFor a more detailed documentation, checkout the [Gearbox's HexDoc](https://hexdocs.pm/gearbox).\n\n## Installation\n\nGet the latest version from [Hex](https://hex.pm/packages/gearbox)\n\n```elixir\ndef deps do\n  [\n    {:gearbox, \"~\u003e 0.3.1\"}\n  ]\nend\n```\n\n## Usage\n\nGearbox's main API is `Gearbox.transition/3`. There's a `bang!` variant available too.\n\n### Example\n\n`Gearbox.transitions(%Order{}, PaymentMachine, \"paid\")`\n\n- **First Argument** - an Elixir map, can be a struct or a non-struct.\n- **Second Argument** - a State Machine, read on to find out how to create a state machine.\n- **Third Argument** - the desired next state.\n\nHere's how to create a state machine:\n\n```elixir\ndefmodule PaymentMachine do\n  use Gearbox,\n    field: :status, # used to retrieve the state of the given struct. Defaults to `:state`\n    states: ~w(pending_payment paid refunded), # list of finite states in the state machine\n    initial: \"pending_payment\", # initial state of the struct, if struct has `nil` state to begin with. Defaults to the first item of `:states`\n    transitions: %{\n      \"pending_payment\" =\u003e \"paid\",\n      \"paid\" =\u003e \"refunded\",\n    } # a map of possible transitions from `current_state` to `next_state`. `*` wildcard is allowed to indicate any states.\nend\n```\n\n## Rationale\n\nGearbox operates on the philosophy that it acts purely as a functional state machine, wherein\nit does not care where your state is store (e.g: Ecto, GenServer), all Gearbox does is to help you\nensure state transitions happen the way you expect it to.\n\nIn most cases like for example `Order`, it is very likely that you don't need a process for that.\nJust get the record out of the database, run it through Gearbox machine, then persist it back to database.\n\nIn some rare cases where you need to have a stateful state machine, for example a traffic light\nthat has an internal timer to shift from `red` (30s) -\u003e `green` (30s) -\u003e `yellow` (5s) -\u003e `red`,\nyou are better off to use an `Agent`/`GenServer` where you have better control over backpressuring/\nbusiness logics.\n\nAs of now, Gearbox does not provide a way to create `events/actions` in a state machine.\nThis is because Gearbox is not a domain/context wrapper, events and actions that can\ntrigger a state change should reside closer to your contexts, therefore I urge users to\ngroup these events as domain events (contexts), rather than state machine events.\n\nGearbox previously shipped with `before_transition/3` and `after_transition/3` in `0.1.0`,\nbut after some discussions I have decided to take a deliberate decision to **remove** callbacks.\nThis is because callbacks by nature, allow you to add side effects, but side effects violate\n**Single Responsibility Principle**, and callbacks can often bring unintended surprises\nto your codebase (e.g: \"How come everytime this transition happens, X happens?\").\n\nTherefore, Gearbox nudges you to keep domain/business-logic callbacks close to your contexts/domain events.\nGearbox still ships with a `guard_transition/3` callback, as that is intrinsic to state machines.\n\n## Features\nBelow lists a couple of features that Gearbox currently have.\n\n### State Transitions\nThe core of Gearbox. Allows you to transition a state from one to another (managed by your own machine).\n\n```elixir\ndefmodule Commerce do\n  def pay(user, order) do\n    # ...\n    # Your payment logic\n    {:ok, updated_order} = Gearbox.transition(order, PaymentMachine, \"paid\")\n    # ...\n  end\nend\n```\n\nThere's also a `bang!` variant of transition, `Gearbox.transition!/3`, so you can rewrite your code to like so:\n\n```elixir\ndefmodule Commerce do\n  def pay(user, order) do\n    # ...\n    # Your payment logic\n    order\n    |\u003e Gearbox.transition!(PaymentMachine, \"paid\")\n    |\u003e Repo.insert!\n  end\nend\n```\n\n### Guard Transitions\nGuard transitions enforces a condition to be passed before a transitions is committed.\n\nA transition is halted if the function returns `{:halt, reason}`, it continues otherwise.\nThe reason giving in `{:halt, reason}` will then propagate up to `Gearbox.transition/3`\nas `{:error, reason}`.\n\n```elixir\n# You can add condition check on both `from` and `to` states.\ndef guard_transition(struct, _from, _to) do\n  case :rand.uniform() do\n    val when val \u003e= 0.5 -\u003e\n      # You can return anything\n    _ -\u003e\n      {:halt, \"You have been snapped.\"}\n  end\nend\n```\n\n\u003e **Note** that guard transitions will only be run if transition is valid.\n\n### Ecto Support\nIf your project uses Ecto, you can use the `Gearbox.Ecto` module to create a changeset based on the outcome of the transition. The original struct will not be modified.  \nA successful transition will return a changeset with the change applied, and an unsuccessful one will return a changeset with the appropriate error message.\n\n````elixir\n# similar to the above example\ndef pay(user, order) do\n  with {:ok, changeset} \u003c- Gearbox.Ecto.transition_changeset(order, PaymentMachine, \"paid\")\n  do\n    Repo.update(changeset)\n  else\n    {:error, changeset} -\u003e\n      # handle error here\n  end\nend\n````\n\n## Contributions\nContributions are very welcomed, but please first [open an issue](https://github.com/edisonywh/gearbox/issues/new) so we can align and discuss before any development begins.\n\n## License\n\nView [License](https://github.com/edisonywh/gearbox/blob/master/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedisonywh%2Fgearbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedisonywh%2Fgearbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedisonywh%2Fgearbox/lists"}