DoubleDown.Contract (double_down v0.48.1)

Copy Markdown View Source

Macro for defining contract behaviours with defcallback declarations.

use DoubleDown.Contract imports the defcallback macro and registers a @before_compile hook that generates:

  • @callback declarations on the contract module itself — the contract module is the behaviour
  • __callbacks__/0 — introspection metadata used by DoubleDown.ContractFacade to 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()]
end

This 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
  # ...
end

Compatible 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
end

See DoubleDown.ContractFacade for dispatch configuration and DoubleDown for an overview.

Summary

Functions

Define a typed callback operation.

Functions

defcallback(spec, opts \\ [])

(macro)

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 @callback allows unnamed parameters like @callback get(term(), term()) :: term(). defcallback requires name :: type() for every parameter — these are used to generate meaningful @spec and @doc on the facade.

  • Combined contract + facade. Code.Typespec.fetch_callbacks/1 only works on pre-compiled modules with beam files on disk, ruling out the combined contract + facade pattern entirely. defcallback captures metadata at macro expansion time via __callbacks__/0, so the contract and facade can live in the same module.

  • LSP-friendly docs. Plain @callback declarations don't support @doc — the best you can do is # comments that won't appear in hover docs. With defcallback, @doc placed above the declaration resolves on both the declaration and on any call site that goes through the facade.

  • Additional metadata. defcallback supports options like pre_dispatch: (argument transforms before dispatch). Plain @callback has 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(), opts

Options

Pre-dispatch transform (:pre_dispatch)

  • :pre_dispatch — a function (args, facade_module) -> args that 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 is DoubleDown.Repo's transact operation.

Typespec mismatch severity (:warn_on_typespec_mismatch?)

  • omitted / false (default) — raise CompileError when the defcallback type 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.