# `DoubleDown.Repo.OpenInMemory`
[🔗](https://github.com/mccraigmccraig/double_down/blob/main/lib/double_down/repo/open_in_memory.ex#L5)

Stateful in-memory Repo fake (open-world).

Uses **open-world** semantics: the state may be incomplete. If a
record is not found in state, the adapter falls through to the
fallback function — it cannot assume the record doesn't exist.

For most use cases, prefer `DoubleDown.Repo.InMemory` (closed-world)
which is authoritative for all bare-schema reads without a
fallback. Use `OpenInMemory` when the state is deliberately
partial — e.g. you've inserted some records but expect the
fallback to provide others.

Implements `DoubleDown.Contract.Dispatch.FakeHandler`, so it can
be used by module name with `Double.fake`:

## Usage with Double.fake

    # PK reads only — no fallback needed for records in state:
    DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.OpenInMemory)

    # With seed data and fallback for non-PK reads:
    DoubleDown.Double.fake(
      DoubleDown.Repo,
      DoubleDown.Repo.OpenInMemory,
      [%User{id: 1, name: "Alice"}],
      fallback_fn: fn
        _contract, :all, [User], state ->
          Map.get(state, User, %{}) |> Map.values()
        _contract, :get_by, [User, [email: "alice@example.com"]], _state ->
          %User{id: 1, name: "Alice"}
      end
    )

    # Layer expects for failure simulation:
    DoubleDown.Repo
    |> DoubleDown.Double.fake(DoubleDown.Repo.OpenInMemory)
    |> DoubleDown.Double.expect(:insert, fn [changeset] ->
      {:error, Ecto.Changeset.add_error(changeset, :email, "taken")}
    end)

## Operation dispatch (3-stage)

| Category | Operations | Behaviour |
|----------|-----------|-----------|
| **Writes** | `insert`, `update`, `delete` | Always handled by state |
| **PK reads** | `get`, `get!` | State first, then fallback |
| **get_by** | `get_by`, `get_by!` | PK lookup when PK in clauses, then fallback |
| **Other reads** | `one`, `all`, `exists?`, `aggregate` | Always fallback |
| **Bulk** | `insert_all`, `update_all`, `delete_all` | Always fallback |
| **Transactions** | `transact`, `rollback` | Delegate to sub-operations |

**Note:** Unlike `Repo.InMemory` (closed-world), bulk operations
(`insert_all`, `update_all`, `delete_all`) in OpenInMemory always
delegate to the fallback function and do **not** mutate in-memory
state. This is consistent with open-world semantics — the state
is partial, so bulk mutations could produce incorrect results.

For reads, the dispatch stages are:

1. **State lookup** — if the record is in state, return it
2. **Fallback function** — a 4-arity `(contract, operation, args, state)`
   function that handles operations the state can't answer
3. **Raise** — clear error suggesting a fallback clause

## When to use which Repo fake

| Fake | State | Best for |
|------|-------|----------|
| `Repo.Stub` | None | Fire-and-forget writes, canned reads |
| `Repo.InMemory` | Complete store | All bare-schema reads; ExMachina factories |
| **`Repo.OpenInMemory`** | **Partial store** | **PK reads in state, fallback for rest** |

## See also

- `DoubleDown.Repo.InMemory` — closed-world variant (recommended).
  Authoritative for all bare-schema reads without a fallback.
- `DoubleDown.Repo.Stub` — stateless stub for fire-and-forget writes.

# `store`

```elixir
@type store() :: DoubleDown.Repo.Impl.InMemoryShared.store()
```

# `dispatch`

```elixir
@spec dispatch(module(), atom(), list(), store()) :: {term(), store()}
```

Stateful handler function for use with `DoubleDown.Testing.set_stateful_handler/3`
or `DoubleDown.Double.fake/2..4`.

Handles all `DoubleDown.Repo` operations. The function signature is
`(contract, operation, args, store) -> {result, new_store}`.

Write operations are handled directly by the state. PK-based reads check
the state first, then fall through to the fallback function. All other
reads go directly to the fallback function. If no fallback is registered
or the fallback doesn't handle the operation, an error is raised.

# `new`

```elixir
@spec new(
  term(),
  keyword()
) :: store()
```

Create a new InMemory state map.

## Arguments

  * `seed` — seed data to pre-populate the store. Accepts:
    - a list of structs: `[%User{id: 1, name: "Alice"}]`
    - a pre-built store map: `%{User => %{1 => %User{id: 1}}}`
    - `%{}` or `[]` for empty (default)
  * `opts` — keyword options:
    - `:fallback_fn` — a 4-arity function `(contract, operation, args, state) -> result`
      that handles operations the state cannot answer authoritatively. The
      `state` argument is the clean store map (without internal keys like
      `:__fallback_fn__`), so the fallback can compose canned data with
      records inserted during the test. If the function raises
      `FunctionClauseError`, dispatch falls through to an error.

## Examples

    # Empty state, no fallback
    DoubleDown.Repo.OpenInMemory.new()

    # Seeded with a list of structs
    DoubleDown.Repo.OpenInMemory.new([%User{id: 1, name: "Alice"}])

    # Seeded with a map
    DoubleDown.Repo.OpenInMemory.new(%{User => %{1 => %User{id: 1, name: "Alice"}}})

    # Seeded with fallback
    DoubleDown.Repo.OpenInMemory.new(
      [%User{id: 1, name: "Alice"}],
      fallback_fn: fn
        _contract, :all, [User], state ->
          Map.get(state, User, %{}) |> Map.values()
      end
    )

## Legacy keyword-only form (still supported)

    DoubleDown.Repo.OpenInMemory.new(seed: [%User{id: 1}], fallback_fn: fn ...)

# `seed`

```elixir
@spec seed([struct()]) :: store()
```

Convert a list of structs into the nested state map for seeding.

## Example

    DoubleDown.Repo.OpenInMemory.seed([
      %User{id: 1, name: "Alice"},
      %User{id: 2, name: "Bob"}
    ])
    #=> %{User => %{1 => %User{id: 1, name: "Alice"},
    #               2 => %User{id: 2, name: "Bob"}}}

---

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