All Projects → joaomdmoura → Machinery

joaomdmoura / Machinery

Licence: apache-2.0
State machine thin layer for structs (+ GUI for Phoenix apps)

Programming Languages

elixir
2628 projects

Projects that are alternatives of or similar to Machinery

Little State Machine
📠 React custom hook for persist state management
Stars: ✭ 654 (+78.2%)
Mutual labels:  state-machine, state-management, state
Kaffy
Powerfully simple admin package for phoenix applications
Stars: ✭ 617 (+68.12%)
Mutual labels:  ecto, phoenix, dashboard
Phoenix live dashboard
Realtime dashboard with metrics, request logging, plus storage, OS and VM insights
Stars: ✭ 1,657 (+351.5%)
Mutual labels:  ecto, phoenix, dashboard
Hal
🔴 A non-deterministic finite-state machine for Android & JVM that won't let you down
Stars: ✭ 63 (-82.83%)
Mutual labels:  state-machine, machine, state
Xstate
State machines and statecharts for the modern web.
Stars: ✭ 18,300 (+4886.38%)
Mutual labels:  state-machine, state-management, state
tstate-machine
TypeScript implementation of State Manager(like StateMachine)
Stars: ✭ 20 (-94.55%)
Mutual labels:  state-management, state-machine, state
xstate
State machines and statecharts for the modern web.
Stars: ✭ 21,286 (+5700%)
Mutual labels:  state-management, state-machine, state
xstate-viz
Visualizer for XState machines
Stars: ✭ 274 (-25.34%)
Mutual labels:  state-management, state-machine, state
atomic-state
A decentralized state management library for React
Stars: ✭ 54 (-85.29%)
Mutual labels:  state-management, state
one plus n detector
Elixir library to help you detect 1+n queries in applications using Ecto
Stars: ✭ 20 (-94.55%)
Mutual labels:  phoenix, ecto
Nanny-State
simple state management
Stars: ✭ 68 (-81.47%)
Mutual labels:  state-management, state
Pvm
Build workflows, activities, BPMN like processes, or state machines with PVM.
Stars: ✭ 348 (-5.18%)
Mutual labels:  state-machine, state-management
statebot
Write more robust and understandable programs. Statebot hopes to make Finite State Machines a little more accessible.
Stars: ✭ 24 (-93.46%)
Mutual labels:  state-management, state-machine
StateMachine system for Godot
Flexible and lightweight StateMachine for Godot
Stars: ✭ 19 (-94.82%)
Mutual labels:  machine, state
ecto generator
Generate Ecto schemas from existing database in Phoenix - Elixir
Stars: ✭ 20 (-94.55%)
Mutual labels:  phoenix, ecto
k-ramel
State manager for your components apps, the safe and easy way
Stars: ✭ 20 (-94.55%)
Mutual labels:  state-management, state
aper
A Rust data structure library built on state machines.
Stars: ✭ 100 (-72.75%)
Mutual labels:  state-machine, state
react-wisteria
Managing the State with the Golden Path
Stars: ✭ 18 (-95.1%)
Mutual labels:  state-management, state
SimpleStateMachineLibrary
📚 A simple library for realization state machines in C# code
Stars: ✭ 30 (-91.83%)
Mutual labels:  state-machine, state
alveron-old
Opinionated Elm-inspired Redux Component Architecture for React
Stars: ✭ 17 (-95.37%)
Mutual labels:  state-management, state

Machinery

Build Status Source Level Coverage Status

Machinery

Machinery is a thin State Machine library for Elixir that integrates with Phoenix out of the box.

It's just a small layer that provides a DSL for declaring states and having guard clauses + callbacks for structs in general.

Do you always need a state machine to be a process?

Yes? This is not your library. You might be better off with another library or even gen_statem or gen_fsm from Erlang/OTP.

Don't forget to check the Machinery Docs

Installing

The package can be installed by adding machinery to your list of dependencies in mix.exs:

def deps do
  [
    {:machinery, "~> 1.0.0"}
  ]
end

Create a field state (or a name of your choice to be defined later) for the module you want to have a state machine, make sure you have declared it as part of you defstruct, or if it is a Phoenix model make sure you add it to the schema, as a string, and to the changeset/2:

defmodule YourProject.User do
  schema "users" do
    # ...
    field :state, :string
    # ...
  end

  def changeset(%User{} = user, attrs) do
    #...
    |> cast(attrs, [:state])
    #...
  end
end

Declaring States

Declare the states as an argument when importing Machinery on the module that will control your states transitions.

It's strongly recommended that you create a new module for your State Machine logic. So let's say you want to add it to your User model, you should create a UserStateMachine module to hold your State Machine logic.

