From 0563be9dafc01a2c8c914e5c3588b956bb313bae Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Thu, 2 Feb 2023 23:52:49 -0700 Subject: [PATCH 1/2] first whack at it --- apps/blunt/lib/blunt/application.ex | 13 ++++ apps/blunt/lib/blunt/config.ex | 10 ++- apps/blunt/lib/blunt/dialect.ex | 34 ++++++++++ apps/blunt/lib/blunt/dialect/registry.ex | 48 ++++++++++++++ apps/blunt/lib/blunt/dialect/stock_dialect.ex | 11 ++++ apps/blunt/mix.exs | 1 + .../blunt/custom_dispatch_strategy_test.exs | 9 ++- apps/blunt_commanded/.formatter.exs | 4 ++ apps/blunt_commanded/.gitignore | 26 ++++++++ apps/blunt_commanded/README.md | 21 ++++++ .../lib/blunt/commanded/command_enrichment.ex | 14 ++++ .../commanded/command_enrichment/default.ex | 5 ++ .../lib/blunt/commanded/dialect.ex | 20 ++++++ .../lib/blunt/commanded/dispatch_strategy.ex | 32 +++++++++ .../dispatch_strategy/command_strategy.ex | 65 +++++++++++++++++++ apps/blunt_commanded/mix.exs | 31 +++++++++ .../test/blunt_commanded_dialect_test.exs | 8 +++ apps/blunt_commanded/test/test_helper.exs | 1 + 18 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 apps/blunt/lib/blunt/application.ex create mode 100644 apps/blunt/lib/blunt/dialect.ex create mode 100644 apps/blunt/lib/blunt/dialect/registry.ex create mode 100644 apps/blunt/lib/blunt/dialect/stock_dialect.ex create mode 100644 apps/blunt_commanded/.formatter.exs create mode 100644 apps/blunt_commanded/.gitignore create mode 100644 apps/blunt_commanded/README.md create mode 100644 apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex create mode 100644 apps/blunt_commanded/lib/blunt/commanded/command_enrichment/default.ex create mode 100644 apps/blunt_commanded/lib/blunt/commanded/dialect.ex create mode 100644 apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex create mode 100644 apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy/command_strategy.ex create mode 100644 apps/blunt_commanded/mix.exs create mode 100644 apps/blunt_commanded/test/blunt_commanded_dialect_test.exs create mode 100644 apps/blunt_commanded/test/test_helper.exs diff --git a/apps/blunt/lib/blunt/application.ex b/apps/blunt/lib/blunt/application.ex new file mode 100644 index 0000000..92faa63 --- /dev/null +++ b/apps/blunt/lib/blunt/application.ex @@ -0,0 +1,13 @@ +defmodule Blunt.Application do + @moduledoc false + use Application + + def start(_type, _args) do + children = [ + Blunt.Dialect.Registry + ] + + opts = [strategy: :one_for_one, name: Blunt.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/apps/blunt/lib/blunt/config.ex b/apps/blunt/lib/blunt/config.ex index 8e19f16..c95749f 100644 --- a/apps/blunt/lib/blunt/config.ex +++ b/apps/blunt/lib/blunt/config.ex @@ -75,16 +75,14 @@ defmodule Blunt.Config do @doc false def dispatch_strategy! do - :dispatch_strategy - |> get(DispatchStrategy.Default) - |> Behaviour.validate!(DispatchStrategy) + %Blunt.Dialect{dispatch_strategy: strategy} = Blunt.Dialect.Registry.get_dialect!() + strategy end @doc false def pipeline_resolver! do - :pipeline_resolver - |> get(PipelineResolver.Default) - |> Behaviour.validate!(PipelineResolver) + %Blunt.Dialect{pipeline_resolver: resolver} = Blunt.Dialect.Registry.get_dialect!() + resolver end def type_spec_provider do diff --git a/apps/blunt/lib/blunt/dialect.ex b/apps/blunt/lib/blunt/dialect.ex new file mode 100644 index 0000000..053c336 --- /dev/null +++ b/apps/blunt/lib/blunt/dialect.ex @@ -0,0 +1,34 @@ +defmodule Blunt.Dialect do + alias Blunt.Behaviour + @callback setup(opts :: list({atom(), any()})) :: t() + + @type t :: %__MODULE__{ + dispatch_strategy: module(), + pipeline_resolver: module() | nil, + opts: list({atom, any}) + } + + @enforce_keys [:dispatch_strategy] + defstruct [ + :dispatch_strategy, + :pipeline_resolver, + opts: [] + ] + + def configured_dialect! do + {module, args} = + case Application.get_env(:blunt, :dialect) do + nil -> + {Blunt.Dialect.StockDialect, [[]]} + + module when is_atom(module) -> + {module, [[]]} + + {module, opts} when is_atom(module) and is_list(opts) -> + {module, opts} + end + + Behaviour.validate!(module, __MODULE__) + apply(module, :setup, args) + end +end diff --git a/apps/blunt/lib/blunt/dialect/registry.ex b/apps/blunt/lib/blunt/dialect/registry.ex new file mode 100644 index 0000000..ce817f4 --- /dev/null +++ b/apps/blunt/lib/blunt/dialect/registry.ex @@ -0,0 +1,48 @@ +defmodule Blunt.Dialect.Registry do + @moduledoc false + + alias Blunt.Dialect + use GenServer + + def start_link(_opts) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def get_dialect! do + :ets.first(__MODULE__) + end + + def put_dialect(%Blunt.Dialect{} = dialect) do + GenServer.call(__MODULE__, {:put_dialect, dialect}) + end + + def restore_dialect do + GenServer.call(__MODULE__, :restore_dialect) + end + + @impl true + def init(:ok) do + table = :ets.new(__MODULE__, [:named_table, read_concurrency: true]) + table = put_configured_dialect!(table) + {:ok, table} + end + + @impl true + def handle_call({:put_dialect, dialect}, _from, table) do + true = :ets.delete_all_objects(table) + true = :ets.insert(table, {dialect}) + {:reply, :ok, table} + end + + def handle_call(:restore_dialect, _from, table) do + table = put_configured_dialect!(table) + {:reply, :ok, table} + end + + defp put_configured_dialect!(table) do + dialect = Dialect.configured_dialect!() + true = :ets.delete_all_objects(table) + true = :ets.insert(table, {dialect}) + table + end +end diff --git a/apps/blunt/lib/blunt/dialect/stock_dialect.ex b/apps/blunt/lib/blunt/dialect/stock_dialect.ex new file mode 100644 index 0000000..17778bc --- /dev/null +++ b/apps/blunt/lib/blunt/dialect/stock_dialect.ex @@ -0,0 +1,11 @@ +defmodule Blunt.Dialect.StockDialect do + @behaviour Blunt.Dialect + + @impl true + def setup(_opts) do + %Blunt.Dialect{ + dispatch_strategy: Blunt.DispatchStrategy.Default, + pipeline_resolver: Blunt.DispatchStrategy.PipelineResolver.Default + } + end +end diff --git a/apps/blunt/mix.exs b/apps/blunt/mix.exs index 09d2aeb..939b9f0 100644 --- a/apps/blunt/mix.exs +++ b/apps/blunt/mix.exs @@ -37,6 +37,7 @@ defmodule Blunt.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ + mod: {Blunt.Application, []}, extra_applications: [:logger] ] end diff --git a/apps/blunt/test/blunt/custom_dispatch_strategy_test.exs b/apps/blunt/test/blunt/custom_dispatch_strategy_test.exs index d45048e..3ccbde1 100644 --- a/apps/blunt/test/blunt/custom_dispatch_strategy_test.exs +++ b/apps/blunt/test/blunt/custom_dispatch_strategy_test.exs @@ -2,13 +2,16 @@ defmodule Blunt.CustomDispatchStrategyTest do use ExUnit.Case, async: false alias Blunt.DispatchContext, as: Context - alias Blunt.{Command, DispatchStrategy, CustomDispatchStrategy} + alias Blunt.{Command, CustomDispatchStrategy} setup do - Application.put_env(:blunt, :dispatch_strategy, CustomDispatchStrategy) + Blunt.Dialect.Registry.put_dialect(%Blunt.Dialect{ + dispatch_strategy: CustomDispatchStrategy, + pipeline_resolver: Blunt.DispatchStrategy.PipelineResolver.Default + }) on_exit(fn -> - Application.put_env(:blunt, :dispatch_strategy, DispatchStrategy.Default) + Blunt.Dialect.Registry.restore_dialect() end) end diff --git a/apps/blunt_commanded/.formatter.exs b/apps/blunt_commanded/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/apps/blunt_commanded/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/apps/blunt_commanded/.gitignore b/apps/blunt_commanded/.gitignore new file mode 100644 index 0000000..ca6676f --- /dev/null +++ b/apps/blunt_commanded/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +blunt_commanded_dialect-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/apps/blunt_commanded/README.md b/apps/blunt_commanded/README.md new file mode 100644 index 0000000..e531d84 --- /dev/null +++ b/apps/blunt_commanded/README.md @@ -0,0 +1,21 @@ +# BluntCommandedDialect + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `blunt_commanded_dialect` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:blunt_commanded_dialect, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex b/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex new file mode 100644 index 0000000..35c93b9 --- /dev/null +++ b/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex @@ -0,0 +1,14 @@ +defmodule Blunt.Commanded.CommandEnrichment do + alias Blunt.DispatchStrategy.PipelineResolver + + @behaviour PipelineResolver + + @callback enrich(command :: struct(), context :: Blunt.DispatchContext.t()) :: struct() + + def resolve(_message_type, message_module) do + handler = message_module |> Module.concat(:Enrichment) |> to_string() + {:ok, String.to_existing_atom(handler)} + rescue + _ -> {:ok, Blunt.Commanded.CommandEnrichment.Default} + end +end diff --git a/apps/blunt_commanded/lib/blunt/commanded/command_enrichment/default.ex b/apps/blunt_commanded/lib/blunt/commanded/command_enrichment/default.ex new file mode 100644 index 0000000..3312dca --- /dev/null +++ b/apps/blunt_commanded/lib/blunt/commanded/command_enrichment/default.ex @@ -0,0 +1,5 @@ +defmodule Blunt.Commanded.CommandEnrichment.Default do + @behaviour Blunt.Commanded.CommandEnrichment + + def enrich(command), do: command +end diff --git a/apps/blunt_commanded/lib/blunt/commanded/dialect.ex b/apps/blunt_commanded/lib/blunt/commanded/dialect.ex new file mode 100644 index 0000000..23cf49d --- /dev/null +++ b/apps/blunt_commanded/lib/blunt/commanded/dialect.ex @@ -0,0 +1,20 @@ +defmodule Blunt.Commanded.Dialect do + @moduledoc false + @behaviour Blunt.Dialect + + def setup(opts) do + commanded_app = Keyword.fetch!(opts, :commanded_app) + + %Blunt.Dialect{ + dispatch_strategy: Blunt.Commanded.DispatchStrategy, + pipeline_resolver: Blunt.Commanded.CommandEnrichment, + opts: [ + commanded_app: commanded_application + ] + } + end + + def commanded_app!(%Blunt.Dialect{opts: opts}) do + Keyword.fetch!(opts, :commanded_app) + end +end diff --git a/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex b/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex new file mode 100644 index 0000000..90fa39c --- /dev/null +++ b/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex @@ -0,0 +1,32 @@ +defmodule Blunt.Commanded.DispatchStrategy do + alias Blunt.Dialect + alias Blunt.Commanded.CommandEnrichment + + import Blunt.DispatchStrategy + + @behaviour Blunt.DispatchStrategy + + @impl true + def dispatch(%{message_type: :command} = context) do + %{message_module: message_module, message: command, opts: opts} = context + + enrichment = PipelineResolver.get_pipeline!(context, CommandEnrichment) + dialect = Dialect.Registry.get_dialect!() + commanded_app = Blunt.Commanded.Dialect.commanded_app!(dialect) + + case DispatchContext.get_return(context) do + :command_context -> + {:ok, context} + + :command -> + return_final(command, context) + + _ -> + with {:ok, context} <- execute({enrichment, :enrich, [command, context]}, context) do + {:ok, result} <- + commanded_app.dispatch command, opts do + end + end + end + end +end diff --git a/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy/command_strategy.ex b/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy/command_strategy.ex new file mode 100644 index 0000000..0d7b94e --- /dev/null +++ b/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy/command_strategy.ex @@ -0,0 +1,65 @@ +defmodule Blunt.Commanded.DispatchStrategy.CommandStrategy do + alias Blunt.Commanded.Dialect, as: CommandedDialect + alias Blunt.{Dialect, DispatchContext, PipelineResolver} + + @behaviour Blunt.DispatchStrategy + + @impl true + def dispatch(%{message_type: :command} = context) do + %{message_module: message_module, message: command, opts: opts} = context + + case DispatchContext.get_return(context) do + :command_context -> + {:ok, context} + + :command -> + return_final(command, context) + + _ -> + # fetch the currently configured Blunt dialect + dialect = Dialect.Registry.get_dialect!() + commanded_app = CommandedDialect.commanded_app!(dialect) + + opts = Keyword.put(opts, :metadata, metadata_from_context(context)) + enrichment = PipelineResolver.get_pipeline!(context, CommandEnrichment) + + with {:ok, context} <- execute({enrichment, :enrich, [command, context]}, context) do + case commanded_app.dispatch(command, opts) do + {:ok, execution_result} -> + %{events: events} = execution_result + + context = + context + |> DispatchContext.put_pipeline(:commanded, events) + |> DispatchContext.put_private(execution_result) + + {:ok, context} + + {:error, error} -> + {:error, + context + |> DispatchContext.put_error(error) + |> DispatchContext.put_pipeline(:commanded, {:error, error})} + end + end + end + end + + def metadata_from_context(%{id: dispatch_id} = context) do + user = Map.get(context, :user, %{}) || %{} + + options = DispatchContext.options_map(context) + + context + |> Map.take([ + :created_at, + :message, + :message_module, + :message_type, + :user_supplied_fields + ]) + |> Map.merge(options) + |> Map.put(:dispatch_id, dispatch_id) + |> Map.put(:dispatched_at, DateTime.utc_now()) + end +end diff --git a/apps/blunt_commanded/mix.exs b/apps/blunt_commanded/mix.exs new file mode 100644 index 0000000..4592596 --- /dev/null +++ b/apps/blunt_commanded/mix.exs @@ -0,0 +1,31 @@ +defmodule BluntCommandedDialect.MixProject do + use Mix.Project + + def project do + [ + app: :blunt_commanded_dialect, + version: "0.1.0", + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + elixir: "~> 1.14", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:blunt, in_umbrella: true} + ] + end +end diff --git a/apps/blunt_commanded/test/blunt_commanded_dialect_test.exs b/apps/blunt_commanded/test/blunt_commanded_dialect_test.exs new file mode 100644 index 0000000..bb3bdcd --- /dev/null +++ b/apps/blunt_commanded/test/blunt_commanded_dialect_test.exs @@ -0,0 +1,8 @@ +defmodule BluntCommandedDialectTest do + use ExUnit.Case + doctest BluntCommandedDialect + + test "greets the world" do + assert BluntCommandedDialect.hello() == :world + end +end diff --git a/apps/blunt_commanded/test/test_helper.exs b/apps/blunt_commanded/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/apps/blunt_commanded/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() From 950be65c1b152241593ec027553b5e3e3ef95772 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Fri, 3 Feb 2023 14:43:41 -0700 Subject: [PATCH 2/2] start testing --- apps/blunt_commanded/config/config.exs | 5 ++++ apps/blunt_commanded/config/test.exs | 1 + .../lib/blunt/commanded/command_enrichment.ex | 7 ++++- .../lib/blunt/commanded/dispatch_strategy.ex | 26 ++----------------- apps/blunt_commanded/mix.exs | 9 +++++-- .../test/support/aggregates/user.ex | 26 +++++++++++++++++++ .../test/support/commanded_app.ex | 3 +++ .../test/support/protocol/create_user.ex | 14 ++++++++++ .../test/support/protocol/update_user.ex | 12 +++++++++ .../protocol/update_user/enrichment.ex | 11 ++++++++ apps/blunt_commanded/test/support/router.ex | 8 ++++++ .../blunt_commanded/test/support/test_case.ex | 3 +++ mix.lock | 8 +++--- 13 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 apps/blunt_commanded/config/config.exs create mode 100644 apps/blunt_commanded/config/test.exs create mode 100644 apps/blunt_commanded/test/support/aggregates/user.ex create mode 100644 apps/blunt_commanded/test/support/commanded_app.ex create mode 100644 apps/blunt_commanded/test/support/protocol/create_user.ex create mode 100644 apps/blunt_commanded/test/support/protocol/update_user.ex create mode 100644 apps/blunt_commanded/test/support/protocol/update_user/enrichment.ex create mode 100644 apps/blunt_commanded/test/support/router.ex create mode 100644 apps/blunt_commanded/test/support/test_case.ex diff --git a/apps/blunt_commanded/config/config.exs b/apps/blunt_commanded/config/config.exs new file mode 100644 index 0000000..effc0fa --- /dev/null +++ b/apps/blunt_commanded/config/config.exs @@ -0,0 +1,5 @@ +import Config + +if config_env() == :test do + import_config "test.exs" +end diff --git a/apps/blunt_commanded/config/test.exs b/apps/blunt_commanded/config/test.exs new file mode 100644 index 0000000..becde76 --- /dev/null +++ b/apps/blunt_commanded/config/test.exs @@ -0,0 +1 @@ +import Config diff --git a/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex b/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex index 35c93b9..626c06b 100644 --- a/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex +++ b/apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex @@ -5,10 +5,15 @@ defmodule Blunt.Commanded.CommandEnrichment do @callback enrich(command :: struct(), context :: Blunt.DispatchContext.t()) :: struct() - def resolve(_message_type, message_module) do + @impl PipelineResolver + def resolve(:command, message_module) do handler = message_module |> Module.concat(:Enrichment) |> to_string() {:ok, String.to_existing_atom(handler)} rescue _ -> {:ok, Blunt.Commanded.CommandEnrichment.Default} end + + def resolve(_message_type, _message_module) do + {:ok, Blunt.Commanded.CommandEnrichment.Default} + end end diff --git a/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex b/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex index 90fa39c..dc423a2 100644 --- a/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex +++ b/apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex @@ -1,32 +1,10 @@ defmodule Blunt.Commanded.DispatchStrategy do - alias Blunt.Dialect - alias Blunt.Commanded.CommandEnrichment - - import Blunt.DispatchStrategy + alias Blunt.Commanded.DispatchStrategy.CommandStrategy @behaviour Blunt.DispatchStrategy @impl true def dispatch(%{message_type: :command} = context) do - %{message_module: message_module, message: command, opts: opts} = context - - enrichment = PipelineResolver.get_pipeline!(context, CommandEnrichment) - dialect = Dialect.Registry.get_dialect!() - commanded_app = Blunt.Commanded.Dialect.commanded_app!(dialect) - - case DispatchContext.get_return(context) do - :command_context -> - {:ok, context} - - :command -> - return_final(command, context) - - _ -> - with {:ok, context} <- execute({enrichment, :enrich, [command, context]}, context) do - {:ok, result} <- - commanded_app.dispatch command, opts do - end - end - end + CommandStrategy.dispatch(context) end end diff --git a/apps/blunt_commanded/mix.exs b/apps/blunt_commanded/mix.exs index 4592596..5318a05 100644 --- a/apps/blunt_commanded/mix.exs +++ b/apps/blunt_commanded/mix.exs @@ -11,10 +11,14 @@ defmodule BluntCommandedDialect.MixProject do lockfile: "../../mix.lock", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + elixirc_paths: elixirc_paths(Mix.env()) ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + # Run "mix help compile.app" to learn about applications. def application do [ @@ -25,7 +29,8 @@ defmodule BluntCommandedDialect.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:blunt, in_umbrella: true} + {:blunt, in_umbrella: true}, + {:commanded, "~> 1.4", only: :test} ] end end diff --git a/apps/blunt_commanded/test/support/aggregates/user.ex b/apps/blunt_commanded/test/support/aggregates/user.ex new file mode 100644 index 0000000..0eb287f --- /dev/null +++ b/apps/blunt_commanded/test/support/aggregates/user.ex @@ -0,0 +1,26 @@ +defmodule BluntCommanded.Test.Aggregates.User do + defstruct [:id, :name] + + alias BluntCommanded.Test.Protocol.{CreateUser, UserCreated} + alias BluntCommanded.Test.Protocol.{UpdateUser, UserUpdated} + + def execute(%{id: nil}, %CreateUser{} = command) do + UserCreated.new(command) + end + + def execute(_state, _command) do + {:error, "user not found"} + end + + def execute(%{id: id}, %UpdateUser{} = command) do + UserUpdated.new(command) + end + + def apply(state, %UserCreated{id: id, name: name}) do + %{state | id: id, name: name} + end + + def apply(state, %UserUpdated{name: name}) do + %{state | name: name} + end +end diff --git a/apps/blunt_commanded/test/support/commanded_app.ex b/apps/blunt_commanded/test/support/commanded_app.ex new file mode 100644 index 0000000..7759307 --- /dev/null +++ b/apps/blunt_commanded/test/support/commanded_app.ex @@ -0,0 +1,3 @@ +defmodule BluntCommanded.CommandedApp do + use Commanded.Application, otp_app: :blunt_commanded_dialect +end diff --git a/apps/blunt_commanded/test/support/protocol/create_user.ex b/apps/blunt_commanded/test/support/protocol/create_user.ex new file mode 100644 index 0000000..d2283c4 --- /dev/null +++ b/apps/blunt_commanded/test/support/protocol/create_user.ex @@ -0,0 +1,14 @@ +defmodule BluntCommanded.Test.Protocol.CreateUser do + use Blunt.Command + use Blunt.Command.EventDerivation + + field :name, :string + internal_field :id, :binary_id + + @impl true + def after_validate(command) do + Map.put(command, :id, UUID.uuid4()) + end + + derive_event UserCreated +end diff --git a/apps/blunt_commanded/test/support/protocol/update_user.ex b/apps/blunt_commanded/test/support/protocol/update_user.ex new file mode 100644 index 0000000..4a568c7 --- /dev/null +++ b/apps/blunt_commanded/test/support/protocol/update_user.ex @@ -0,0 +1,12 @@ +defmodule BluntCommanded.Test.Protocol.UpdateUser do + use Blunt.Command + use Blunt.Command.EventDerivation + + field :id, :binary_id + field :name, :string + + # sample enrichment field. See BluntCommanded.Test.Protocol.UpdateUser.Enrichment + internal_field :date, :utc_datetime + + derive_event UserUpdated +end diff --git a/apps/blunt_commanded/test/support/protocol/update_user/enrichment.ex b/apps/blunt_commanded/test/support/protocol/update_user/enrichment.ex new file mode 100644 index 0000000..0f08259 --- /dev/null +++ b/apps/blunt_commanded/test/support/protocol/update_user/enrichment.ex @@ -0,0 +1,11 @@ +defmodule BluntCommanded.Test.Protocol.UpdateUser.Enrichment do + use Blunt.Commanded.CommandEnrichment + + alias Blunt.DispatchContext + alias BluntCommanded.Test.Protocol.UpdateUser + + @impl true + def enrich(%UpdateUser{} = command, %DispatchContext{} = _context) do + %{command | date: DateTime.utc_now()} + end +end diff --git a/apps/blunt_commanded/test/support/router.ex b/apps/blunt_commanded/test/support/router.ex new file mode 100644 index 0000000..eb47db7 --- /dev/null +++ b/apps/blunt_commanded/test/support/router.ex @@ -0,0 +1,8 @@ +defmodule BluntCommanded.Router do + @moduledoc false + use Commanded.Commands.Router + + alias BluntCommanded.Test.Protocol.{CreateUser, UpdateUser} + + dispatch([CreateUser, UpdateUser]) +end diff --git a/apps/blunt_commanded/test/support/test_case.ex b/apps/blunt_commanded/test/support/test_case.ex new file mode 100644 index 0000000..2ff6739 --- /dev/null +++ b/apps/blunt_commanded/test/support/test_case.ex @@ -0,0 +1,3 @@ +defmodule BluntCommanded.TestCase do + use ExUnit.CaseTemplate +end diff --git a/mix.lock b/mix.lock index a502645..ec78196 100644 --- a/mix.lock +++ b/mix.lock @@ -3,13 +3,13 @@ "absinthe_relay": {:hex, :absinthe_relay, "1.5.1", "adf298e77cf83d52bae1d7dc1579146bf9b893fcaa7b556d62e81a8c6f997514", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1fd2a3559f8472c5bac5778c8b87ae5a5d7f89b594eba26b684ce1d0345a910a"}, "asciichart": {:hex, :asciichart, "1.0.0", "6ef5dbeab545cb7a0bdce7235958f129de6cd8ad193684dc0953c9a8b4c3db5b", [:mix], [], "hexpm", "edc475e4cdd317599310fa714dbc1f53485c32fc918e23e95f0c2bbb731f2ee2"}, "backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"}, - "commanded": {:hex, :commanded, "1.3.1", "d18a73bface68c04cbbda69647604a3cc1918fbdf8af4a784fc3a3a30ca34a13", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "9bd03ef6fc05e3a8fb4d0808f13a2106688e60ee4b2bdb78cf7e63a6788c9faf"}, + "commanded": {:hex, :commanded, "1.4.1", "928b8357ebe1817f88b109693b4d717d20c11ef45cebe42a71dee0a56be36c2c", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "6cd94b4b3369871c030a83b934548720cc834ec7b8549ba031510120aceb7ef9"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"}, - "ecto": {:hex, :ecto, "3.8.3", "5e681d35bc2cbb46dcca1e2675837c7d666316e5ada14eca6c9c609b6232817c", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af92dd7815967bcaea0daaaccf31c3b23165432b1c7a475d84144efbc703d105"}, + "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "elixir_uuid": {:hex, :uuid_utils, "1.6.5", "bafd6ffcbec895513a7c10855df3954f29909fb5d05ee52681e30e84297b1a80", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "36aaeee10740eae4d357231f48571a2687cb541730f94f47cbd3f186dc07899c"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, @@ -21,7 +21,7 @@ "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, "fsm": {:hex, :fsm, "0.3.1", "087aa9b02779a84320dc7a2d8464452b5308e29877921b2bde81cdba32a12390", [:mix], [], "hexpm", "fbf0d53f89e9082b326b0b5828b94b4c549ff9d1452bbfd00b4d1ac082208e96"}, "gen_stage": {:hex, :gen_stage, "1.1.2", "b1656cd4ba431ed02c5656fe10cb5423820847113a07218da68eae5d6a260c23", [:mix], [], "hexpm", "9e39af23140f704e2b07a3e29d8f05fd21c2aaf4088ff43cb82be4b9e3148d02"}, - "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, @@ -32,6 +32,6 @@ "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.0.0", "d5982a319e725fcd2305b306b65c18a86afdcf7d96821473cf0649ff88877615", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.0", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "3401d13a1d4b7aa941a77e6b3ec074f0ae77f83b5b2206766ce630123a9291a9"}, "postgrex": {:hex, :postgrex, "0.16.2", "0f83198d0e73a36e8d716b90f45f3bde75b5eebf4ade4f43fa1f88c90a812f74", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9ea589754d9d4d076121090662b7afe155b374897a6550eb288f11d755acfa0"}, "ratatouille": {:hex, :ratatouille, "0.5.1", "0f80009fa9534e257505bfe06bff28e030b458d4a33ec2427f7be34a6ef1acf7", [:mix], [{:asciichart, "~> 1.0", [hex: :asciichart, repo: "hexpm", optional: false]}, {:ex_termbox, "~> 1.0", [hex: :ex_termbox, repo: "hexpm", optional: false]}], "hexpm", "b2394eb1cc662eae53ae0fb7c27c04543a6d2ce11ab6dc41202c5c4090cbf652"}, - "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_registry": {:hex, :telemetry_registry, "0.3.0", "6768f151ea53fc0fbca70dbff5b20a8d663ee4e0c0b2ae589590e08658e76f1e", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e2adbc609f3e79ece7f29fec363a97a2c484ac78a83098535d6564781e917"}, }