Skip to content

Commit

Permalink
add custom type to command (#85)
Browse files Browse the repository at this point in the history
feat: add ability to pass custom type as identity and identifier
  • Loading branch information
yordis committed Jun 16, 2023
1 parent f271b09 commit 4562704
Show file tree
Hide file tree
Showing 15 changed files with 398 additions and 30 deletions.
58 changes: 57 additions & 1 deletion apps/one_piece_commanded/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,62 @@

## Unreleased

## v0.19.0 - 2023-06-15

`OnePiece.Commanded.ValueObject` implements `Ecto.Type`, it means that `cast/2`, `dump/2`, and `load/2` are
added to every module that uses `OnePiece.Commanded.ValueObject`. This will allow you to use
`OnePiece.Commanded.ValueObject` as a field in your `Ecto.Schema` and as custom type in your
`OnePiece.Commanded.Aggregate`, `OnePiece.Commanded.Command`, and `OnePiece.Commanded.Event`.

Added support for custom types for `OnePiece.Commanded.Aggregate`, `OnePiece.Commanded.Command`, and
`OnePiece.Commanded.Event`. This will allow you to have Custom aggregate identity. Read more about at
https://hexdocs.pm/commanded/commands.html#define-aggregate-identity under "Custom aggregate identity".

```elixir
defmodule AccountNumber do
use OnePiece.Commanded.ValueObject

embedded_schema do
field :account_number, :string
field :branch, :string
end

# You must implement `String.Chars` protocol in order to work when dispatching the Command.
defimpl String.Chars do
def to_string(%AccountNumber{branch: branch, account_number: account_number}) do
branch <> ":" <> account_number
end
end
end

defmodule DepositAccount do
use OnePiece.Commanded.Aggregate,
identifier: {:account_number, AccountNumber}

embedded_schema do
# ...
end
end

defmodule DepositAccountOpened do
use OnePiece.Commanded.Event,
aggregate_identifier: {:account_number, AccountNumber}

embedded_schema do
# ...
end
end

defmodule OpenDepositAccount do
use OnePiece.Commanded.Command,
aggregate_identifier: {:account_number, AccountNumber}

embedded_schema do
# ...
end
end
```

## v0.18.0 - 2023-05-15

- Added `OnePiece.Commanded.ignore_error/2`.
Expand All @@ -20,7 +76,7 @@

## v0.15.1 - 2022-12-24

- Fix `OnePiece.Commanded.Event` and ``OnePiece.Commanded.Entity` to extend from `OnePiece.Commanded.ValueObject`.
- Fix `OnePiece.Commanded.Event` and `OnePiece.Commanded.Entity` to extend from `OnePiece.Commanded.ValueObject`.

## v0.15.0 - 2022-12-23

Expand Down
25 changes: 19 additions & 6 deletions apps/one_piece_commanded/lib/one_piece/commanded/aggregate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ defmodule OnePiece.Commanded.Aggregate do
Defines "Aggregate" modules.
"""

@typedoc """
A struct that represents an aggregate.
"""
@type t :: struct()

@type event :: struct()
Expand All @@ -12,25 +15,35 @@ defmodule OnePiece.Commanded.Aggregate do
## Example
def apply(account, event) do
account
def apply(%MyAggregate{} = aggregate, %MyEvent{} = event) do
aggregate
|> Map.put(:name, event.name)
|> Map.put(:description, event.description)
end
"""
@callback apply(aggregate :: t(), event :: event()) :: t()

@doc """
Convert the module into a `Aggregate` behaviour.
Convert the module into a `Aggregate` behaviour and a `t:t/0`.
It adds an `apply/2` callback to the module as a fallback, return the aggregate as it is.
## Using
- `OnePiece.Commanded.Entity`
## Usage
defmodule Account do
use OnePiece.Commanded.Aggregate
use OnePiece.Commanded.Aggregate, identifier: :name
embedded_schema do
field :description, :string
end
@impl OnePiece.Commanded.Aggregate
def apply(account, event) do
account
def apply(%Account{} = aggregate, %AccountOpened{} = event) do
aggregate
|> Map.put(:name, event.name)
|> Map.put(:description, event.description)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,24 @@ defmodule OnePiece.Commanded.Aggregate.StatelessLifespan do

@behaviour Commanded.Aggregates.AggregateLifespan

@doc """
Stops the aggregate after a command.
iex> OnePiece.Commanded.Aggregate.StatelessLifespan.after_command(%MyCommandOne{})
:stop
"""
def after_command(_command), do: :stop

@doc """
Stops the aggregate after an event.
iex> OnePiece.Commanded.Aggregate.StatelessLifespan.after_event(%DepositAccountOpened{})
:stop
"""
def after_event(_event), do: :stop

@doc """
Stops the aggregate after an error.
iex> OnePiece.Commanded.Aggregate.StatelessLifespan.after_error({:error, :something_happened})
:stop
"""
def after_error(_error), do: :stop
end
47 changes: 41 additions & 6 deletions apps/one_piece_commanded/lib/one_piece/commanded/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ defmodule OnePiece.Commanded.Command do
Defines "Command" modules.
"""

@typedoc """
A struct that represents a command.
"""
@type t :: struct()

@typedoc """
The aggregate identifier key of the event.
If it's a tuple, the type must be a module that implements the `OnePiece.Commanded.ValueObject` module or [`Ecto` built-in types](https://hexdocs.pm/ecto/Ecto.Schema.html#module-types-and-casting)
"""
@type aggregate_identifier_opt ::
atom() | {key_name :: atom(), type :: atom()} | {key_name :: atom(), type :: module()}

@doc """
Converts the module into an `Ecto.Schema`.
Converts the module into an `t:t/0`.
## Using
It derives from `Jason.Encoder` and also adds some factory functions to create
structs.
- `OnePiece.Commanded.ValueObject`
## Usage
Expand All @@ -20,14 +32,37 @@ defmodule OnePiece.Commanded.Command do
# ...
end
end
You can also define a custom type as the aggregate identifier:
defmodule IdentityRoleId do
use OnePiece.Commanded.ValueObject
embedded_schema do
field :identity_id, :string
field :role_id, :string
end
end
defmodule AssignRole do
use OnePiece.Commanded.Command,
aggregate_identifier: {:id, IdentityRoleId}
embedded_schema do
# ...
end
end
"""
@spec __using__(opts :: [aggregate_identifier: atom()]) :: any()
@spec __using__(opts :: [aggregate_identifier: aggregate_identifier_opt()]) :: any()
defmacro __using__(opts \\ []) do
unless Keyword.has_key?(opts, :aggregate_identifier) do
raise ArgumentError, "missing :aggregate_identifier key"
end

aggregate_identifier = Keyword.fetch!(opts, :aggregate_identifier)
{aggregate_identifier, aggregate_identifier_type} =
opts
|> Keyword.fetch!(:aggregate_identifier)
|> OnePiece.Commanded.Helpers.get_primary_key()

quote do
use OnePiece.Commanded.ValueObject
Expand All @@ -37,7 +72,7 @@ defmodule OnePiece.Commanded.Command do
"""
@type aggregate_identifier_key :: unquote(aggregate_identifier)

@primary_key {unquote(aggregate_identifier), :string, autogenerate: false}
@primary_key {unquote(aggregate_identifier), unquote(aggregate_identifier_type), autogenerate: false}

@doc """
Returns the aggregate identifier key.
Expand Down
50 changes: 43 additions & 7 deletions apps/one_piece_commanded/lib/one_piece/commanded/entity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,29 @@ defmodule OnePiece.Commanded.Entity do
Defines "Entity" modules.
"""

@typedoc """
A struct that represents an entity.
"""
@type t :: struct()

@typedoc """
The identity of an entity.
"""
@type identity :: String.t()
@type identity :: any()

@typedoc """
The identity key of the entity.
If it's a tuple, the type must be a module that implements the `OnePiece.Commanded.ValueObject` module or [`Ecto` built-in types](https://hexdocs.pm/ecto/Ecto.Schema.html#module-types-and-casting)
"""
@type identifier_opt :: atom() | {key_name :: atom(), type :: atom()} | {key_name :: atom(), type :: module()}

@doc """
Converts the module into an `Ecto.Schema`.
Converts the module into an `t:t/0`.
## Using
It derives from `Jason.Encoder` and also adds some factory functions to create
structs.
- `OnePiece.Commanded.ValueObject`
## Usage
Expand All @@ -23,14 +36,37 @@ defmodule OnePiece.Commanded.Entity do
# ...
end
end
You can also define a custom type as the identifier:
defmodule IdentityRoleId do
use OnePiece.Commanded.ValueObject
embedded_schema do
field :identity_id, :string
field :role_id, :string
end
end
defmodule IdentityRole do
use OnePiece.Commanded.Entity,
identifier: {:id, IdentityRoleId}
embedded_schema do
# ...
end
end
"""
@spec __using__(opts :: [identifier: atom()]) :: any()
@spec __using__(opts :: [identifier: identifier_opt()]) :: any()
defmacro __using__(opts \\ []) do
unless Keyword.has_key?(opts, :identifier) do
raise ArgumentError, "missing :identifier key"
end

identifier = Keyword.fetch!(opts, :identifier)
{identifier, identifier_type} =
opts
|> Keyword.fetch!(:identifier)
|> OnePiece.Commanded.Helpers.get_primary_key()

quote do
use OnePiece.Commanded.ValueObject
Expand All @@ -40,7 +76,7 @@ defmodule OnePiece.Commanded.Entity do
"""
@type identifier_key :: unquote(identifier)

@primary_key {unquote(identifier), :string, autogenerate: false}
@primary_key {unquote(identifier), unquote(identifier_type), autogenerate: false}

@doc """
Returns the identity field of the entity.
Expand Down
46 changes: 41 additions & 5 deletions apps/one_piece_commanded/lib/one_piece/commanded/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,25 @@ defmodule OnePiece.Commanded.Event do
Defines "Event" modules.
"""

@typedoc """
A struct that represents an event.
"""
@type t :: struct()

@typedoc """
The aggregate identifier key of the event.
If it's a tuple, the type must be a module that implements the `OnePiece.Commanded.ValueObject` module or [`Ecto` built-in types](https://hexdocs.pm/ecto/Ecto.Schema.html#module-types-and-casting)
"""
@type aggregate_identifier_opt ::
atom() | {key_name :: atom(), type :: atom()} | {key_name :: atom(), type :: module()}

@doc """
Converts the module into an `Ecto.Schema`.
Converts the module into an `t:t/0`.
## Using
It derives from `Jason.Encoder`.
- `OnePiece.Commanded.ValueObject`
## Usage
Expand All @@ -19,14 +32,37 @@ defmodule OnePiece.Commanded.Event do
# ...
end
end
You can also define a custom type as the aggregate identifier:
defmodule IdentityRoleId do
use OnePiece.Commanded.ValueObject
embedded_schema do
field :identity_id, :string
field :role_id, :string
end
end
defmodule IdentityRoleAssigned do
use OnePiece.Commanded.Event,
aggregate_identifier: {:id, IdentityRoleId}
embedded_schema do
# ...
end
end
"""
@spec __using__(opts :: [aggregate_identifier: atom()]) :: any()
@spec __using__(opts :: [aggregate_identifier: aggregate_identifier_opt()]) :: any()
defmacro __using__(opts \\ []) do
unless Keyword.has_key?(opts, :aggregate_identifier) do
raise ArgumentError, "missing :aggregate_identifier key"
end

aggregate_identifier = Keyword.fetch!(opts, :aggregate_identifier)
{aggregate_identifier, aggregate_identifier_type} =
opts
|> Keyword.fetch!(:aggregate_identifier)
|> OnePiece.Commanded.Helpers.get_primary_key()

quote do
use OnePiece.Commanded.ValueObject
Expand All @@ -36,7 +72,7 @@ defmodule OnePiece.Commanded.Event do
"""
@type aggregate_identifier_key :: unquote(aggregate_identifier)

@primary_key {unquote(aggregate_identifier), :string, autogenerate: false}
@primary_key {unquote(aggregate_identifier), unquote(aggregate_identifier_type), autogenerate: false}

@doc """
Returns the aggregate identifier key.
Expand Down

0 comments on commit 4562704

Please sign in to comment.