Packages
double_down
0.55.0
0.69.0
0.68.0
0.66.0
0.65.0
0.64.1
0.64.0
0.63.3
0.63.2
0.63.1
0.63.0
0.62.1
0.61.0
0.60.4
0.60.3
0.60.2
0.60.1
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.1
0.48.0
0.47.2
0.47.1
0.47.0
0.46.3
0.46.2
0.46.1
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.2
0.37.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.1
0.30.0
0.29.0
0.28.1
0.28.0
0.27.0
0.26.0
0.24.0
Builds on the Mox pattern — generates behaviours and dispatch facades from `defcallback` declarations — and adds stateful test doubles powerful enough to test Ecto.Repo operations without a database.
Current section
Files
Jump to
Current section
Files
lib/double_down/behaviour_facade.ex
defmodule DoubleDown.BehaviourFacade do
@moduledoc """
Generates a dispatch facade for a vanilla Elixir `@behaviour` module.
Use this when you want DoubleDown's dispatch machinery for a behaviour
you don't control — a third-party library behaviour, an existing
`@behaviour` in your codebase, or any module that defines `@callback`
declarations without using `defcallback`.
For behaviours you *do* control, prefer `DoubleDown.ContractFacade` with
`defcallback` — it gives you richer features (pre_dispatch transforms,
`@doc` tag sync, combined contract + facade in one module).
## Usage
defmodule MyApp.Todos do
use DoubleDown.BehaviourFacade,
behaviour: MyApp.Todos.Behaviour,
otp_app: :my_app
end
The behaviour module must be compiled **in a prior compilation
unit** — its `.beam` file must be on disk before the facade
compiles. In a Mix project, this means the behaviour and facade
must be in separate `elixirc_paths` directories (e.g. behaviour
in `lib/` or an earlier test support directory, facade in a
later one). Combined contract + facade in a single module is
not supported — use `DoubleDown.ContractFacade` for that.
## Options
* `:behaviour` (required) — the vanilla behaviour module to generate
a facade for. Must define `@callback` declarations.
* `:otp_app` (required) — the OTP application name for config-based
dispatch. Implementations are resolved from
`Application.get_env(otp_app, behaviour)[:impl]`.
* `:test_dispatch?` — same as `DoubleDown.ContractFacade`. Defaults to
`Mix.env() != :prod`.
* `:static_dispatch?` — same as `DoubleDown.ContractFacade`. Defaults to
`Mix.env() == :prod`.
## Param names
Where `@callback` declarations use annotated types like
`id :: String.t()`, the annotation name is used as the facade
function's parameter name. For bare types like `String.t()`,
parameter names are synthesized as `arg1`, `arg2`, etc.
## Configuration
# config/config.exs
config :my_app, MyApp.Todos.Behaviour, impl: MyApp.Todos.Impl
## Testing
setup do
DoubleDown.Testing.set_stateless_handler(MyApp.Todos.Behaviour, fn _contract, operation, args ->
case {operation, args} do
{:get_item, [id]} -> {:ok, %{id: id}}
{:list_items, []} -> []
end
end)
:ok
end
## Limitations vs `DoubleDown.ContractFacade`
* No `pre_dispatch` transforms
* No `@doc` tag sync from contract to facade
* No combined contract + facade in one module
* Param names are synthesized for bare (unannotated) types
* No compile-time spec mismatch warnings
## See also
* `DoubleDown.ContractFacade` — dispatch facades for `defcallback` contracts
(richer features, recommended for new code).
* `DoubleDown.DynamicFacade` — Mimic-style bytecode interception for any module.
"""
alias DoubleDown.Facade.BehaviourIntrospection
alias DoubleDown.Facade.Codegen
@doc false
defmacro __using__(opts) do
behaviour =
case Keyword.get(opts, :behaviour) do
nil ->
raise CompileError,
description:
"use DoubleDown.BehaviourFacade requires a :behaviour option. " <>
"Example: use DoubleDown.BehaviourFacade, behaviour: MyBehaviour, otp_app: :my_app",
file: __CALLER__.file,
line: __CALLER__.line
b ->
Macro.expand(b, __CALLER__)
end
if behaviour == __CALLER__.module do
raise CompileError,
description:
"DoubleDown.BehaviourFacade cannot be used in the same module as the behaviour " <>
"(#{inspect(behaviour)}). The behaviour must be compiled first. " <>
"For combined contract + facade, use DoubleDown.ContractFacade with defcallback instead.",
file: __CALLER__.file,
line: __CALLER__.line
end
# Reject DoubleDown contract modules — they should use ContractFacade
Code.ensure_compiled(behaviour)
if function_exported?(behaviour, :__callbacks__, 0) do
raise CompileError,
description:
"#{inspect(behaviour)} is a DoubleDown contract module (it defines defcallback operations). " <>
"Use DoubleDown.ContractFacade instead of BehaviourFacade:\n\n" <>
" use DoubleDown.ContractFacade, contract: #{inspect(behaviour)}, otp_app: :my_app",
file: __CALLER__.file,
line: __CALLER__.line
end
otp_app = Keyword.fetch!(opts, :otp_app)
test_dispatch? =
Codegen.resolve_dispatch_option(
Keyword.get(opts, :test_dispatch?),
__CALLER__,
Mix.env() != :prod
)
static_dispatch? =
Codegen.resolve_dispatch_option(
Keyword.get(opts, :static_dispatch?),
__CALLER__,
Mix.env() == :prod
)
# Fetch operations NOW, in the macro context. Code.ensure_compiled!
# blocks until the behaviour module is compiled. We then read the
# callback specs from the in-memory module — this works because the
# macro runs during compilation, and the Elixir compiler guarantees
# the dependency module is fully available at this point.
operations = BehaviourIntrospection.fetch_operations!(behaviour, __CALLER__)
static_impl =
Codegen.resolve_static_impl(otp_app, behaviour, test_dispatch?, static_dispatch?)
facades =
Enum.map(
operations,
&Codegen.generate_facade(&1, behaviour, otp_app, test_dispatch?, static_impl)
)
key_helpers = Enum.map(operations, &Codegen.generate_key_helper(&1, behaviour))
moduledoc = Codegen.generate_moduledoc(behaviour, otp_app)
quote do
unquote(moduledoc)
unquote_splicing(facades)
unquote_splicing(key_helpers)
end
end
end