├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── commanded │ ├── command.ex │ ├── command_dispatch_validation.ex │ ├── command_error.ex │ └── event.ex └── commanded_messaging.ex ├── mix.exs ├── mix.lock └── test ├── commanded_messaging_test.exs ├── support └── messages.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /cover/ 3 | /deps/ 4 | /doc/ 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | commanded_messaging-*.tar 9 | .elixir_ls 10 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commanded Messaging 2 | 3 | **Common macros for messaging in a Commanded application** 4 | 5 | ## Installation 6 | 7 | This package can be installed 8 | by adding `commanded_messaging` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:commanded_messaging, "~> 0.2.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | [Documentation](https://hexdocs.pm/commanded_messaging) 19 | 20 | ## Usage 21 | 22 | ### Commands 23 | 24 | The `Commanded.Command` macro creates an Ecto `embedded_schema` so you can take advantage of the well known `Ecto.Changeset` API. 25 | 26 | #### Default 27 | 28 | ```elixir 29 | defmodule CreateAccount do 30 | use Commanded.Command, 31 | username: :string, 32 | email: :string, 33 | age: :integer 34 | end 35 | 36 | iex> CreateAccount.new() 37 | #Ecto.Changeset, valid?: true> 38 | ``` 39 | 40 | #### Validation 41 | 42 | ```elixir 43 | defmodule CreateAccount do 44 | use Commanded.Command, 45 | username: :string, 46 | email: :string, 47 | age: :integer 48 | 49 | def handle_validate(changeset) do 50 | changeset 51 | |> validate_required([:username, :email, :age]) 52 | |> validate_format(:email, ~r/@/) 53 | |> validate_number(:age, greater_than: 12) 54 | end 55 | end 56 | 57 | iex> CreateAccount.new() 58 | #Ecto.Changeset< 59 | action: nil, 60 | changes: %{}, 61 | errors: [ 62 | username: {"can't be blank", [validation: :required]}, 63 | email: {"can't be blank", [validation: :required]}, 64 | age: {"can't be blank", [validation: :required]} 65 | ], 66 | data: #CreateAccount<>, 67 | valid?: false 68 | > 69 | 70 | iex> changeset = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 71 | #Ecto.Changeset< 72 | action: nil, 73 | changes: %{age: 5, email: "chris@example.com", username: "chris"}, 74 | errors: [ 75 | age: {"must be greater than %{number}", 76 | [validation: :number, kind: :greater_than, number: 12]} 77 | ], 78 | data: #CreateAccount<>, 79 | valid?: false 80 | > 81 | ``` 82 | 83 | To create the actual command struct, use `Ecto.Changeset.apply_changes/1` 84 | 85 | ```elixir 86 | iex> cmd = Ecto.Changeset.apply_changes(changeset) 87 | %CreateAccount{age: 5, email: "chris@example.com", username: "chris"} 88 | ``` 89 | 90 | > Note that `apply_changes` will not validate values. 91 | 92 | ### Events 93 | 94 | Most events mirror the commands that produce them. So we make it easy to reduce the boilerplate in creating them with the `Commanded.Event` macro. 95 | 96 | ```elixir 97 | defmodule AccountCreated do 98 | use Commanded.Event, 99 | from: CreateAccount 100 | end 101 | 102 | iex> AccountCreated.new(cmd) 103 | %AccountCreated{ 104 | age: 5, 105 | email: "chris@example.com", 106 | username: "chris", 107 | version: 1 108 | } 109 | ``` 110 | 111 | #### Extra Keys 112 | 113 | There are times when we need keys defined on an event that aren't part of the originating command. We can add these very easily. 114 | 115 | ```elixir 116 | defmodule AccountCreated do 117 | use Commanded.Event, 118 | from: CreateAccount, 119 | with: [:date] 120 | end 121 | 122 | iex> AccountCreated.new(cmd, date: NaiveDateTime.utc_now()) 123 | %AccountCreated{ 124 | age: 5, 125 | date: ~N[2019-07-25 08:03:15.372212], 126 | email: "chris@example.com", 127 | username: "chris", 128 | version: 1 129 | } 130 | ``` 131 | 132 | #### Excluding Keys 133 | 134 | And you may also want to drop some keys from your command. 135 | 136 | ```elixir 137 | defmodule AccountCreated do 138 | use Commanded.Event, 139 | from: CreateAccount, 140 | with: [:date], 141 | drop: [:email] 142 | end 143 | 144 | iex> event = AccountCreated.new(cmd) 145 | %AccountCreated{age: 5, date: nil, username: "chris", version: 1} 146 | ``` 147 | 148 | #### Versioning 149 | 150 | You may have noticed that we provide a default version of `1`. 151 | 152 | You can change the version of an event at anytime. 153 | 154 | After doing so, you should define an upcast instance that knows how to transform older events into the latest version. 155 | 156 | ```elixir 157 | defmodule AccountCreated do 158 | use Commanded.Event, 159 | version: 2, 160 | from: CreateAccount, 161 | with: [:date, :sex], 162 | drop: [:email] 163 | 164 | defimpl Commanded.Event.Upcaster do 165 | def upcast(%{version: 1} = event, _metadata) do 166 | AccountCreated.new(event, sex: "maybe", version: 2) 167 | end 168 | 169 | def upcast(event, _metadata), do: event 170 | end 171 | end 172 | 173 | iex> Commanded.Event.Upcaster.upcast(event, %{}) 174 | %AccountCreated{age: 5, date: nil, sex: "maybe", username: "chris", version: 2} 175 | ``` 176 | 177 | > Note that you won't normally call `upcast` manually. `Commanded` will take care of that for you. 178 | 179 | ### Command Dispatch Validation 180 | 181 | The `Commanded.CommandDispatchValidation` macro will inject the `validate_and_dispatch` into your `Commanded.Commands.Router`. 182 | 183 | ```elixir 184 | defmodule AccountsRouter do 185 | use Commanded.Commands.Router 186 | use Commanded.CommandDispatchValidation 187 | end 188 | 189 | iex> AccountsRouter.validate_and_dispatch(changeset) 190 | {:error, {:validation_failure, %{age: ["must be greater than 12"]}}} 191 | ``` 192 | 193 | *** 194 | 195 | I hope you find this library as useful as my team and I do. -Chris 196 | -------------------------------------------------------------------------------- /lib/commanded/command.ex: -------------------------------------------------------------------------------- 1 | defmodule Commanded.Command do 2 | @moduledoc ~S""" 3 | Creates an `Ecto.Schema.embedded_schema` that supplies a command with all the validation power of the `Ecto.Changeset` data structure. 4 | 5 | defmodule CreateAccount do 6 | use Commanded.Command, 7 | username: :string, 8 | email: :string, 9 | age: :integer 10 | 11 | def handle_validate(changeset) do 12 | changeset 13 | |> validate_required([:username, :email, :age]) 14 | |> validate_format(:email, ~r/@/) 15 | |> validate_number(:age, greater_than: 12) 16 | end 17 | end 18 | 19 | iex> CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 20 | #Ecto.Changeset, valid?: false> 21 | 22 | iex> CreateAccount.new!(username: "chris", email: "chris@example.com", age: 5) 23 | ** (Commanded.CommandError) Invalid command 24 | """ 25 | 26 | @doc """ 27 | Optional callback to define validation rules 28 | """ 29 | @callback handle_validate(Ecto.Changeset.t()) :: Ecto.Changeset.t() 30 | 31 | defmacro __using__(schema) do 32 | quote do 33 | use Ecto.Schema 34 | import Ecto.Changeset 35 | import Commanded.Command 36 | @behaviour Commanded.Command 37 | 38 | @type t :: %Ecto.Changeset{data: %__MODULE__{}} 39 | 40 | @primary_key false 41 | embedded_schema do 42 | Enum.map(unquote(schema), fn 43 | {name, {type, opts}} -> field(name, field_type(type), opts) 44 | {name, type} -> field(name, field_type(type)) 45 | end) 46 | end 47 | 48 | def new(attrs \\ [], opts \\ []) 49 | 50 | def new(attrs, []) do 51 | attrs 52 | |> Enum.into(%{}) 53 | |> cast() 54 | |> handle_validate() 55 | end 56 | 57 | def new(attrs, opts) do 58 | attrs 59 | |> Enum.into(%{}) 60 | |> cast() 61 | |> handle_validate(opts) 62 | end 63 | 64 | def new!(attrs, opts \\ []) do 65 | result = 66 | attrs 67 | |> new(opts) 68 | |> apply_action(:create) 69 | 70 | case result do 71 | {:ok, command} -> command 72 | {:error, command} -> raise Commanded.CommandError, command: command 73 | end 74 | end 75 | 76 | def handle_validate(changeset), do: handle_validate(changeset, []) 77 | def handle_validate(changeset, _opts), do: changeset 78 | 79 | defoverridable handle_validate: 1, handle_validate: 2 80 | 81 | @cast_keys unquote(schema) |> Enum.into(%{}) |> Map.keys() 82 | 83 | defp cast(attrs) do 84 | Ecto.Changeset.cast(%__MODULE__{}, attrs, @cast_keys) 85 | end 86 | end 87 | end 88 | 89 | def field_type(:binary_id), do: Ecto.UUID 90 | def field_type(type), do: type 91 | end 92 | -------------------------------------------------------------------------------- /lib/commanded/command_dispatch_validation.ex: -------------------------------------------------------------------------------- 1 | defmodule Commanded.CommandDispatchValidation do 2 | @moduledoc ~S""" 3 | Provides validation before dispatching your commands. 4 | """ 5 | 6 | defmacro __using__(_env) do 7 | quote do 8 | alias Ecto.Changeset, as: Command 9 | 10 | @type validation_failure :: [%{required(atom()) => [String.t()]}] 11 | 12 | @spec validate_and_dispatch(Command.t(), Keyword.t()) :: 13 | :ok 14 | | {:ok, aggregate_state :: struct} 15 | | {:ok, aggregate_version :: non_neg_integer()} 16 | | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()} 17 | | {:error, :unregistered_command} 18 | | {:error, :consistency_timeout} 19 | | {:error, reason :: term} 20 | | {:error, {:validation_failure, Command.t()}} 21 | 22 | def validate_and_dispatch(command, opts \\ []) 23 | 24 | def validate_and_dispatch(%Command{valid?: true} = command, opts) do 25 | command 26 | |> Command.apply_changes() 27 | |> __MODULE__.dispatch(opts) 28 | end 29 | 30 | def validate_and_dispatch(%Command{} = command, _opts) do 31 | {:error, {:validation_failure, command}} 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/commanded/command_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Commanded.CommandError do 2 | defexception [:command] 3 | 4 | def message(_), do: "Invalid command" 5 | end 6 | -------------------------------------------------------------------------------- /lib/commanded/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Commanded.Event do 2 | @moduledoc ~S""" 3 | Creates a domain event structure. 4 | 5 | ## Options 6 | 7 | * `from` - A struct to adapt the keys from. 8 | * `with` - A list of keys to add to the event. 9 | * `drop` - A list of keys to drop from the keys adapted from a struct. 10 | * `version` - An optional version of the event. Defaults to `1`. 11 | 12 | ## Example 13 | 14 | # This is for demonstration purposes only. You don't need to create a new event to version one. 15 | defmodule AccountCreatedVersioned do 16 | use Commanded.Event, 17 | version: 2, 18 | from: CreateAccount, 19 | with: [:date, :sex], 20 | drop: [:email], 21 | 22 | defimpl Commanded.Event.Upcaster, for: AccountCreatedWithDroppedKeys do 23 | def upcast(%{version: 1} = event, _metadata) do 24 | AccountCreatedVersioned.new(event, sex: "maybe", version: 2) 25 | end 26 | 27 | def upcast(event, _metadata), do: event 28 | end 29 | end 30 | 31 | iex> changeset = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 32 | iex> cmd = Ecto.Changeset.apply_changes(changeset) 33 | iex> event = AccountCreatedWithDroppedKeys.new(cmd) 34 | iex> Commanded.Event.Upcaster.upcast(event, %{}) 35 | %AccountCreatedVersioned{age: 5, date: nil, sex: "maybe", username: "chris", version: 2} 36 | """ 37 | 38 | defmacro __using__(opts) do 39 | quote bind_quoted: [opts: opts] do 40 | from = 41 | case Keyword.get(opts, :from) do 42 | nil -> 43 | [] 44 | 45 | source -> 46 | unless Code.ensure_compiled(source) do 47 | raise "#{source} should be a valid struct to use with DomainEvent" 48 | end 49 | 50 | struct(source) 51 | |> Map.from_struct() 52 | |> Map.keys() 53 | end 54 | 55 | version = Keyword.get(opts, :version, 1) 56 | keys_to_drop = Keyword.get(opts, :drop, []) 57 | explicit_keys = Keyword.get(opts, :with, []) 58 | 59 | @derive Jason.Encoder 60 | defstruct from 61 | |> Kernel.++(explicit_keys) 62 | |> Enum.reject(&Enum.member?(keys_to_drop, &1)) 63 | |> Kernel.++([{:version, version}]) 64 | 65 | def new(), do: %__MODULE__{} 66 | def new(source, attrs \\ []) 67 | 68 | def new(%{__struct__: _} = source, attrs) do 69 | source 70 | |> Map.from_struct() 71 | |> new(attrs) 72 | end 73 | 74 | def new(source, attrs) when is_list(source) do 75 | source 76 | |> Enum.into(%{}) 77 | |> new(attrs) 78 | end 79 | 80 | def new(source, %{__struct__: _} = attrs) when is_map(source) do 81 | new(source, Map.from_struct(attrs)) 82 | end 83 | 84 | def new(source, attrs) when is_map(source) do 85 | Map.merge(source, Enum.into(attrs, %{})) 86 | |> create() 87 | end 88 | 89 | use ExConstructor, :create 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/commanded_messaging.ex: -------------------------------------------------------------------------------- 1 | defmodule CommandedMessaging do 2 | @moduledoc ~S""" 3 | # Commanded Messaging 4 | 5 | **Common macros for messaging in a Commanded application** 6 | 7 | ## Commands 8 | 9 | The `Commanded.Command` macro creates an Ecto `embedded_schema` so you can take advantage of the well known `Ecto.Changeset` API. 10 | 11 | defmodule BasicCreateAccount do 12 | use Commanded.Command, 13 | username: :string, 14 | email: :string, 15 | age: :integer 16 | end 17 | 18 | iex> BasicCreateAccount.new() 19 | #Ecto.Changeset, valid?: true> 20 | 21 | 22 | ### Validation 23 | 24 | defmodule CreateAccount do 25 | use Commanded.Command, 26 | username: :string, 27 | email: :string, 28 | age: :integer 29 | 30 | def handle_validate(command) do 31 | command 32 | |> validate_required([:username, :email, :age]) 33 | |> validate_format(:email, ~r/@/) 34 | |> validate_number(:age, greater_than: 12) 35 | end 36 | end 37 | 38 | iex> CreateAccount.new() 39 | #Ecto.Changeset, valid?: false> 40 | 41 | iex> CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 42 | #Ecto.Changeset, valid?: false> 43 | 44 | To create the actual command struct, use `Ecto.Changeset.apply_changes/1` 45 | 46 | iex> command = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 47 | iex> Ecto.Changeset.apply_changes(command) 48 | %CreateAccount{age: 5, email: "chris@example.com", username: "chris"} 49 | 50 | > Note that `apply_changes` will not validate values. 51 | 52 | ## Events 53 | 54 | Most events mirror the commands that produce them. So we make it easy to reduce the boilerplate in creating them with the `Commanded.Event` macro. 55 | 56 | defmodule BasicAccountCreated do 57 | use Commanded.Event, 58 | from: CreateAccount 59 | end 60 | 61 | iex> command = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 62 | iex> cmd = Ecto.Changeset.apply_changes(command) 63 | iex> BasicAccountCreated.new(cmd) 64 | %BasicAccountCreated{ 65 | age: 5, 66 | email: "chris@example.com", 67 | username: "chris", 68 | version: 1 69 | } 70 | 71 | 72 | ### Extra Keys 73 | 74 | There are times when we need keys defined on an event that aren't part of the originating command. We can add these very easily. 75 | 76 | defmodule AccountCreatedWithExtraKeys do 77 | use Commanded.Event, 78 | from: CreateAccount, 79 | with: [:date] 80 | end 81 | 82 | iex> command = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 83 | iex> cmd = Ecto.Changeset.apply_changes(command) 84 | iex> AccountCreatedWithExtraKeys.new(cmd, date: ~D[2019-07-25]) 85 | %AccountCreatedWithExtraKeys{ 86 | age: 5, 87 | date: ~D[2019-07-25], 88 | email: "chris@example.com", 89 | username: "chris", 90 | version: 1 91 | } 92 | 93 | 94 | ### Excluding Keys 95 | 96 | And you may also want to drop some keys from your command. 97 | 98 | defmodule AccountCreatedWithDroppedKeys do 99 | use Commanded.Event, 100 | from: CreateAccount, 101 | with: [:date], 102 | drop: [:email] 103 | end 104 | 105 | iex> command = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 106 | iex> cmd = Ecto.Changeset.apply_changes(command) 107 | iex> AccountCreatedWithDroppedKeys.new(cmd) 108 | %AccountCreatedWithDroppedKeys{ 109 | age: 5, 110 | date: nil, 111 | username: "chris", 112 | version: 1 113 | } 114 | 115 | 116 | ### Versioning 117 | 118 | You may have noticed that we provide a default version of `1`. 119 | 120 | You can change the version of an event at anytime. 121 | 122 | After doing so, you should define an upcast instance that knows how to transform older events into the latest version. 123 | 124 | # This is for demonstration purposes only. You don't need to create a new event to version one. 125 | defmodule AccountCreatedVersioned do 126 | use Commanded.Event, 127 | version: 2, 128 | from: CreateAccount, 129 | with: [:date, :sex], 130 | drop: [:email], 131 | 132 | defimpl Commanded.Event.Upcaster, for: AccountCreatedWithDroppedKeys do 133 | def upcast(%{version: 1} = event, _metadata) do 134 | AccountCreatedVersioned.new(event, sex: "maybe", version: 2) 135 | end 136 | 137 | def upcast(event, _metadata), do: event 138 | end 139 | end 140 | 141 | iex> command = CreateAccount.new(username: "chris", email: "chris@example.com", age: 5) 142 | iex> cmd = Ecto.Changeset.apply_changes(command) 143 | iex> event = AccountCreatedWithDroppedKeys.new(cmd) 144 | iex> Commanded.Event.Upcaster.upcast(event, %{}) 145 | %AccountCreatedVersioned{age: 5, date: nil, sex: "maybe", username: "chris", version: 2} 146 | 147 | > Note that you won't normally call `upcast` manually. `Commanded` will take care of that for you. 148 | 149 | ## Command Dispatch Validation 150 | 151 | The `Commanded.CommandDispatchValidation` macro will inject the `validate_and_dispatch` function into your `Commanded.Application`. 152 | """ 153 | end 154 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EsMessaging.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.2.8" 5 | 6 | def project do 7 | [ 8 | app: :commanded_messaging, 9 | version: @version, 10 | elixir: "~> 1.9", 11 | aliases: aliases(), 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | description: "Common macros for messaging in a Commanded application*", 16 | source_url: "https://github.com/trbngr/commanded_messaging", 17 | package: [ 18 | licenses: ["MIT"], 19 | links: %{"GitHub" => "https://github.com/trbngr/commanded_messaging"} 20 | ], 21 | docs: [ 22 | main: "readme", 23 | source_ref: "v#{@version}", 24 | extras: ["README.md"] 25 | ] 26 | ] 27 | end 28 | 29 | defp elixirc_paths(:test), do: ["lib"] ++ Path.wildcard("test/**/support") 30 | defp elixirc_paths(_), do: ["lib"] 31 | 32 | # Run "mix help compile.app" to learn about applications. 33 | def application do 34 | if Mix.env() == :test do 35 | [included_applications: [:commanded]] 36 | else 37 | [] 38 | end 39 | end 40 | 41 | # Run "mix help deps" to learn about dependencies. 42 | defp deps do 43 | [ 44 | {:ecto, "~> 3.3"}, 45 | {:elixir_uuid, "~> 1.2", only: :test}, 46 | {:exconstructor, "~> 1.1"}, 47 | {:jason, "~> 1.1"}, 48 | {:commanded, "~> 1.0", only: :test}, 49 | {:ex_doc, "~> 0.14", only: :dev, runtime: false} 50 | ] 51 | end 52 | 53 | defp aliases do 54 | [ 55 | test: ["test --no-start"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"}, 3 | "commanded": {:hex, :commanded, "1.0.1", "d4d0583aeedfda8d6c129064155695349352d51e7729f6025590172f94fbd7bf", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}], "hexpm", "5b08b8dc940aaf8fae7e47b536601b77a442e857eb55afaa32c34e4c4e12d13c"}, 4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 5 | "db_connection": {:hex, :db_connection, "2.1.0", "122e2f62c4906bf2e49554f1e64db5030c19229aa40935f33088e7d543aa79d0", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, 7 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 8 | "ecto": {:hex, :ecto, "3.3.4", "95b05c82ae91361475e5491c9f3ac47632f940b3f92ae3988ac1aad04989c5bb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "9b96cbb83a94713731461ea48521b178b0e3863d310a39a3948c807266eebd69"}, 9 | "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 11 | "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, 12 | "exconstructor": {:hex, :exconstructor, "1.1.0", "272623a7b203cb2901c20cbb92c5c3ab103cc0087ff7c881979e046043346752", [:mix], [], "hexpm", "0edd55e8352e04dabf71f35453a57650175c7d7e6af707b1d3df610e5052afe0"}, 13 | "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"}, 14 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, 15 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, 17 | "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, 18 | } 19 | -------------------------------------------------------------------------------- /test/commanded_messaging_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CommandTest do 2 | use ExUnit.Case 3 | doctest Commanded.Event 4 | doctest Commanded.Command 5 | doctest CommandedMessaging 6 | end 7 | -------------------------------------------------------------------------------- /test/support/messages.ex: -------------------------------------------------------------------------------- 1 | defmodule BasicCreateAccount do 2 | use Commanded.Command, 3 | username: :string, 4 | email: :string, 5 | age: :integer 6 | end 7 | 8 | defmodule CreateAccount do 9 | use Commanded.Command, 10 | username: :string, 11 | email: :string, 12 | age: :integer 13 | 14 | def handle_validate(changeset) do 15 | changeset 16 | |> validate_required([:username, :email, :age]) 17 | |> validate_format(:email, ~r/@/) 18 | |> validate_number(:age, greater_than: 12) 19 | end 20 | end 21 | 22 | defmodule BasicAccountCreated do 23 | use Commanded.Event, 24 | from: CreateAccount 25 | end 26 | 27 | defmodule AccountCreatedWithExtraKeys do 28 | use Commanded.Event, 29 | from: CreateAccount, 30 | with: [:date] 31 | end 32 | 33 | defmodule AccountCreatedWithDroppedKeys do 34 | use Commanded.Event, 35 | from: CreateAccount, 36 | with: [:date], 37 | drop: [:email] 38 | end 39 | 40 | defmodule AccountCreatedVersioned do 41 | use Commanded.Event, 42 | from: CreateAccount, 43 | with: [:date, :sex], 44 | drop: [:email], 45 | version: 2 46 | 47 | defimpl Commanded.Event.Upcaster, for: AccountCreatedWithDroppedKeys do 48 | def upcast(%{version: 1} = event, _metadata) do 49 | AccountCreatedVersioned.new(event, sex: "maybe", version: 2) 50 | end 51 | 52 | def upcast(event, _metadata), do: event 53 | end 54 | end 55 | 56 | defmodule AccountsRouter do 57 | use Commanded.Commands.Router 58 | use Commanded.CommandDispatchValidation 59 | end 60 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------