Machinery expects a Keyword as argument with the keys field, states and transitions.

  • field: An atom of your state field name (defaults to state)
  • states: A List of Strings representing each state.
  • transitions: A Map for each state and it allowed next state(s).

Example

defmodule YourProject.UserStateMachine do
  use Machinery,
    # This is a way to define a custom field, if not defined
    # it will expect the default `state` field in the struct
    field: :custom_state_name,
    # The first state declared will be considered
    # the initial state.
    states: ["created", "partial", "complete", "canceled"],
    transitions: %{
      "created" =>  ["partial", "complete"],
      "partial" => "complete",
      "*" => "canceled"
    }
end

As you might notice you can use wildcards "*" to declare a transition that can happen from any state to a specific one.

Changing States

To transit a struct into another state, you just need to call Machinery.transition_to/3.

Machinery.transition_to/3

It takes three arguments:

  • struct: The struct you want to transit to another state.
  • state_machine_module: The module that holds the state machine logic, where Machinery as imported.
  • next_event: string of the next state you want the struct to transition to.

Guard functions, before and after callbacks will be checked automatically.

Machinery.transition_to(your_struct, YourStateMachine, "next_state")
# {:ok, updated_struct}

Example:

user = Accounts.get_user!(1)
Machinery.transition_to(user, UserStateMachine, "complete")

Persist State

To persist the struct and the state transition automatically, instead of having Machinery changing the struct itself, you can declare a persist/2 function on the state machine module.

It will receive the unchanged struct as the first argument and a string of the next state as the second one, after every state transition. That will be called between the before and after transition callbacks.

persist/2 should always return the updated struct.

Example:

defmodule YourProject.UserStateMachine do
  alias YourProject.Accounts

  use Machinery,
    states: ["created", "complete"],
    transitions: %{"created" => "complete"}

  def persist(struct, next_state) do
    # Updating a user on the database with the new state.
    {:ok, user} = Accounts.update_user(struct, %{state: next_state})
    user
  end
end

Logging Transitions

To log/persist the transitions itself Machinery provides a callback log_transitions/2 that will be called on every transition.

It will receive the unchanged struct as the first argument and a string of the next state as the second one, after every state transition. This function will be called between the before and after transition callbacks and after the persist function.

log_transition/2 should always return the updated struct.

Example:

defmodule YourProject.UserStateMachine do
  alias YourProject.Accounts

  use Machinery,
    states: ["created", "complete"],
    transitions: %{"created" => "complete"}

  def log_transition(struct, _next_state) do
    # Log transition here, save on the DB or whatever.
    # ...
    # Return the struct.
    struct
  end
end

Guard functions

Create guard conditions by adding signatures of the guard_transition/2 function, it will receive two arguments, the struct and an string of the state it will transit to, use this second argument to pattern matching the desired state you want to guard.

# The second argument is used to pattern match into the state
# and guard the transition to it.
def guard_transition(struct, "guarded_state") do
 # Your guard logic here
end

Guard conditions will allow the transition if it returns anything other than a tuple with {:error, "cause"}:

  • {:error, "cause"}: Transition won't be allowed.
  • _ (anything else): Guard clause will allow the transition.

Example:

defmodule YourProject.UserStateMachine do
  use Machinery,
    states: ["created", "complete"],
    transitions: %{"created" => "complete"}

  # Guard the transition to the "complete" state.
  def guard_transition(struct, "complete") do
    if Map.get(struct, :missing_fields) == true do
      {:error, "There are missing fields"}
    end
  end
end

When trying to transition an struct that is blocked by its guard clause you will have the following return:

blocked_struct = %TestStruct{state: "created", missing_fields: true}
Machinery.transition_to(blocked_struct, TestStateMachineWithGuard, "completed")

# {:error, "There are missing fields"}

Before and After callbacks

You can also use before and after callbacks to handle desired side effects and reactions to a specific state transition.

You can just declare before_transition/2 and after_transition/2, pattern matching the desired state you want to.

Make sure Before and After callbacks should return the struct.

# callbacks should always return the struct.
def before_transition(struct, "state"), do: struct
def after_transition(struct, "state"), do: struct

Example:

defmodule YourProject.UserStateMachine do
  use Machinery,
    states: ["created", "partial", "complete"],
    transitions: %{
      "created" =>  ["partial", "complete"],
      "partial" => "completed"
    }

    def before_transition(struct, "partial") do
      # ... overall desired side effects
      struct
    end

    def after_transition(struct, "completed") do
      # ... overall desired side effects
      struct
    end
end
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].