-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add onepiece commanded enum
- Loading branch information
Showing
4 changed files
with
292 additions
and
3 deletions.
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
apps/one_piece_commanded/lib/one_piece/commanded/enum.ex
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
defmodule OnePiece.Commanded.Enum do | ||
@moduledoc """ | ||
Enum module with added macros to define a type and check supported values. | ||
""" | ||
|
||
defmacro __using__(opts) do | ||
values = Keyword.fetch!(opts, :values) | ||
type_ast = Enum.reduce(values, &{:|, [], [&1, &2]}) | ||
|
||
value_functions_ast = | ||
for value <- values do | ||
quote do | ||
def unquote(value)(), do: %__MODULE__{value: unquote(value)} | ||
end | ||
end | ||
|
||
load_functions_ast = | ||
for value <- values do | ||
quote do | ||
@impl Ecto.Type | ||
def load(unquote(Atom.to_string(value))) do | ||
{:ok, %__MODULE__{value: unquote(value)}} | ||
end | ||
end | ||
end | ||
|
||
dump_functions_ast = | ||
for value <- values do | ||
value_string = Atom.to_string(value) | ||
|
||
quote do | ||
@impl Ecto.Type | ||
def dump(%__MODULE__{value: unquote(value)}) do | ||
{:ok, unquote(value_string)} | ||
end | ||
end | ||
end | ||
|
||
cast_as_function_ast = | ||
for value <- values do | ||
value_string = Atom.to_string(value) | ||
|
||
quote do | ||
@impl Ecto.Type | ||
def cast(unquote(value_string)) do | ||
{:ok, %__MODULE__{value: unquote(value)}} | ||
end | ||
end | ||
end | ||
|
||
quote generated: true do | ||
alias OnePiece.Commanded.ValueObject | ||
alias Ecto.Changeset | ||
|
||
use Ecto.Schema | ||
use Ecto.Type | ||
|
||
@primary_key false | ||
@enforce_keys [:value] | ||
embedded_schema do | ||
field :value, Ecto.Enum, values: unquote(values) | ||
end | ||
|
||
@type value :: unquote(type_ast) | ||
@type t :: %__MODULE__{value: value()} | ||
|
||
@doc """ | ||
Creates a `t:t/0`. | ||
""" | ||
@spec new(attrs :: %{required(:value) => value()}) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()} | ||
def new(attrs) when is_map(attrs) do | ||
ValueObject.__new__(__MODULE__, attrs) | ||
end | ||
|
||
@spec new(value :: value()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()} | ||
def new(value) do | ||
ValueObject.__new__(__MODULE__, %{value: value}) | ||
end | ||
|
||
@doc """ | ||
Creates a `t:t/0`. | ||
""" | ||
@spec new!(attrs :: %{required(:value) => value()}) :: %__MODULE__{} | ||
def new!(attrs) when is_map(attrs) do | ||
ValueObject.__new__!(__MODULE__, attrs) | ||
end | ||
|
||
@spec new!(value :: value()) :: %__MODULE__{} | ||
def new!(value) do | ||
ValueObject.__new__!(__MODULE__, %{value: value}) | ||
end | ||
|
||
@doc """ | ||
Returns an `t:Ecto.Changeset.t/0` for a given `t:t/0` value object. | ||
""" | ||
@spec changeset(message :: %__MODULE__{}, attrs :: %{required(:value) => value()}) :: Ecto.Changeset.t() | ||
def changeset(message, attrs) do | ||
message | ||
|> Changeset.cast(attrs, [:value]) | ||
|> Changeset.validate_required([:value]) | ||
end | ||
|
||
@spec values() :: [unquote(type_ast)] | ||
def values, do: unquote(values) | ||
|
||
unquote_splicing(value_functions_ast) | ||
|
||
@impl Ecto.Type | ||
def type, do: :string | ||
|
||
@impl Ecto.Type | ||
def cast(value) when is_struct(value, __MODULE__) do | ||
{:ok, value} | ||
end | ||
|
||
unquote_splicing(cast_as_function_ast) | ||
|
||
@impl Ecto.Type | ||
def cast(value) when value in unquote(values) do | ||
{:ok, %__MODULE__{value: value}} | ||
end | ||
|
||
@impl Ecto.Type | ||
def cast(_), do: :error | ||
|
||
unquote_splicing(load_functions_ast) | ||
@impl Ecto.Type | ||
def load(_), do: :error | ||
|
||
unquote_splicing(dump_functions_ast) | ||
@impl Ecto.Type | ||
def dump(_), do: :error | ||
|
||
@impl Ecto.Type | ||
def equal?(%__MODULE__{value: value1}, %__MODULE__{value: value1}) do | ||
true | ||
end | ||
|
||
@impl Ecto.Type | ||
def equal?(_term1, _term2), do: false | ||
|
||
defimpl Jason.Encoder do | ||
def encode(v, opts) do | ||
Jason.Encode.value(v.value, opts) | ||
end | ||
end | ||
end | ||
end | ||
end |
134 changes: 134 additions & 0 deletions
134
apps/one_piece_commanded/test/one_piece/commanded/enum_test.exs
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
defmodule OnePiece.Commanded.EnumTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias TestSupport.CommandRouterExample.BankAccountType | ||
|
||
describe "new/1" do | ||
test "returns a value object" do | ||
assert {:ok, value} = BankAccountType.new(:business) | ||
assert value == %BankAccountType{value: :business} | ||
end | ||
|
||
test "returns a value object when passing a map" do | ||
assert {:ok, value} = BankAccountType.new(%{value: :business}) | ||
assert value == %BankAccountType{value: :business} | ||
end | ||
|
||
test "returns an error when a validation fails" do | ||
assert {:error, changeset} = BankAccountType.new(nil) | ||
assert %{value: ["can't be blank"]} = TestSupport.errors_on(changeset) | ||
end | ||
|
||
test "with an invalid value" do | ||
assert {:error, changeset} = BankAccountType.new(:invalid) | ||
assert %{value: ["is invalid"]} = TestSupport.errors_on(changeset) | ||
end | ||
end | ||
|
||
describe "new!/1" do | ||
test "creates a value object" do | ||
assert %BankAccountType{value: :business} = BankAccountType.new!(:business) | ||
end | ||
|
||
test "creates a value object when passing a map" do | ||
assert %BankAccountType{value: :business} = BankAccountType.new!(%{value: :business}) | ||
end | ||
|
||
test "raises an error when a validation fails" do | ||
assert_raise Ecto.InvalidChangesetError, fn -> | ||
BankAccountType.new!(nil) | ||
end | ||
end | ||
|
||
test "raises an error with an invalid value" do | ||
assert_raise Ecto.InvalidChangesetError, fn -> | ||
BankAccountType.new!(:invalid) | ||
end | ||
end | ||
end | ||
|
||
test "values/0" do | ||
assert BankAccountType.values() == [:business, :personal] | ||
end | ||
|
||
test "specific enum values" do | ||
assert BankAccountType.business() == %BankAccountType{value: :business} | ||
assert BankAccountType.personal() == %BankAccountType{value: :personal} | ||
end | ||
|
||
describe "type/1" do | ||
test "returns the type of the enum" do | ||
assert BankAccountType.type() == :string | ||
end | ||
end | ||
|
||
describe "cast/1" do | ||
test "casts atom values" do | ||
assert BankAccountType.cast(:business) == {:ok, %BankAccountType{value: :business}} | ||
assert BankAccountType.cast(:personal) == {:ok, %BankAccountType{value: :personal}} | ||
assert BankAccountType.cast(:invalid) == :error | ||
end | ||
|
||
test "casts string values" do | ||
assert BankAccountType.cast("business") == {:ok, %BankAccountType{value: :business}} | ||
assert BankAccountType.cast("personal") == {:ok, %BankAccountType{value: :personal}} | ||
assert BankAccountType.cast("invalid") == :error | ||
end | ||
|
||
test "casts existing struct" do | ||
existing = %BankAccountType{value: :business} | ||
assert BankAccountType.cast(existing) == {:ok, existing} | ||
end | ||
end | ||
|
||
describe "load/1" do | ||
test "loads string values" do | ||
assert BankAccountType.load("business") == {:ok, %BankAccountType{value: :business}} | ||
assert BankAccountType.load("personal") == {:ok, %BankAccountType{value: :personal}} | ||
assert BankAccountType.load("invalid") == :error | ||
end | ||
|
||
test "returns error for non-string values" do | ||
assert BankAccountType.load(:business) == :error | ||
assert BankAccountType.load(123) == :error | ||
assert BankAccountType.load(%{}) == :error | ||
end | ||
end | ||
|
||
describe "dump/1" do | ||
test "dumps struct to string" do | ||
assert BankAccountType.dump(%BankAccountType{value: :business}) == {:ok, "business"} | ||
assert BankAccountType.dump(%BankAccountType{value: :personal}) == {:ok, "personal"} | ||
end | ||
|
||
test "returns error for invalid values" do | ||
assert BankAccountType.dump(%BankAccountType{value: :invalid}) == :error | ||
assert BankAccountType.dump("business") == :error | ||
assert BankAccountType.dump(:business) == :error | ||
assert BankAccountType.dump(%{}) == :error | ||
end | ||
end | ||
|
||
describe "equal?/2" do | ||
test "returns true for matching values" do | ||
assert BankAccountType.equal?(%BankAccountType{value: :business}, %BankAccountType{value: :business}) | ||
assert BankAccountType.equal?(%BankAccountType{value: :personal}, %BankAccountType{value: :personal}) | ||
end | ||
|
||
test "returns false for different values" do | ||
refute BankAccountType.equal?(%BankAccountType{value: :business}, %BankAccountType{value: :personal}) | ||
end | ||
|
||
test "returns false for non-struct values" do | ||
refute BankAccountType.equal?(%BankAccountType{value: :business}, :business) | ||
refute BankAccountType.equal?(:business, %BankAccountType{value: :business}) | ||
refute BankAccountType.equal?("business", "business") | ||
end | ||
end | ||
|
||
describe "Jason.Encoder" do | ||
test "encodes to the value" do | ||
assert Jason.encode!(%BankAccountType{value: :business}) == ~s("business") | ||
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
6 changes: 6 additions & 0 deletions
6
apps/one_piece_commanded/test/support/command_router_example/bank_account_type.ex
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
defmodule TestSupport.CommandRouterExample.BankAccountType do | ||
@moduledoc false | ||
|
||
use OnePiece.Commanded.Enum, | ||
values: [:business, :personal] | ||
end |