DEV Community

Cover image for Simple Elixir State Machine for Validation
Turbo Panumarch
Turbo Panumarch

Posted on

Simple Elixir State Machine for Validation

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"}
Enter fullscreen mode Exit fullscreen mode

πŸ€– 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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

⚑️ 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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)