Packages
double_down
0.52.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/double/canonical_handler_state.ex
defmodule DoubleDown.Double.CanonicalHandlerState do
@moduledoc """
State for `DoubleDown.Double.canonical_handler/5`.
Stored inline in `HandlerMeta.Stateful.state` when `Double` installs
its canonical stateful handler. Tracks queued expectations,
per-operation stubs, per-operation fakes, and the fallback handler
(function, stateful fake, or module) with its associated state.
## Fields
* `contract` — the contract module this state belongs to (never nil)
* `expects` — `%{operation => [fun | :passthrough]}` queued expectations
* `fakes` — `%{operation => fun}` per-operation stateful fake overrides
* `stubs` — `%{operation => fun}` per-operation stateless stub functions
* `fallback` — the fallback handler, one of:
- `nil` — no fallback configured
- `{:stateless, fun}` — stateless 3-arity function fallback
- `{:stateful, fun}` — 4/5-arity stateful fake function
- `{:module, module}` — module implementing the contract behaviour
* `fallback_state` — domain state for stateful fakes (only meaningful
when `fallback` is `{:stateful, _}`)
"""
@enforce_keys [:contract]
defstruct [
:contract,
expects: %{},
fakes: %{},
stubs: %{},
fallback: nil,
fallback_state: nil
]
@type fallback ::
nil
| {:stateless, DoubleDown.Contract.Dispatch.Types.stateless_fun()}
| {:stateful, DoubleDown.Contract.Dispatch.Types.stateful_fun()}
| {:module, module()}
@type t :: %__MODULE__{
contract: module(),
expects: %{atom() => [DoubleDown.Double.Types.expect_fun() | :passthrough]},
fakes: %{atom() => DoubleDown.Double.Types.fake_fun()},
stubs: %{atom() => DoubleDown.Double.Types.stub_fun()},
fallback: fallback(),
fallback_state: term()
}
@doc """
Create a new canonical handler state for the given contract.
"""
@spec new(module()) :: t()
def new(contract) when is_atom(contract) do
%__MODULE__{contract: contract}
end
# -- Mutation functions --
@doc "Add a single expect entry for an operation."
@spec add_expect(t(), atom(), DoubleDown.Double.Types.expect_fun() | :passthrough) :: t()
def add_expect(%__MODULE__{} = state, operation, entry)
when is_atom(operation) and (is_function(entry) or entry == :passthrough) do
add_expects(state, operation, [entry])
end
@doc "Add multiple expect entries for an operation (e.g. from `times: n`)."
@spec add_expects(t(), atom(), [DoubleDown.Double.Types.expect_fun() | :passthrough]) :: t()
def add_expects(%__MODULE__{expects: expects} = state, operation, entries)
when is_atom(operation) and is_list(entries) do
existing = Map.get(expects, operation, [])
%{state | expects: Map.put(expects, operation, existing ++ entries)}
end
@doc "Set a per-operation stub."
@spec put_stub(t(), atom(), DoubleDown.Double.Types.stub_fun()) :: t()
def put_stub(%__MODULE__{} = state, operation, fun)
when is_atom(operation) and is_function(fun, 1) do
%{state | stubs: Map.put(state.stubs, operation, fun)}
end
@doc "Set a per-operation fake."
@spec put_fake(t(), atom(), DoubleDown.Double.Types.fake_fun()) :: t()
def put_fake(%__MODULE__{} = state, operation, fun)
when is_atom(operation) and (is_function(fun, 2) or is_function(fun, 3)) do
%{state | fakes: Map.put(state.fakes, operation, fun)}
end
@doc "Set a stateless function fallback."
@spec set_stateless_fallback(t(), DoubleDown.Contract.Dispatch.Types.stateless_fun()) :: t()
def set_stateless_fallback(%__MODULE__{} = state, fun) when is_function(fun, 3) do
%{state | fallback: {:stateless, fun}}
end
@doc "Set a stateful function fallback with initial state."
@spec set_stateful_fallback(t(), DoubleDown.Contract.Dispatch.Types.stateful_fun(), term()) ::
t()
def set_stateful_fallback(%__MODULE__{} = state, fun, init_state)
when is_function(fun, 4) or is_function(fun, 5) do
%{state | fallback: {:stateful, fun}, fallback_state: init_state}
end
@doc "Set a module fallback."
@spec set_module_fallback(t(), module()) :: t()
def set_module_fallback(%__MODULE__{} = state, module) when is_atom(module) do
%{state | fallback: {:module, module}}
end
@doc "Update the fallback_state (used during dispatch)."
@spec put_fallback_state(t(), term()) :: t()
def put_fallback_state(%__MODULE__{} = state, new_fallback_state) do
%{state | fallback_state: new_fallback_state}
end
@doc "Pop the next expect entry for an operation."
@spec pop_expect(t(), atom()) ::
{:ok, DoubleDown.Double.Types.expect_fun() | :passthrough, t()} | :none
def pop_expect(%__MODULE__{expects: expects} = state, operation) do
case Map.get(expects, operation, []) do
[entry | rest] ->
{:ok, entry, %{state | expects: Map.put(expects, operation, rest)}}
[] ->
:none
end
end
@doc "Check if a stateful fallback is configured."
@spec stateful_fallback?(t()) :: boolean()
def stateful_fallback?(%__MODULE__{fallback: {:stateful, _}}), do: true
def stateful_fallback?(%__MODULE__{}), do: false
end