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

Reads a `<chart>.fixtures.json` sidecar (ADR-0003) and produces the same
`StatifierUI.Fixtures` struct the behaviour-based delivery path produces.

Uses the stdlib `JSON` module (Elixir 1.18) to decode, never `jason` -
`jason` reaches this repository only through the optional
`phoenix_live_view` dependency and must not become load-bearing for core
fixture loading.

Every top-level key beyond `"version"`, `"scenarios"`, `"events"`,
`"datasets"`, and `"expressions"` is ignored with a `:unknown_key`
diagnostic (ADR-0006's extension-friendly requirement), and every value
under `"scenarios"`, `"events"`, and `"datasets"` is decoded through
`StatifierUI.Value.decode/1`.

A `$duration` cannot appear inside `"scenarios"` or `"datasets"`: it decodes
to an atom-keyed map, and a datamodel forbids atom keys at every level (the
engine's own invariant, tightened in `StatifierUI.Fixtures`). Such a
sidecar is rejected with `{:duration_in_scenario, path}` or
`{:duration_in_dataset, path}`. Durations under `"events"` are fine - an
`_event.data` payload has no key constraint.

An `"expressions"` entry is an object rather than a free value, so it is
decoded on its own: `"source"` passes through untouched (it is source text,
never a tagged value), each `"expect"` value is decoded through
`StatifierUI.Value.decode/1` (a duration is fine there - `"expect"` values
are predicator values, not datamodel keys), and any other key in the entry
is copied through verbatim rather than rejected or flagged, per ADR-0006's
ignore-unknown-keys discipline extended to this depth.

# `from_json`

```elixir
@spec from_json(
  map(),
  keyword()
) :: {:ok, StatifierUI.Fixtures.t()} | {:error, term()}
```

Converts an already JSON-decoded sidecar map into a validated
`StatifierUI.Fixtures` struct.

Options:

  * `:source` - the file the map was read from, recorded on each
    diagnostic and named in each logged warning. `load/1` passes it;
    omitting it is for a map that never was a file.

# `load`

```elixir
@spec load(Path.t()) :: {:ok, StatifierUI.Fixtures.t()} | {:error, term()}
```

Reads, parses, and validates a sidecar file at `path`, returning the same
struct `StatifierUI.Fixtures.from_source/1` produces.

Every error names `path`, because a corpus run loads many sidecars and a
bare POSIX reason does not say which one failed.

# `load_for_chart`

```elixir
@spec load_for_chart(Path.t()) :: {:ok, StatifierUI.Fixtures.t()} | {:error, term()}
```

Derives the sidecar path for `chart_path` and loads it.

A missing sidecar is `{:error, {:sidecar_not_found, path}}`, not an empty
bundle - whether "no fixtures" is acceptable is a caller-level decision,
and a typo'd chart path must not look like a chart without fixtures.

# `sidecar_path`

```elixir
@spec sidecar_path(Path.t()) :: Path.t()
```

Derives a sidecar path from a chart path, per ADR-0003's naming:
`authorization.scxml` becomes `authorization.fixtures.json`. A path with no
extension has `.fixtures.json` appended.

## Examples

    iex> StatifierUI.Fixtures.Sidecar.sidecar_path("authorization.scxml")
    "authorization.fixtures.json"

    iex> StatifierUI.Fixtures.Sidecar.sidecar_path("authorization")
    "authorization.fixtures.json"

---

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