# `StatifierUI.Fixtures.Bundle`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/fixtures/bundle.ex#L1)

A fixture bundle that travels with one reusable chart fragment rather than
with a whole chart: the ADR-0003/ADR-0006 bundle plus the fragment's name
and a record of where it was loaded from.

ADR-0003 pairs a bundle with a *chart* - `authorization.scxml` carries
`authorization.fixtures.json` beside it. An embedder composing charts from a
palette of reusable fragments has no such file to sit beside: the fragment
is a module in the host's code or an entry in a palette, and the chart it
eventually lands in does not exist yet. This module is the same contract
addressed by fragment name instead of by chart path, so each palette entry
can carry its own executable examples and surface a per-fragment "test this
step" panel.

Nothing here is a new fixture shape. A bundle's `fixtures` field is exactly
the `StatifierUI.Fixtures` struct both ADR-0003 delivery paths already
produce, and every spelling below routes through `StatifierUI.Fixtures.new/1`
or `StatifierUI.Fixtures.Sidecar.from_json/2` for validation. What this
record adds is identity (which fragment these examples belong to),
provenance (which module or file they came from), and discovery (finding
every fragment's bundle across a palette or a directory).

## The convention

A fragment supplies its bundle in one of four spellings, and `load/3`
accepts all four:

| Spelling | Recognized by | Validated through |
|---|---|---|
| `%StatifierUI.Fixtures{}` | the struct | already validated at construction |
| `%{scenarios: ..., events: ..., datasets: ..., expressions: ...}` | **atom** top-level keys | `StatifierUI.Fixtures.new/1` |
| `%{"version" => 1, "datasets" => ...}` | **string** top-level keys | `StatifierUI.Fixtures.Sidecar.from_json/2` |
| `"path/to/step.fixtures.json"` | a binary path | `StatifierUI.Fixtures.Sidecar.load/1` |

The atom-vs-string top-level key is the whole discriminator, and it is not
an accident of implementation: atom keys are the Elixir spelling a host
writes by hand in a module, string keys are the JSON spelling that survives
a file, and ADR-0003 requires both paths to converge on one struct rather
than one path to be primary. A map mixing the two is rejected as
`{:mixed_bundle_keys, name}` instead of guessed at.

**Unknown top-level keys are treated differently by spelling, deliberately.**
The JSON spelling keeps the sidecar's ignore-unknown-keys discipline
(ADR-0006): a file written by a newer producer must still load. The Elixir
spelling rejects an unknown atom key as `{:unknown_bundle_key, name, key}`,
because an atom key in a host's own module is compiled code the author is
looking at, and a silently ignored `:datsets` typo there is a bundle that
reports zero datasets and no reason why. Forward compatibility is a
property of a wire format, not of a function call.

A fragment that ships no examples at all is not an error anywhere in this
module. `discover/2` reports it as `without`, never as a failure.

## Discovery

Two discovery paths, mirroring the two delivery paths:

  * `discover/2` walks a palette - a map or list of `{name, module}` pairs -
    and loads each module's bundle callback (`fixtures/0` by default,
    `:callback` to name another). This is the host-application path.
  * `discover_dir/2` walks a directory of `*.fixtures.json` files and names
    each bundle after its file (`authorize.fixtures.json` becomes `"authorize"`).
    This is the corpus and CLI path, and it is what lets a palette of
    fragments travel as files with no host code around them.

Neither raises and neither is all-or-nothing: one fragment's malformed
bundle is reported against that fragment's name and the rest still load,
because a palette is exactly the setting where the alternative - one bad
entry hiding every good one - is least useful.

This module names no block, palette, or fragment type of its own and takes
no dependency on any package that defines one. It calls a zero-arity
callback on whatever modules it is handed, which is all a bundle convention
needs to be.

## Rendering

`StatifierUI.Fixtures.Bundle.Markdown` renders a bundle as its truth table
plus its expectation results - the per-fragment test panel - and
`StatifierUI.Kino.test_panel/2` wraps that for a Livebook cell.

# `discovery`

```elixir
@type discovery() :: %{bundles: [t()], without: [name()], errors: [{name(), term()}]}
```

What `discover/2` and `discover_dir/2` found.

`bundles` are the ones that loaded, sorted by name. `without` names the
entries that ship no bundle at all - an absence, not a failure. `errors`
pairs a name with the reason its bundle did not load.

# `name`

