Macro for defining contract behaviours with defcallback declarations.
use DoubleDown.Contract imports the defcallback macro and registers a
@before_compile hook that generates:
@callbackdeclarations on the contract module itself — the contract module is the behaviour__callbacks__/0— introspection metadata used byDoubleDown.ContractFacadeto generate dispatch functions
Contracts are purely static interface definitions. They do not
generate a dispatch facade — that is the concern of
DoubleDown.ContractFacade, which the consuming application uses separately to
bind a contract to an OTP application's config.
Usage
defmodule MyApp.Todos do
use DoubleDown.Contract
defcallback get_todo(tenant_id :: String.t(), id :: String.t()) ::
{:ok, Todo.t()} | {:error, term()}
defcallback list_todos(tenant_id :: String.t()) :: [Todo.t()]
endThis generates @callback declarations on MyApp.Todos and
MyApp.Todos.__callbacks__/0.
Implementations use @behaviour MyApp.Todos directly:
defmodule MyApp.Todos.Ecto do
@behaviour MyApp.Todos
# ...
endCompatible with Mox.defmock(Mock, for: MyApp.Todos).
To generate a dispatch facade, use DoubleDown.ContractFacade in a separate module:
defmodule MyApp.Todos do
use DoubleDown.ContractFacade, contract: MyApp.Todos.Contract, otp_app: :my_app
endSee DoubleDown.ContractFacade for dispatch configuration and DoubleDown for an overview.
Summary
Functions
Define a typed callback operation.
Functions
Define a typed callback operation.
defcallback uses a superset of the standard @callback syntax,
with mandatory parameter names and optional metadata. If your
existing @callback declarations include parameter names, you can
replace @callback with defcallback and you're done:
# Standard @callback — already valid as a defcallback
@callback get_todo(id :: String.t()) :: {:ok, Todo.t()} | {:error, term()}
# Equivalent defcallback
defcallback get_todo(id :: String.t()) :: {:ok, Todo.t()} | {:error, term()}Why defcallback instead of plain @callback?
Parameter names are mandatory. Plain
@callbackallows unnamed parameters like@callback get(term(), term()) :: term().defcallbackrequiresname :: type()for every parameter — these are used to generate meaningful@specand@docon the facade.Combined contract + facade.
Code.Typespec.fetch_callbacks/1only works on pre-compiled modules with beam files on disk, ruling out the combined contract + facade pattern entirely.defcallbackcaptures metadata at macro expansion time via__callbacks__/0, so the contract and facade can live in the same module.LSP-friendly docs. Plain
@callbackdeclarations don't support@doc— the best you can do is#comments that won't appear in hover docs. Withdefcallback,@docplaced above the declaration resolves on both the declaration and on any call site that goes through the facade.Additional metadata.
defcallbacksupports options likepre_dispatch:(argument transforms before dispatch). Plain@callbackhas no mechanism for this.
See Why defcallback instead of plain @callback?
in the Getting Started guide for the full rationale.
Syntax
defcallback function_name(param :: type(), ...) :: return_type()
defcallback function_name(param :: type(), ...) :: return_type(), optsOptions
Pre-dispatch transform (:pre_dispatch)
:pre_dispatch— a function(args, facade_module) -> argsthat transforms the argument list before dispatch. The function receives the args as a list and the facade module atom, and must return the (possibly modified) args list. This is useful for injecting facade-specific context into arguments at the dispatch boundary. Most contracts don't need this — the canonical example isDoubleDown.Repo'stransactoperation.
Typespec mismatch severity (:warn_on_typespec_mismatch?)
- omitted /
false(default) — raiseCompileErrorwhen thedefcallbacktype spec doesn't match the production impl's@spec. true— emit a warning instead of an error. Use this during migration when you know the specs differ and want to defer fixing them.
See DoubleDown.Contract.SpecWarnings for details on compile-time
spec mismatch detection.