# `DoubleDown.Log`
[🔗](https://github.com/mccraigmccraig/double_down/blob/main/lib/double_down/log.ex#L1)

Log-based expectation matcher for DoubleDown dispatch logs.

Declares expectations against the dispatch log after execution.
Matches on the full `{contract, operation, args, result}` tuple —
including results, which is meaningful because DoubleDown handlers
(especially `Repo.Stub`) do real computation (changeset validation,
PK autogeneration, timestamps).

## Basic usage

    DoubleDown.Log.match(:insert, fn
      {_, _, [%Changeset{data: %Thing{}}], {:ok, %Thing{}}} -> true
    end)
    |> DoubleDown.Log.reject(:delete)
    |> DoubleDown.Log.verify!(MyContract)

Matcher functions only need the positive matching clauses —
`FunctionClauseError` is caught and interpreted as "didn't match".
No need for a `_ -> false` catch-all, though returning `false`
explicitly can be useful for excluding specific values that are
hard to exclude with pattern matching alone.

## Matching on results

Unlike Mox/Mimic where asserting on return values would be
circular (you wrote the stub), DoubleDown handlers do real
computation. Matching on results is a meaningful assertion:

    DoubleDown.Log.match(:insert, fn
      {_, _, [%Changeset{data: %Thing{}}],
       {:ok, %Thing{id: id}}} when is_binary(id) -> true
    end)
    |> DoubleDown.Log.verify!(RepoContract)

## Counting occurrences

    DoubleDown.Log.match(:insert, fn
      {_, _, [%Changeset{data: %Discrepancy{}}], {:ok, _}} -> true
    end, times: 3)
    |> DoubleDown.Log.verify!(DoubleDown.Repo)

## Multi-contract

Build separate matcher chains and verify each against its contract:

    todos_log =
      DoubleDown.Log.match(:create_todo, fn {_, _, _, {:ok, _}} -> true end)

    repo_log =
      DoubleDown.Log.match(:insert, fn {_, _, _, {:ok, _}} -> true end)

    DoubleDown.Log.verify!(todos_log, MyApp.Todos)
    DoubleDown.Log.verify!(repo_log, DoubleDown.Repo)

## Matching modes

### Loose (default)

Matchers must be satisfied in order within each operation, but
other log entries are allowed between them. Different operations
are matched independently (no cross-operation ordering):

    DoubleDown.Log.match(:insert, matcher)
    |> DoubleDown.Log.match(:update, matcher)
    |> DoubleDown.Log.verify!(MyContract)
    # Passes if log contains an insert and an update,
    # regardless of other entries or relative order.

### Strict

Every log entry for the contract must be matched.
No unmatched entries allowed:

    DoubleDown.Log.match(:insert, matcher)
    |> DoubleDown.Log.match(:update, matcher)
    |> DoubleDown.Log.verify!(MyContract, strict: true)

## Relationship to existing APIs

Built on `DoubleDown.Testing.get_log/1`. Completely decoupled from
handler choice — works with `Repo.Stub`, `Repo.OpenInMemory`,
`set_fn_handler`, `set_stateful_handler`, or `DoubleDown.Double`.

Can be used alongside `DoubleDown.Double` — Handler for fail-fast
validation and producing return values, Log for after-the-fact
result inspection.

# `expectation`

```elixir
@type expectation() :: {:match, atom(), matcher(), pos_integer()} | {:reject, atom()}
```

# `matcher`

```elixir
@type matcher() :: (tuple() -&gt; boolean())
```

# `t`

```elixir
@type t() :: %DoubleDown.Log{expectations: [expectation()]}
```

# `match`

Add a match expectation for an operation.

The matcher function receives the full log tuple
`{contract, operation, args, result}` and should return a truthy
value for entries that match. Write only the clauses that should
match — `FunctionClauseError` is caught and treated as "didn't
match".

The accumulator argument is optional — when omitted, a fresh
accumulator is created via `new/0`.

## Options

  * `:times` — require exactly `n` matching entries (default 1).

# `match`

```elixir
@spec match(t(), atom(), matcher(), keyword()) :: t()
```

# `new`

```elixir
@spec new() :: t()
```

Create an empty log expectation accumulator.

# `reject`

Add a reject expectation for an operation.

Verification will fail if the operation appears anywhere in the
log for the contract.

The accumulator argument is optional — when omitted, a fresh
accumulator is created via `new/0`.

# `reject`

```elixir
@spec reject(t(), atom()) :: t()
```

# `verify!`

```elixir
@spec verify!(t(), module(), keyword()) :: {:ok, list()}
```

Verify all expectations against the dispatch log for a contract.

Reads the dispatch log via `DoubleDown.Testing.get_log/1` and
checks that all match expectations are satisfied and all reject
expectations hold.

## Options

  * `:strict` — when `true`, every log entry for the contract
    must be matched by some matcher. Unmatched entries cause
    verification to fail. Default `false` (loose mode).

Returns `{:ok, log}` where `log` is the full dispatch log for the
contract — useful in the REPL for inspecting what happened.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
