Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/blunt/lib/blunt/application.ex
Original file line number Diff line number Diff line change
@@ -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
10 changes: 4 additions & 6 deletions apps/blunt/lib/blunt/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions apps/blunt/lib/blunt/dialect.ex
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions apps/blunt/lib/blunt/dialect/registry.ex
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions apps/blunt/lib/blunt/dialect/stock_dialect.ex
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions apps/blunt/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions apps/blunt/test/blunt/custom_dispatch_strategy_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions apps/blunt_commanded/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions apps/blunt_commanded/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
21 changes: 21 additions & 0 deletions apps/blunt_commanded/README.md
Original file line number Diff line number Diff line change
@@ -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 <https://hexdocs.pm/blunt_commanded_dialect>.

5 changes: 5 additions & 0 deletions apps/blunt_commanded/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Config

if config_env() == :test do
import_config "test.exs"
end
1 change: 1 addition & 0 deletions apps/blunt_commanded/config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import Config
19 changes: 19 additions & 0 deletions apps/blunt_commanded/lib/blunt/commanded/command_enrichment.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Blunt.Commanded.CommandEnrichment do
alias Blunt.DispatchStrategy.PipelineResolver

@behaviour PipelineResolver

@callback enrich(command :: struct(), context :: Blunt.DispatchContext.t()) :: struct()

@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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule Blunt.Commanded.CommandEnrichment.Default do
@behaviour Blunt.Commanded.CommandEnrichment

def enrich(command), do: command
end
20 changes: 20 additions & 0 deletions apps/blunt_commanded/lib/blunt/commanded/dialect.ex
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions apps/blunt_commanded/lib/blunt/commanded/dispatch_strategy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Blunt.Commanded.DispatchStrategy do
alias Blunt.Commanded.DispatchStrategy.CommandStrategy

@behaviour Blunt.DispatchStrategy

@impl true
def dispatch(%{message_type: :command} = context) do
CommandStrategy.dispatch(context)
end
end
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions apps/blunt_commanded/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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(),
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
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:blunt, in_umbrella: true},
{:commanded, "~> 1.4", only: :test}
]
end
end
8 changes: 8 additions & 0 deletions apps/blunt_commanded/test/blunt_commanded_dialect_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule BluntCommandedDialectTest do
use ExUnit.Case
doctest BluntCommandedDialect

test "greets the world" do
assert BluntCommandedDialect.hello() == :world
end
end
Loading