Packages
cqrs_tools
0.2.13
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.1
0.1.0
A collection of handy Elixir macros for CQRS applications.
Current section
Files
Jump to
Current section
Files
lib/cqrs/command.ex
defmodule Cqrs.Command do
alias Cqrs.Command.CommandState
alias Cqrs.{Command, CommandError, Documentation, DomainEvent, Guards}
@moduledoc """
The `Command` macro allows you to define a command that encapsulates a struct definition,
data validation, dependency validation, and dispatching of the command.
## Options
* `require_all_fields` - If `true`, all fields will be required. Defaults to `true`
* `dispatcher` - A module that defines a `dispatch/2`.
## Examples
defmodule CreateUser do
use Cqrs.Command
field :email, :string
field :name, :string
field :id, :binary_id, internal: true
@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 handle_dispatch(_command, _opts) do
{:ok, :dispatched}
end
end
### Creation
iex> CreateUser.new()
#CreateUser<errors: %{email: [\"can't be blank\"], name: [\"can't be blank\"]}>
iex> CreateUser.new(email: "chris@example.com", name: "chris")
#CreateUser<%{email: \"chris@example.com\", id: \"052c1984-74c9-522f-858f-f04f1d4cc786\", name: \"chris\"}>
iex> %{id: "052c1984-74c9-522f-858f-f04f1d4cc786"} = CreateUser.new!(email: "chris@example.com", name: "chris")
### Dispatching
iex> {:error, {:invalid_command, state}} =
...> CreateUser.new(name: "chris", email: "wrong")
...> |> CreateUser.dispatch()
...> state.errors
%{email: ["has invalid format"]}
iex> CreateUser.new(name: "chris", email: "chris@example.com")
...> |> CreateUser.dispatch()
{:ok, :dispatched}
## Event derivation
You can derive [events](`Cqrs.DomainEvent`) directly from a command.
see `derive_event/2`
defmodule DeactivateUser do
use Cqrs.Command
field :id, :binary_id
derive_event UserDeactivated
end
## Usage with `Commanded`
defmodule Commanded.Application do
use Commanded.Application,
otp_app: :my_app,
default_dispatch_opts: [
consistency: :strong,
returning: :execution_result
],
event_store: [
adapter: Commanded.EventStore.Adapters.EventStore,
event_store: MyApp.EventStore
]
end
defmodule DeactivateUser do
use Cqrs.Command, dispatcher: Commanded.Application
field :id, :binary_id
derive_event UserDeactivated
end
iex> {:ok, event} = DeactivateUser.new(id: "052c1984-74c9-522f-858f-f04f1d4cc786")
...> |> DeactivateUser.dispatch()
...> %{id: event.id, version: event.version}
%{id: "052c1984-74c9-522f-858f-f04f1d4cc786", version: 1}
"""
@type command :: struct()
@doc """
Allows one to define any custom data validation aside from casting and requiring fields.
This callback is optional.
Invoked when the `new()` or `new!()` function is called.
"""
@callback handle_validate(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
@doc """
Allows one to modify the fully validated command. The changes to the command are validated again after this callback.
This callback is optional.
Invoked after the `handle_validate/2` callback is called.
"""
@callback after_validate(command()) :: command()
@doc """
This callback is intended to be used as a last chance to do any validation that performs IO.
This callback is optional.
Invoked before `handle_dispatch/2`.
"""
@callback before_dispatch(command(), keyword()) :: {:ok, command()} | {:error, any()}
@doc """
This callback is intended to be used to run the fully validated command.
This callback is required.
"""
@callback handle_dispatch(command(), keyword()) :: {:ok, any} | {:error, any()}
defmacro __using__(opts \\ []) do
require_all_fields = Keyword.get(opts, :require_all_fields, true)
quote location: :keep do
Module.put_attribute(__MODULE__, :require_all_fields, unquote(require_all_fields))
Module.put_attribute(__MODULE__, :dispatcher, Keyword.get(unquote(opts), :dispatcher))
Module.register_attribute(__MODULE__, :events, accumulate: true)
Module.register_attribute(__MODULE__, :schema_fields, accumulate: true)
Module.register_attribute(__MODULE__, :required_fields, accumulate: true)
import Command, only: [field: 2, field: 3, derive_event: 1, derive_event: 2]
@behaviour Command
@before_compile Command
@after_compile Command
def handle_validate(command, _opts), do: command
def after_validate(command), do: command
def before_dispatch(command, _opts), do: {:ok, command}
defoverridable handle_validate: 2, before_dispatch: 2, after_validate: 1
end
end
defmacro __before_compile__(_env) do
quote location: :keep do
Command.__schema__()
Command.__introspection__()
Command.__constructor__()
Command.__dispatch__()
Command.__events__()
Module.delete_attribute(__MODULE__, :events)
Module.delete_attribute(__MODULE__, :schema_fields)
Module.delete_attribute(__MODULE__, :required_fields)
Module.delete_attribute(__MODULE__, :require_all_fields)
end
end
defmacro __after_compile__(_env, _bytecode) do
quote location: :keep do
if @dispatcher, do: Guards.ensure_is_dispatcher!(@dispatcher)
end
end
defmacro __schema__ do
quote generated: true, location: :keep do
use Ecto.Schema
if Code.ensure_loaded?(Jason), do: @derive(Jason.Encoder)
@primary_key false
embedded_schema do
Ecto.Schema.field(:created_at, :utc_datetime)
Enum.map(@schema_fields, fn
{name, :binary_id, opts} ->
Ecto.Schema.field(name, Ecto.UUID, opts)
{name, type, opts} ->
Ecto.Schema.field(name, type, opts)
end)
end
end
end
defmacro __introspection__ do
quote do
require Documentation
@field_docs Documentation.field_docs("Fields", @schema_fields, @required_fields)
@name __MODULE__ |> Module.split() |> Enum.reverse() |> hd() |> to_string()
def __fields__, do: @schema_fields
def __field_docs__, do: @field_docs
def __module_docs__, do: @moduledoc
def __command__, do: __MODULE__
def __name__, do: @name
end
end
defmacro __constructor__ do
quote generated: true, location: :keep do
# @spec new(maybe_improper_list() | map(), maybe_improper_list()) :: CommandState.t()
# @spec new!(maybe_improper_list() | map(), maybe_improper_list()) :: %__MODULE__{}
@doc """
Creates a new `#{__MODULE__} command.`
#{@field_docs}
"""
def new(attrs \\ [], opts \\ []) when is_list(opts),
do: Command.__new__(__MODULE__, attrs, @required_fields, opts)
@doc """
Creates a new `#{__MODULE__} command.`
#{@field_docs}
"""
def new!(attrs \\ [], opts \\ []) when is_list(opts),
do: Command.__new__!(__MODULE__, attrs, @required_fields, opts)
end
end
defmacro __dispatch__ do
quote location: :keep do
def dispatch(command, opts \\ []) do
Command.__do_dispatch__(__MODULE__, command, opts)
end
if @dispatcher do
def handle_dispatch(%__MODULE__{} = cmd, opts) do
@dispatcher.dispatch(cmd, opts)
end
end
end
end
defmacro __events__ do
quote location: :keep do
command_fields = Enum.map(@schema_fields, &elem(&1, 0))
Enum.map(@events, fn {name, opts} ->
options =
Keyword.update(opts, :with, command_fields, fn fields ->
fields
|> List.wrap()
|> Kernel.++(command_fields)
|> Enum.uniq()
end)
defmodule name do
use DomainEvent, options
end
end)
end
end
@doc """
Defines a command field.
* `:name` - any `atom`
* `:type` - any valid [Ecto Schema](`Ecto.Schema`) type
* `:opts` - any valid [Ecto Schema](`Ecto.Schema`) field options. Plus:
* `:required` - `true | false`. Defaults to the `require_all_fields` option.
* `:internal` - `true | false`. If `true`, this field is meant to be used internally. If `true`, the required option will be set to `false` and the field will be hidden from documentation.
* `:description` - Documentation for the field.
"""
@spec field(name :: atom(), type :: atom(), keyword()) :: any()
defmacro field(name, type, opts \\ []) do
quote location: :keep do
required =
case Keyword.get(unquote(opts), :internal, false) do
true ->
false
false ->
required = Keyword.get(unquote(opts), :required, @require_all_fields)
if required, do: @required_fields(unquote(name))
required
end
opts = Keyword.put(unquote(opts), :required, required)
@schema_fields {unquote(name), unquote(Macro.escape(type)), opts}
end
end
@doc """
Generates an [event](`Cqrs.DomainEvent`) based on the fields defined in the [command](`Cqrs.Command`).
Accepts all the options that [DomainEvent](`Cqrs.DomainEvent`) accepts.
"""
defmacro derive_event(name, opts \\ []) do
quote location: :keep do
[_command_name | namespace] =
__MODULE__
|> Module.split()
|> Enum.reverse()
namespace =
namespace
|> Enum.reverse()
|> Module.concat()
name = Module.concat(namespace, unquote(name))
@events {name, unquote(opts)}
end
end
alias Ecto.Changeset
def __init__(mod, attrs, required_fields, opts) do
fields = mod.__schema__(:fields)
struct(mod)
|> Changeset.cast(normalize(attrs), fields)
|> Changeset.validate_required(required_fields)
|> mod.handle_validate(opts)
end
def __new__(mod, attrs, required_fields, opts) when is_list(opts) do
mod
|> __init__(attrs, required_fields, opts)
|> Changeset.put_change(:created_at, DateTime.utc_now())
|> CommandState.new()
|> case do
%{valid?: true} = state ->
attrs =
state
|> CommandState.apply_changes()
|> mod.after_validate()
mod
|> __init__(attrs, required_fields, opts)
|> CommandState.merge(state)
state ->
state
end
end
def __new__!(mod, attrs, required_fields, opts \\ []) when is_list(opts) do
case __new__(mod, attrs, required_fields, opts) |> CommandState.apply() do
{:ok, command} -> command
{:error, command} -> raise CommandError, command: command
end
end
defp normalize(values) when is_list(values), do: Enum.into(values, %{})
defp normalize(values) when is_struct(values), do: Map.from_struct(values)
defp normalize(values) when is_map(values), do: values
def __do_dispatch__(mod, %CommandState{changeset: changeset} = state, opts) do
case changeset do
%{valid?: false} ->
{:error, {:invalid_command, state}}
_ ->
command = Ecto.Changeset.apply_changes(changeset)
__do_dispatch__(mod, command, opts)
end
end
def __do_dispatch__(mod, command, opts) do
with {:ok, command} <- mod.before_dispatch(command, opts) do
mod.handle_dispatch(command, opts)
end
end
end