StatifierUI.Shape (StatifierUI v0.1.0)

Copy Markdown View Source

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. 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.

Summary

Functions

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.

Infers a t() shape from a predicator value.

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

Types

t()

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

Functions

duration?(value)

@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. Predicator.Duration's own builder returns all eight, but predicator's expression parser returns seven - Predicator.evaluate("3d") omits :milliseconds - so requiring the full set misses every duration produced by evaluating an expression, which is the common case. 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(value)

@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(shape, opts \\ [])

@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, ...}"