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

Infers a structured shape from a predicator value, and renders that shape
as a display label.

`infer/1` walks a value from the predicator value domain (ADR-0003) and
produces a `t()` term describing its structure - scalars, lists, maps,
and the predicator-specific `:null` / `:undefined` / `:duration`
distinctions, plus `:redacted` for a value a projected trace stream is not
carrying (ADR-0012). `:redacted` is deliberately distinct from
`:undefined`: a redacted slot held a value, an undefined one did not, and
a datamodel pane that renders the first as the second reports a live
datamodel as permanently unbound. `label/2` renders that term as a short human-readable string
for use in the explorer tree; a later phase's editor completions consume
the structured term directly instead of the label.

Kept a pure, standalone module on purpose: it depends on nothing from
`StatifierUI.Fixtures` and nothing here reaches back into it.

# `t`

```elixir
@type t() ::
  :boolean
  | :integer
  | :float
  | :string
  | :date
  | :datetime
  | :duration
  | :null
  | :undefined
  | :redacted
  | :unknown
  | {:list, t() | :empty}
  | {:map, %{optional(String.t()) =&gt; t()}}
  | {:union, [t()]}
```

# `duration?`

```elixir
@spec duration?(term()) :: boolean()
```

Whether `value` is a predicator duration: a non-empty map whose keys are
atoms drawn from the eight duration units and whose values are all
integers.

A **subset** of the units is enough, deliberately, and that is a statement
about this repository rather than about predicator. Predicator's contract is
eight keys: `Predicator.Duration.new/1` fills every unit it was not given,
and since predicator 9.0 the expression evaluator seeds all eight too, so
`Predicator.evaluate("3d")` carries `milliseconds: 0` like the rest
(px-69c). Nothing here is working around that.

The rule is wider than the contract because this is a viewer. It renders a
value stream it did not produce - a fixture written by hand, a decoded wire
message, a datamodel from an older engine or another interpreter - and the
right response to a duration missing a unit is to render it as a duration,
not to fall back to a seven-field map and make the reader work out what
they are looking at. Tightening to exactly eight would trade that tolerance
for nothing: no caller gains a guarantee, because callers that need the
full eight keys go through `StatifierUI.Value.encode/1`, which canonicalizes
to all of them on the way out.

Nothing else in the value domain can collide: atom keys reach a fixture only
from Elixir, and a datamodel forbids them outright (`StatifierUI.Fixtures`),
so the only atom-keyed maps that arrive here are durations.

Public because `StatifierUI.Fixtures` and `StatifierUI.Value` need the same
rule, and three copies of it are what let the seven-versus-eight gap go
unnoticed once already.

## Examples

    iex> StatifierUI.Shape.duration?(%{days: 3, hours: 8})
    true

    iex> StatifierUI.Shape.duration?(%{"days" => 3})
    false

# `infer`

```elixir
@spec infer(term()) :: t()
```

Infers a `t()` shape from a predicator value.

Total over `term()`: anything outside the predicator value domain (a
tuple, a pid, a reference, an unrecognized struct) infers as `:unknown`
rather than raising.

## Examples

    iex> StatifierUI.Shape.infer(1999)
    :integer

    iex> StatifierUI.Shape.infer(%{"amount" => 1999})
    {:map, %{"amount" => :integer}}

    iex> StatifierUI.Shape.infer(["a", "b"])
    {:list, :string}

# `label`

```elixir
@spec label(
  t(),
  keyword()
) :: String.t()
```

Renders a `t()` shape as a short display label.

Options:

  * `:max_keys` - maximum map pairs shown before truncating to `...`
    (default `4`).
  * `:max_depth` - maximum nesting depth rendered before a nested map or
    list collapses to `map{...}` / `list<...>` (default `3`).

Truncation is a label-only concern; `infer/1` itself recurses fully.

## Examples

    iex> StatifierUI.Shape.label(StatifierUI.Shape.infer(%{"amount" => 1999}))
    "map{amount: integer}"

    iex> StatifierUI.Shape.label(StatifierUI.Shape.infer(["a", "b"]))
    "list<string>"

    iex> shape = StatifierUI.Shape.infer(%{"amount" => 1999, "currency" => "usd"})
    iex> StatifierUI.Shape.label(shape, max_keys: 1)
    "map{amount: integer, ...}"

---

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