```elixir
@type name() :: String.t()
```

The fragment a bundle belongs to: a palette entry name or a file basename.

# `origin`

```elixir
@type origin() :: {:module, module()} | {:sidecar, Path.t()} | :inline
```

Where a bundle came from. `{:module, mod}` for a palette entry's callback,
`{:sidecar, path}` for a file, `:inline` for a term handed straight to
`load/3`.

# `t`

```elixir
@type t() :: %StatifierUI.Fixtures.Bundle{
  diagnostics: [StatifierUI.Fixtures.diagnostic()],
  fixtures: StatifierUI.Fixtures.t(),
  name: name(),
  origin: origin()
}
```

# `by_name`

```elixir
@spec by_name(discovery()) :: %{optional(name()) =&gt; t()}
```

Every bundle in `discovery`, keyed by name.

A convenience for a consumer that holds a palette by name and wants the
matching bundle without scanning the list.

# `discover`

```elixir
@spec discover(
  %{optional(name()) =&gt; module()} | [{name(), module()}],
  keyword()
) :: discovery()
```

Loads the bundle every entry of `palette` ships.

`palette` is a map or list of `{name, module}` pairs - a palette of
fragment types, however the caller happens to hold it. For each entry, when
the module exports the bundle callback it is called and its return value
goes through `load/3`; when it does not, the entry is reported under
`without`.

Options:

  * `:callback` - the zero-arity function to call. Defaults to `:fixtures`.

Never raises: a module whose callback itself raises is reported as
`{:bundle_callback_raised, exception}` against its name, because one
fragment's broken examples must not take down the panel for every other
fragment in the palette.

# `discover_dir`

```elixir
@spec discover_dir(
  Path.t(),
  keyword()
) :: {:ok, discovery()} | {:error, term()}
```

Loads every `*.fixtures.json` file directly inside `dir`, naming each
bundle after its file: `authorize.fixtures.json` becomes `"authorize"`.

The directory is not walked recursively - a palette directory is a flat
list of fragments, and a nested one is a second palette, not a deeper part
of this one.

A missing or unreadable directory is `{:error, reason}`; a directory that
exists and holds no sidecars is an empty discovery, which is a legitimate
answer rather than a failure.

# `empty?`

```elixir
@spec empty?(t()) :: boolean()
```

Whether the bundle carries anything at all to evaluate.

A bundle with no expressions has no truth table and no expectations to
run; a renderer says so once rather than drawing an empty matrix and an
empty results list.

# `load`

```elixir
@spec load(name(), term(), keyword()) :: {:ok, t()} | {:error, term()}
```

Builds a bundle named `name` from `term`, in any of the four spellings
above.

Options:

  * `:origin` - the `t:origin/0` to record. Defaults to `:inline`, or to
    `{:sidecar, path}` when `term` is a path.

Never raises. A term in none of the four spellings is
`{:error, {:unrecognized_bundle, name, term}}`.

## Examples

    iex> {:ok, bundle} =
    ...>   StatifierUI.Fixtures.Bundle.load("myapp.signup", %{
    ...>     datasets: %{
    ...>       "variant-b-complete" => %{
    ...>         "signup" => %{"steps_completed" => 4, "variant" => "B"}
    ...>       }
    ...>     },
    ...>     expressions: %{
    ...>       "is-complete-variant-b" => %{
    ...>         "source" => "signup.steps_completed >= 3 and signup.variant == 'B'"
    ...>       }
    ...>     }
    ...>   })
    iex> bundle.name
    "myapp.signup"
    iex> StatifierUI.Fixtures.dataset_names(bundle.fixtures)
    ["variant-b-complete"]

# `load!`

```elixir
@spec load!(name(), term(), keyword()) :: t()
```

Like `load/3` but raises `ArgumentError` on failure.

For a host that wants a malformed bundle to fail loudly at wiring time
rather than be carried around as an error tuple.

# `name_from_path`

```elixir
@spec name_from_path(Path.t()) :: name()
```

Derives a bundle name from a sidecar filename:
`"authorize.fixtures.json"` becomes `"authorize"`.

The inverse of `StatifierUI.Fixtures.Sidecar.sidecar_path/1` for the
fragment case, where the name is the identity and the path is derived from
it rather than the other way round.

## Examples

    iex> StatifierUI.Fixtures.Bundle.name_from_path("palette/authorize.fixtures.json")
    "authorize"

---

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