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

Example data a host supplies for a chart: named scenario datamodels, example
event payloads, and named datasets for evaluating expressions (ADR-0003,
ADR-0006).

A fixture bundle is a set of maps:

  * `scenarios` - a scenario name (for example `"within-budget-account"`)
    mapped to a complete example of the host-supplied datamodel for that
    situation.
  * `events` - an event name (for example `"authorize.approved"`) mapped to
    a sample `_event.data` payload for that event.
  * `datasets` - a dataset name mapped to a situation for evaluating a
    free-standing expression against.

A scenario and a dataset are not the same thing, even though both are
string-keyed example data. A scenario is a complete example of the
host-supplied datamodel for running a chart; it stands in for the whole
state a chart would actually see. A dataset exists only to evaluate
expressions and may be as small as the expression needs - a two-key map
naming just the variables a single guard reads, with nothing else a real
chart run would carry. ADR-0006 keeps datasets separate from scenarios
rather than asking a scenario to double as both, so a dataset can stay
minimal and a scenario can stay a faithful example.

Two delivery paths converge on this one struct: a host module implementing
`StatifierUI.Fixtures.Source` (`from_source/1`), and a JSON sidecar next to
a chart (a later phase's loader). Every consumer downstream of either path
is indifferent to which one produced the struct.

`new/1` validates rather than raises: scenario datamodels must have string
keys at every level, so a fixture that would blow up deep inside the engine
is instead rejected at load time. Event payloads are preserved verbatim,
including the three-way `:undefined` / `nil` / map distinction predicator's
`_event.data` semantics require.

Scenario validation is **deliberately stricter than the engine's**, not a
mirror of it. `Statifier.MachineState.new/2` rejects only atom keys that
are not booleans, so it accepts an integer or boolean key; this rejects
every key that is not a string. The reason is convergence: a JSON object
key is always a string, so a scenario a behaviour source could supply but
a sidecar could never express would break the one-struct guarantee
ADR-0003 rests on. Rejecting it in both paths keeps them interchangeable.

A dataset is validated exactly as a scenario is - string keys at every
depth, for the same convergence reason - so a predicator duration cannot
appear inside a dataset either: it decodes to a bare atom-keyed map, and
the same rule that rejects it inside a scenario
(`test/support/fixtures/tagged_source.ex`) rejects it here.

`expressions` is the fourth map: an expression name mapped to a
free-standing predicator `"source"` string and an optional `"expect"` map
of expected results, keyed by dataset name. Unlike a scenario or a dataset,
an expression entry is not walked for string keys - it is validated only
for the shape ADR-0006 fixes (a `"source"` string, an `"expect"` map with
binary keys), because its `expect` *values* are predicator values in their
own right and a duration is a legal one there, unlike inside a datamodel.
An absent `"expect"` entry for a dataset name means no expectation is
stated for that dataset - not that the expression is expected to be
undefined against it - and `expect/3` returns `:error` for both that case
and an unknown expression name, since neither is a bundle error.

# `datamodel`

```elixir
@type datamodel() :: %{optional(String.t()) =&gt; term()}
```

An example datamodel: string keys at every level.

# `dataset_name`

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

The name of a fixture dataset.

# `diagnostic`

```elixir
@type diagnostic() :: %{
  kind: atom(),
  message: String.t(),
  path: [String.t()],
  source: Path.t() | nil
}
```

A diagnostic surfaced while loading a fixture bundle. `path` is the key
path inside the bundle; `source` is the file it came from, or `nil` for a
bundle that never was a file.

# `event_name`

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

The name of a fixture event, matching an SCXML event name.

# `expression`

```elixir
@type expression() :: %{required(String.t()) =&gt; term()}
```

A free-standing expression fixture: a predicator `"source"` string and an
optional `"expect"` map keyed by dataset name (ADR-0006). Keys beyond those
two are preserved verbatim.

# `expression_name`

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

The name of a fixture expression.

# `scenario_name`

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

The name of a fixture scenario.

# `t`

```elixir
@type t() :: %StatifierUI.Fixtures{
  datasets: %{optional(dataset_name()) =&gt; datamodel()},
  diagnostics: [diagnostic()],
  events: %{optional(event_name()) =&gt; term()},
  expressions: %{optional(expression_name()) =&gt; expression()},
  scenarios: %{optional(scenario_name()) =&gt; datamodel()}
}
```

# `dataset`

```elixir
@spec dataset(t(), dataset_name()) :: {:ok, datamodel()} | :error
```

Fetches a dataset by name.

# `dataset_names`

```elixir
@spec dataset_names(t()) :: [dataset_name()]
```

Dataset names, sorted (ADR-0005's canonical-order rule).

# `event`

```elixir
@spec event(t(), event_name()) :: {:ok, term()} | :error
```

Fetches an event's sample payload by name. Returns `:error` when the event
has no fixture, distinct from an event whose fixture value is `nil` or
`:undefined`.

# `event_names`

```elixir
@spec event_names(t()) :: [event_name()]
```

Event names, sorted (ADR-0005's canonical-order rule).

# `expect`

```elixir
@spec expect(t(), expression_name(), dataset_name()) :: {:ok, term()} | :error
```

Fetches the expected result stated for `expression_name` against
`dataset_name`.

Returns `:error` both when `expression_name` names no expression and when
the expression exists but states no expectation for `dataset_name` - an
absent `expect` key means no expectation is stated (ADR-0006), which is not
an error condition, so the two cases are deliberately not distinguished.

# `expression`

```elixir
@spec expression(t(), expression_name()) :: {:ok, expression()} | :error
```

Fetches an expression by name.

# `expression_names`

```elixir
@spec expression_names(t()) :: [expression_name()]
```

Expression names, sorted (ADR-0005's canonical-order rule).

# `from_source`

```elixir
@spec from_source(module()) :: {:ok, t()} | {:error, term()}
```

Builds a validated fixture bundle from a `StatifierUI.Fixtures.Source`
module.

Calls `module.scenarios()` and `module.example_events()`, and - when the
module implements the optional `datasets/0` and `expressions/0` callbacks -
`module.datasets()` and `module.expressions()`, and routes all of them
through `new/1`, so this path and the sidecar loader share exactly one
validation implementation. Returns `{:error, {:not_a_source, module}}` when
`module` does not implement the behaviour.

# `new`

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

Builds a validated fixture bundle from `:scenarios`, `:events`,
`:datasets`, and `:expressions` options.

All four options default to `%{}`. Returns `{:error, reason}` rather than
raising when a scenario, event, or dataset key is not a string, when a
scenario or dataset datamodel is not a map, or when one contains a
non-string key at any depth. A non-string key found inside a datamodel is
reported as `{:invalid_key, key, path}`, where `path` starts with the
scenario or dataset name - or, when the offending map is a predicator
duration, as `{:duration_in_scenario, path}` or `{:duration_in_dataset,
path}`, which says why rather than naming a unit key the author never
wrote.

## Examples

    iex> StatifierUI.Fixtures.new(scenarios: %{"within-budget-account" => %{"currency" => "USD"}})
    {:ok, %StatifierUI.Fixtures{scenarios: %{"within-budget-account" => %{"currency" => "USD"}}}}

# `scenario`

```elixir
@spec scenario(t(), scenario_name()) :: {:ok, datamodel()} | :error
```

Fetches a scenario's datamodel by name.

# `scenario_names`

```elixir
@spec scenario_names(t()) :: [scenario_name()]
```

Scenario names, sorted (ADR-0005's canonical-order rule).

---

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