Current section

Files

Jump to
cqrs_tools lib cqrs command_validation.ex
Raw

lib/cqrs/command_validation.ex

defmodule Cqrs.CommandValidation do
@moduledoc """
Defines validation functions for a validated command.
## Example
defmodule CreateUser do
use Cqrs.Command
alias Cqrs.CommandValidation
field :email, :string
field :name, :string
field :id, :binary_id, internal: true
derive_event UserCreated
@impl true
def handle_validate(command, _opts) do
Ecto.Changeset.validate_format(command, :email, ~r/@/)
end
@impl true
def after_validate(%{email: email} = command) do
Map.put(command, :id, UUID.uuid5(:oid, email))
end
@impl true
def before_dispatch(command, _opts) do
command
|> CommandValidation.new()
|> CommandValidation.add(&ensure_uniqueness/1)
|> CommandValidation.run()
end
@impl true
def handle_dispatch(_command, _opts) do
{:ok, :dispatched}
end
defp ensure_uniqueness(%{id: id}) do
if Repo.exists?(from u in User, where: u.id == ^id),
do: {:error, "user already exists"},
else: :ok
end
end
"""
@type command :: map()
@type t :: %__MODULE__{command: command(), validations: list()}
@type validation_function ::
(command() -> any() | {:error, any()})
| (command(), keyword() -> any() | {:error, any()})
defstruct [:command, validations: []]
@doc """
Creates a new `Cqrs.CommandValidation` struct.
"""
@spec new(command()) :: t()
def new(command), do: %__MODULE__{command: command}
@doc """
Adds a `validation_function` to the list of validations to run.
"""
@spec add(t(), validation_function()) :: t()
def add(%__MODULE__{validations: validations} = validation, fun)
when is_function(fun, 1) or is_function(fun, 2) do
%{validation | validations: [fun | validations]}
end
@doc """
Runs the list of 'validation_function' functions
"""
@spec run(t(), keyword()) :: {:ok, command()} | {:error, list()}
def run(%__MODULE__{command: command} = validation, opts \\ []) do
case collect_errors(validation, opts) do
[] ->
{:ok, command}
errors ->
{:error,
errors
|> Keyword.get_values(:error)
|> Enum.sort()}
end
end
defp collect_errors(%{command: command, validations: validations}, opts) do
stream_opts = [
on_timeout: :kill_task,
timeout: Keyword.get(opts, :timeout, 5000),
max_concurrency: Keyword.get(opts, :max_concurrency, System.schedulers_online())
]
run_validation = fn
fun when is_function(fun, 1) -> fun.(command)
fun when is_function(fun, 2) -> fun.(command, opts)
end
validations
|> Task.async_stream(run_validation, stream_opts)
|> Stream.map(&elem(&1, 1))
|> Stream.map(fn
:timeout -> {:error, :timeout}
other -> other
end)
|> Enum.filter(&match?({:error, _}, &1))
end
end