Created
September 7, 2022 04:31
-
-
Save danhawkins/35e6720254c68b388e132e658f132ccd to your computer and use it in GitHub Desktop.
Command macro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Cmd.Command do | |
@moduledoc """ | |
Utility for command definition, wrapping a design pattern into code | |
For our commands we will used type_embedded_schema to design an in memory schema | |
which will be used for the command payload | |
As well as this we require the developer to implement a changeset/2 function | |
""" | |
@type struct_or_map :: struct() | map() | |
@callback changeset(schema :: struct(), attrs :: struct_or_map) :: Ecto.Changeset.t() | |
defmacro __using__(_) do | |
module_name = __CALLER__.module | |
quote do | |
@module_name unquote(module_name) | |
import Ecto.Changeset | |
import Utils.ConvertStructs, only: [convert_to_map: 1] | |
import TypedEctoSchema, | |
only: [ | |
typed_embedded_schema: 1, | |
typed_embedded_schema: 2 | |
] | |
@behaviour Cmd.Command | |
# We never want a default PK in command | |
@primary_key false | |
use Ecto.Schema | |
def cast(attrs) do | |
safe_changeset(attrs) |> apply_changes() | |
end | |
def changeset(attrs) do | |
struct(@module_name, %{}) |> changeset(attrs) | |
end | |
def safe_changeset(attrs) do | |
struct(@module_name, %{}) |> safe_changeset(attrs) | |
end | |
def safe_changeset(schema, attrs) do | |
map_attrs = convert_to_map(attrs) | |
changeset(schema, map_attrs) | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Utils.ConvertStructs do | |
@moduledoc """ | |
Utilities to conver structs into map and vice versa (specifically for moving maps to ecto changesets) | |
""" | |
def convert_to_struct(struct_name, map) do | |
struct(struct_name, map) | |
end | |
def convert_to_map(%model{__schema__: _} = schema) do | |
new_map = Map.take(schema, model.__schema__(:fields)) | |
convert_to_map(new_map) | |
end | |
def convert_to_map(schema) when is_struct(schema) do | |
new_map = Map.from_struct(schema) | |
convert_to_map(new_map) | |
end | |
def convert_to_map(schema) when is_map(schema) do | |
for {k, v} <- schema, into: %{} do | |
{k, convert_to_map(v)} | |
end | |
end | |
def convert_to_map([h | t] = input) when is_list(input), do: [convert_to_map(h) | convert_to_map(t)] | |
def convert_to_map(:null), do: nil | |
def convert_to_map(term), do: term | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Ivrl.Tournaments.Commands.ActivateTeam do | |
@moduledoc """ | |
Updates a teams score for the tournament | |
""" | |
use Cmd.Command | |
typed_embedded_schema do | |
field(:tournament_id, :string) | |
field(:sub_tournament_id, :string) | |
field(:team_id, :string) | |
embeds_one(:issued_by, User) | |
end | |
def changeset(cmd, attrs) do | |
cmd | |
|> cast(attrs, [:tournament_id, :sub_tournament_id, :team_id]) | |
|> validate_required([:tournament_id, :team_id]) | |
|> cast_embed(:issued_by, with: &User.changeset/2) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Cmd.Middleware.Validate do | |
@moduledoc """ | |
This commanded middleware requires all commands to be Ecto structs, | |
this allows commands to have validation setup, if the command failes validation the command will not proceed | |
and the pipeline will be halted, instead of getting and `:ok` back, you will received a list of validation errors | |
## Error response example | |
``` | |
{:error, :validation_failure, | |
%{ | |
field1: ["can't be blank"], | |
field2: ["can't be blank"], | |
}} | |
## NOTE | |
We don't handle nested errors here at the moment, although they are inside the changeset | |
``` | |
""" | |
@behaviour Commanded.Middleware | |
alias Commanded.Middleware.Pipeline | |
import Pipeline | |
def before_dispatch(%Pipeline{command: %{__struct__: mod} = command} = pipeline) do | |
if is_ecto?(command) do | |
case mod.safe_changeset(command) do | |
%{valid?: false} = changeset -> failed_validation(pipeline, changeset) | |
changeset -> %{pipeline | command: Ecto.Changeset.apply_changes(changeset)} | |
end | |
else | |
pipeline | |
end | |
end | |
def before_dispatch(%Pipeline{command: _} = pipeline) do | |
pipeline | |
end | |
def after_dispatch(pipeline), do: pipeline | |
def after_failure(pipeline), do: pipeline | |
defp is_ecto?(%{__struct__: mod}) do | |
Kernel.function_exported?(mod, :__changeset__, 0) | |
end | |
defp failed_validation(pipeline, %{errors: errors}) do | |
pipeline | |
|> respond({:error, :validation_failure, merge_errors(errors)}) | |
|> halt() | |
end | |
defp merge_errors(errors) do | |
Enum.map(errors, fn {field, {msg, _}} -> | |
{field, [msg]} | |
end) | |
|> Enum.into(%{}) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment