loading...
Cover image for Simple Elixir State Machine for Validation

Simple Elixir State Machine for Validation

turboza profile image Turbo Panumarch ・2 min read

At some point, we will have an entity which the status can be transitioned to many possibilities and we want to have a rule to enforce an integrity of the data.

For example, this is a human emotion state change diagram.

state machine

And some transitions are okay, while some are not.

# 👌 OK
iex> update(%Human{emotion: :hungry}, %{emotion: :full})
{:ok, %Human{emotion: :full}}

# 🙅‍♂️ No, get something to eat first!
iex> update(%Human{emotion: :hungry}, %{emotion: :sleepy})
{:error, "invalid status change"}

🤖 Simple validation with pattern matching

For elixir developer, you can guess the pattern matching is pretty good for solving state change problem like this.

def update(%Human{} = human, attrs) do
  with :ok <- validate_status(human.emotion, attrs.emotion),
  ...
end
def validate_status(current_status, new_status) do
  case {current_status, new_status} do
    # Put all the transition paths as a whitelist
    {:hungry, :full} -> :ok
    {:hungry, :angry} -> :ok
    {:angry, :angry} -> :ok
    {:angry, :full} -> :ok
    {:full, :sleepy} -> :ok

    # return error for the rest
    _ -> {:error, "invalid status change"}
  end
end

⚡️ Can we embed this rules into Ecto changeset?

If we want the rule to be strictly applied to the ecto data schema, we can also build a custom changeset validation for status change as well.

def changeset(human, attrs \\ %{}) do
  human
  |> cast(...)
  |> validate_required(...)
  |> validate_status_change() # <- custom validation
end
def validate_status_change(changeset) do
  # Get current and new field data
  current_status = changeset.data.status
  new_status = get_field(changeset, :status)

  case {current_status, new_status} do
    # do nothing if ok
    {:hungry, :full} -> changeset
    {:hungry, :angry} -> changeset
    {:angry, :angry} -> changeset
    {:angry, :full} -> changeset
    {:full, :sleepy} -> changeset
    {nil, _} -> changeset # any new status is ok

    # add error to ecto changeset errors
    _ -> add_error(changeset, :status, "invalid status change")
  end
end

That's it! Hope this could give you some idea in your next task. Feel free to discuss if you have any comments! 😊

Posted on by:

Discussion

pic
Editor guide