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

The completion source behind the expression-editing component: predicator's
own grammar vocabulary, plus the datamodel paths a host declares.

This module is pure and touches no LiveView, the same split
`StatifierUI.Live.State` gives the ops panes. `StatifierUI.Live.ExpressionInput`
renders what `completions/2` returns; everything about *what* an author can
be offered is decided here, where it is testable without a browser and
without Phoenix.

## Two sources, and only two

A completion is either a **declared datamodel path** - supplied by the caller,
because only the host knows its own datamodel - or a **lexeme of the
predicator grammar**, read from `Predicator.Vocabulary` (px-15q). Nothing is
invented here. An operator this module offered that the lexer does not accept
would be a second, drifting copy of the grammar, which is precisely the
duplication `Predicator.Vocabulary` was published to prevent.

## The grammar half degrades

`Predicator.Vocabulary` is newer than the predicator releases this package
can resolve, so its absence is a supported state rather than a broken one: a
host on an older predicator gets its declared paths and no grammar entries,
and the component renders a plain input with a path list. The module is
reached through `Application.get_env(:statifier_ui, :predicator_vocabulary,
Predicator.Vocabulary)` and guarded with `Code.ensure_loaded?/1`, so nothing
raises and nothing warns at compile time.

## The picklist half

`simple/2` answers a second question about a source string: not "what could
be typed next" but "can a row of dropdowns draw this at all". It reads
`Predicator.Simple` (px-84i), the upstream module that names the
picklist-renderable subset, and returns the clause rows a renderer walks.

The three answers `Predicator.Simple.from_source/1` keeps apart are kept
apart here too, because an editor needs all three: source inside the subset,
a valid expression outside it, and text that is not an expression at all.
Collapsing the middle one into an error would tell an author their working
condition is broken.

`Predicator.Simple` is resolved the way `Predicator.Vocabulary` is, through
`Application.get_env(:statifier_ui, :predicator_simple, Predicator.Simple)`,
but the guard on it is wider than a `Code.ensure_loaded?/1`:
`simple_available?/0` also requires the resolved module to export
`from_source/1`, `to_source/1`, `operators/1` and `value_kind/1`. A host on
an older predicator - or one that points that key at a module missing any of
the four - degrades every function this guard gates, exactly as
`simple_available?/0`'s own documentation lists them: `simple/2` answers
`:outside` for every source string, `operators/1` answers `[]`, and
`source/2`, `value_source/2` and `segments/1` answer `:error`. That is the
answer that makes the component fall back to its plain text input.

## Shape

Every completion is a map with four keys:

- `:label` - what a completion list shows (`"len(...)"`, `"contains"`)
- `:insert` - the text written into the source at the caret
- `:kind` - `"path"`, `"function"`, or the px category of a lexeme
  (`"comparison"`, `"logical"`, ...), which is what a list groups by
- `:detail` - one line of prose, or `nil` when the source carries none

## Examples

    iex> StatifierUI.Expression.completions(["card.brand"])
    ...> |> Enum.find(&(&1.insert == "card.brand"))
    %{label: "card.brand", insert: "card.brand", kind: "path", detail: "declared path"}

# `advisory`

```elixir
@type advisory() :: %{
  severity: :info,
  reason: :value_kind | :operator,
  message: String.t()
}
```

Something worth telling an author about a clause, and never a reason to
refuse one.

`:reason` is `:value_kind` when the value in the source is not of the kind
the host declared for the path, read through the operator, and `:operator`
when the operator in the source is one the declared kind's list does not
offer - which is the very condition that keeps it offered anyway.
`:contains` never raises the second one: it takes its collection on the
left, so the path's declared kind says nothing about whether it belongs.

An advisory is `:info` and only `:info`. ADR-0007 rules out refusing or
rewriting an author's expression, so a declaration that disagrees with the
source is reported and nothing else.

# `candidate`

```elixir
@type candidate() :: %{label: String.t(), value: term()}
```

One offer in a value dropdown, as the host declared it.

# `completion`

```elixir
@type completion() :: %{
  label: String.t(),
  insert: String.t(),
  kind: String.t(),
  detail: String.t() | nil
}
```

One offer: what to show, what to write, how to group it.

# `connective`

```elixir
@type connective() :: :and | :or | nil
```

The connective joining a picklist's clause rows.

`nil` for a single row, which is joined to nothing - the same invariant
`Predicator.Simple` carries.

# `declared_kind`

```elixir
@type declared_kind() ::
  value_kind()
  | :number
  | {:one_of, [candidate() | term()]}
  | {:shape, [StatifierDatamodel.Types.member()]}
```

A kind a host declares for a datamodel path, as the `:path_types` map carries
it: one of this module's own `t:value_kind/0`s, `:number`,
`{:one_of, values}` for a path whose values the host enumerates, or
`{:shape, members}` for one holding a structure whose members it names.

`{:one_of, _}`, `:number` and `{:shape, _}` are the caller's tags rather than
grammar kinds - they are the vocabulary a datamodel document uses, not ones
`Predicator.Vocabulary` names.

For `{:one_of, _}` the operators offered beside one are the operators of the
kind its own values are: binaries are `:string`, integers `:integer`, floats
`:float`, booleans `:boolean`, and an empty or mixed list falls back to
`:string`. Inferring from the host's own values keeps that judgement to
arithmetic over data the host supplied, rather than to a table about the
grammar.

`:number` is `StatifierDatamodel.Index.path_types/1`'s own tag: a datamodel
document declares `integer` and `decimal` and the projection answers
`:number` for both, because the expression language draws no line between
them either. It is admitted here rather than translated at the seam so that
the projection's map is a `:path_types` map as it stands - the same map,
whether a host hands over its document or the projection of it. Nothing else
changes: `:number` is already the grammar's own word, so it asks for the same
operators `:integer` and `:float` ask for, and it compares equal to both.

`{:shape, members}` is `t:StatifierDatamodel.Types.t/0`'s inline arm, carried
here as sd spells it - `%{name:, type:, required?:}` per member, and a
member's own `type` is a type expression again, so a shape nests. It is the
one entry a document cannot write: sd's amendment gives an inline shape no
document spelling, so `StatifierDatamodel.Index.path_types/1` never answers
one and a path carries it only when a consumer holding a structural value the
host never declared - `statifier_blocks`' environment, or a host with its own
reader - puts it in the map itself.

A shape describes a *structure*, not a value the grammar compares, so at the
path that holds it a shape declares nothing: the row offers the operators and
the control an undeclared path offers, and raises no advisory. It says its
whole piece one segment down. A read of `card.brand` where `card` is declared
a shape is typed by the `brand` member, member-wise, exactly as though the
host had declared `card.brand` directly - and an exact entry for the longer
path still wins, because a host that spelled it out has said the more
specific thing.

A member's `required?` is the shape's *promise*, and it decides nothing here.
An unpromised member is still a path an author may read, and an editor that
hid it would be refusing a read the expression language allows; so a member
is found by name whether the shape promises it or not, and the promise is
carried rather than consulted. Deciding what an absent value does is the read
check's job (`StatifierDatamodel.Types.satisfies?/3`), and it is not this
module's.

# `operator`

```elixir
@type operator() :: %{
  op: atom(),
  lexeme: String.t(),
  label: String.t(),
  detail: String.t() | nil
}
```

One entry in an operator dropdown, as `Predicator.Simple.operators/1`
answers it.

`:op` is the atom a clause is built with and `:lexeme` is the source
spelling `Predicator.Simple.to_source/1` writes for it - the two halves
that have to agree, and the reason a picklist can offer an operator
without spelling one itself. `:label` is the grammar's own display phrase
(`"is at least"` for `">="`), a UI string that is never stored, and
`:detail` its one-line description, or `nil` when `Predicator.Vocabulary`
is not resolvable.

# `row`

```elixir
@type row() :: %{
  path: String.t(),
  segments: [tuple()],
  op: atom(),
  op_label: String.t(),
  value: term(),
  value_kind: value_kind(),
  declared_kind: declared_kind() | nil,
  control_kind: value_kind(),
  value_source: String.t(),
  operators: [operator()],
  candidates: [candidate()],
  advisories: [advisory()]
}
```

One row of the picklist: everything a renderer needs to draw a
field / operator / value line, and nothing it would have to compute itself.

`:segments` and `:value` are `Predicator.Simple`'s own structural forms, kept
so a renderer can hand an edited row straight back to
`Predicator.Simple.to_ast/1`; `:path`, `:op_label`, and `:value_source` are
the same three things spelled the way the source spells them.

`:op_label` is therefore a source spelling, which is the one place in this
module where "label" means the opposite of what it means next door: on
`t:operator/0` `:label` is the grammar's display phrase and `:lexeme` is the
spelling. A dropdown draws `t:operator/0`'s `:label`; `:op_label` is what
the expression carries.

`:value_kind` is what the value in the source *is*; `:declared_kind` is what
the host said the path holds, or `nil` when it said nothing; and
`:control_kind` is the one a renderer picks a control from. The three are
kept apart because they answer different questions and can disagree: a
declaration decides which operators are offered, but it never decides the
shape of a control, because a control that wrote a shape the source does not
have would rewrite the author's expression. When they disagree,
`:advisories` says so and nothing is changed.

# `value_kind`

```elixir
@type value_kind() ::
  :integer
  | :float
  | :boolean
  | :string
  | :date
  | :datetime
  | :duration
  | :relative_date
  | {:list, value_kind() | nil}
```

What kind of value a clause row holds, which is what decides its operator
list and its value control.

A list carries the kind of its members, or `nil` when it is empty.

These are the shapes a clause value takes, which is not the vocabulary
`Predicator.Vocabulary.value_kinds/0` names: it has one `:number` where
this has `:integer` and `:float`, and no `:relative_date` at all.
`operators/1` translates between the two, and nothing else needs to.

# `completions`

```elixir
@spec completions(
  [String.t()],
  keyword()
) :: [completion()]
```

Every completion available to an expression field: the declared paths first,
then the grammar.

`candidates` is the declared datamodel path list - what
`StatifierBlocks.Datamodel.candidates/3` returns, arriving through the
`expression_component` seam as `:candidates`. Paths lead because they are the
ones an author cannot look up.

`opts` is passed through to `Predicator.Vocabulary.functions/1`, so a host
with its own `Predicator.FunctionProvider` modules offers exactly the
functions its own contexts will accept.

## Examples

    iex> StatifierUI.Expression.completions() |> Enum.any?(&(&1.insert == ">="))
    true

    iex> StatifierUI.Expression.completions([], builtins: false)
    ...> |> Enum.any?(&(&1.kind == "function"))
    false

# `datalist`

```elixir
@spec datalist([completion()]) :: [completion()]
```

The subset a native `<datalist>` can usefully offer: the word-shaped
completions.

A `<datalist>` filters its options against the whole field value, and it
cannot insert at a caret. Offering `"::"` or `"("` through one is noise, so
the no-JavaScript affordance carries paths, keywords, and function names and
leaves the symbol operators to the hook.

## Examples

    iex> StatifierUI.Expression.completions() |> StatifierUI.Expression.datalist()
    ...> |> Enum.any?(&(&1.insert == "::"))
    false

# `operators`

```elixir
@spec operators(value_kind()) :: [operator()]
```

The operators a picklist offers beside a value of the given kind, read from
`Predicator.Simple.operators/1`.

Eligibility is the grammar's answer, not this module's: which operators are
worth offering beside a date as opposed to a number is decided by the
`:value_kinds` predicator stamps on its own operator entries, so an operator
the lexer would reject - or one the grammar does not admit for this kind of
value - cannot be offered here. Until px-84i landed that function there was
a table here instead, and ADR-0007's 2026-09-04 amendment - accepted
2026-09-05 - named it as the one local exception; delegating closes it.

`:op` builds the clause, `:lexeme` is the spelling the expression will
carry, `:label` is the display phrase, and `:detail` is the grammar's
one-line description, or `nil` when `Predicator.Vocabulary` is not
resolvable.

Empty when `simple_available?/0` is false, for the same reason `simple/2`
answers `:outside`: there is nothing truthful to offer. An atom the `@spec`
does not admit is handed to upstream unchanged rather than rejected here, so
one that happens to name a `Predicator.Vocabulary` kind - `:number`,
`:list` - gets the grammar's answer for that kind, and any other atom raises
there. The raise is upstream's own stance and the right one: an empty list
would say "no operators here" about a kind that does not exist.

## Examples

    iex> StatifierUI.Expression.operators(:boolean) |> Enum.map(& &1.op)
    [:equal_equal, :strict_eq, :ne, :strict_ne, :contains]

    iex> StatifierUI.Expression.operators({:list, :string}) |> Enum.map(& &1.lexeme)
    ["IN"]

    iex> StatifierUI.Expression.operators(:integer) |> Enum.find(&(&1.op == :gte))
    %{op: :gte, lexeme: ">=", label: "is at least", detail: "Greater than or equal to"}

# `relative_date_candidates`

```elixir
@spec relative_date_candidates() :: [candidate()]
```

The relative dates offered beside a path a host declared `:date` or
`:datetime`.

A date declaration carries no value list of its own the way `{:one_of, _}`
does, and a date field with only a free-text control asks an author to spell
a grammar they should not have to know. So a small fixed set is offered
instead. Which dates those are is presentation, which ADR-0007's picklist
amendment leaves to the component rather than to the record; the set lives
here rather than in the component so it is testable without a browser, and a
host wanting different ones supplies `:value_candidates` for the path, which
wins.

Every entry is checked against `Predicator.Simple.duration_units/0` before it
is offered, so a unit the resolved grammar does not know is dropped rather
than written into a source string the writer would refuse.

Empty when `simple_available?/0` is false, and empty when the resolved module
predates `duration_units/0` - that function is deliberately **not** added to
`simple_available?/0`'s four-export guard, because a predicator that can
classify and write should lose the offered dates, not the whole picklist.

## Examples

    iex> StatifierUI.Expression.relative_date_candidates() |> Enum.map(& &1.label)
    ["1d ago", "7d ago", "30d ago", "90d ago", "7d from now", "30d from now"]

# `segments`

```elixir
@spec segments(String.t()) :: {:ok, [tuple()]} | :error
```

The path segments a declared datamodel path parses to.

A picklist's field dropdown offers the paths a host declared, as strings.
Swapping a clause onto one of them needs that path in the structural form a
clause carries, and the only honest way to get there is predicator's own
parser - a path split on dots here would read `account['tags']` wrong and
would be a second parser besides.

`:error` for a string that is not a path, and for every string when
`Predicator.Simple` is not resolvable.

## Examples

    iex> StatifierUI.Expression.segments("card.brand")
    {:ok, [root: "card", property: "brand"]}

    iex> StatifierUI.Expression.segments("amount >= 500")
    :error

# `simple`

```elixir
@spec simple(
  String.t(),
  keyword()
) :: {:ok, [row()], connective()} | :outside | {:error, term()}
```

Classifies a source string against the picklist-renderable subset.

Three answers, and they are three different questions:

- `{:ok, rows, connective}` - inside the subset. `rows` is one `t:row/0` per
  clause and `connective` is `nil` for a single row, `:and` or `:or` for two
  or more.
- `:outside` - a valid expression a picklist cannot draw. Offer the text
  editor; this is not an error.
- `{:error, error}` - the source does not parse, carrying predicator's own
  parse error with the position of the failure.

`opts` takes `:value_candidates` - a map from a clause's `:path` to the
values a host offers for it, as `t:candidate/0` maps or bare strings. Only
the host knows its own value sets, so nothing is inferred here; a path with
no entry gets an empty list and the renderer falls back to a free-text value
control.

`opts` also takes `:path_types` - a map from a clause's `:path` to the
`t:declared_kind/0` the host declares for it. A declaration decides which
operators the row offers, asked of the grammar exactly as an observed kind
is; it fills the candidate list for a `{:one_of, _}` path that
`:value_candidates` says nothing about, and offers
`relative_date_candidates/0` for a `:date` or `:datetime` one. It never
decides a control whose shape the source does not have, and it never
rewrites, coerces, or refuses anything: a declaration the source disagrees
with lands in `:advisories`. A path with no entry behaves exactly as it did
before there was a map at all.

An entry may also be `{:shape, members}` - sd's inline arm, which a consumer
builds and no document spells. A read *into* one is typed by the member it
names, member-wise and through nesting, so `card.brand` takes the `brand`
member's type where `card` is declared a shape; an exact entry for the longer
path still wins. At the path holding the shape itself nothing is declared:
a structure is not a kind the grammar compares, so the row offers what an
undeclared path offers and raises no advisory.

Only the host knows its own datamodel, so nothing is read from one here.
The caller builds the map - `statifier_blocks`, or any host with a datamodel
document to read - which is why this package takes no dependency on one.

When `simple_available?/0` is false - the resolved predicator has no
`Predicator.Simple`, or the module the `:predicator_simple` key points at
does not export all four of `from_source/1`, `to_source/1`, `operators/1`
and `value_kind/1` - every source string answers `:outside`. That is a degraded
answer rather than a wrong one: the component renders the text input it
would render for an unsupported expression.

## Examples

    iex> {:ok, [row], nil} = StatifierUI.Expression.simple("plan == 'pro'")
    iex> {row.path, row.op, row.value_source}
    {"plan", :equal_equal, "'pro'"}

    iex> {:ok, rows, connective} = StatifierUI.Expression.simple("status == 'active' AND amount >= 500")
    iex> {length(rows), connective}
    {2, :and}

    iex> StatifierUI.Expression.simple("status == 'active' AND (amount >= 500 OR plan == 'pro')")
    :outside

    iex> {:error, error} = StatifierUI.Expression.simple("amount >= >=")
    iex> error.position
    {1, 11}

    iex> {:ok, [row], nil} =
    ...>   StatifierUI.Expression.simple("step in ['payment', 'review']",
    ...>     value_candidates: %{"step" => ["payment", "review", "confirmation"]}
    ...>   )
    iex> {row.value_kind, Enum.map(row.candidates, & &1.value)}
    {{:list, :string}, ["payment", "review", "confirmation"]}

    iex> {:ok, [row], nil} =
    ...>   StatifierUI.Expression.simple("plan == 'pro'", path_types: %{"plan" => :boolean})
    iex> {row.control_kind, Enum.map(row.operators, & &1.lexeme)}
    {:boolean, ["==", "===", "!=", "!==", "CONTAINS"]}

    iex> {:ok, [row], nil} =
    ...>   StatifierUI.Expression.simple("amount >= 500", path_types: %{"amount" => :string})
    iex> Enum.map(row.advisories, & &1.reason)
    [:value_kind]

    iex> {:ok, [row], nil} =
    ...>   StatifierUI.Expression.simple("card.brand == 'visa'",
    ...>     path_types: %{
    ...>       "card" => {:shape, [%{name: "brand", type: :string, required?: false}]}
    ...>     }
    ...>   )
    iex> {row.declared_kind, row.control_kind, row.advisories}
    {:string, :string, []}

# `simple_available?`

```elixir
@spec simple_available?() :: boolean()
```

Whether the resolved predicator exposes a usable `Predicator.Simple`.

The counterpart to `vocabulary_available?/0`, and the same distinction: a
component stamps it so a host can tell "this expression is outside the
subset" from "this predicator cannot answer the question".

Usable is four exports, not one loaded module: `from_source/1`,
`to_source/1`, `operators/1` and `value_kind/1` all have to be there. Under
the `~> 9.4` requirement that is inert, since every admitted predicator
carries all four; it is a host overriding `:predicator_simple` that the
condition measures, and a stub exporting three of the four reads as
unavailable - which is why the fourth joined the guard when this module
started asking upstream what kind a value is. Every
function this guard gates then degrades together: `simple/2` to `:outside`,
`operators/1` to `[]`, and `source/2`, `value_source/2` and `segments/1` to
`:error`. A picklist is lost silently, so a host stubbing this module is
stubbing the whole surface.

## Examples

    iex> is_boolean(StatifierUI.Expression.simple_available?())
    true

# `source`

```elixir
@spec source([row() | {[tuple()], atom(), term()}], connective()) ::
  {:ok, String.t()} | :error
```

Writes a source string back from the rows `simple/2` returned.

The write half of the same round trip: `simple/2` reads source into rows and
this reads rows back into source, both through `Predicator.Simple`, so a
picklist never has to spell an operator, a quote, or a connective itself.
That is what makes the rendered dropdowns a *view* of the source text rather
than a second representation of the condition - a renderer edits a row's
`:segments`, `:op`, or `:value`, asks for the source, and stores the string
it gets back.

`:error` when `Predicator.Simple` is not resolvable, or when `rows` is
empty: there is no source string to write, and inventing one would be the
duplication this module exists to avoid.

## Examples

    iex> {:ok, rows, connective} =
    ...>   StatifierUI.Expression.simple("status == 'active' AND amount >= 500")
    iex> StatifierUI.Expression.source(rows, connective)
    {:ok, "status == 'active' AND amount >= 500"}

    iex> {:ok, [row], nil} = StatifierUI.Expression.simple("plan == 'pro'")
    iex> StatifierUI.Expression.source([%{row | value: {:string, "free", :single}}], nil)
    {:ok, "plan == 'free'"}

    iex> StatifierUI.Expression.source([], nil)
    :error

# `value_candidates`

```elixir
@spec value_candidates(
  %{optional(String.t()) =&gt; [candidate() | String.t()]},
  String.t()
) :: [
  candidate()
]
```

The values a host offers for one clause path, normalized.

`candidates` is `simple/2`'s `:value_candidates` map. An entry may be a
`t:candidate/0` map or a bare string, because a declared path list is
usually already a list of strings and making a caller wrap each one buys
nothing.

`simple/2` folds this into every row it returns. It is public because a
renderer adding a *new* row has a path with no clause behind it yet, and
still needs its value list.

## Examples

    iex> StatifierUI.Expression.value_candidates(%{"step" => ["payment", "review"]}, "step")
    [%{label: "payment", value: "payment"}, %{label: "review", value: "review"}]

    iex> StatifierUI.Expression.value_candidates(%{}, "plan")
    []

# `value_source`

```elixir
@spec value_source(atom(), term()) :: {:ok, String.t()} | :error
```

The source text one clause value is written as, on its own.

`source/2` covers every edit a renderer makes to a whole expression. This
covers the one case it cannot: a control that has to *compose* a value - a
free-text field that types into a quoted string, a multi-select that builds
a list - needs the spellings of the pieces, and asking for them here keeps
them coming from `Predicator.Simple.to_source/1` rather than from a quoting
rule written a second time in JavaScript.

`op` is the operator the value sits beside, because `:in` is the one
operator whose right-hand side is a list.

## Examples

    iex> StatifierUI.Expression.value_source(:equal_equal, {:string, "pro", :single})
    {:ok, "'pro'"}

    iex> StatifierUI.Expression.value_source(:gte, {:integer, 500})
    {:ok, "500"}

    iex> StatifierUI.Expression.value_source(:in, {:list, [{:string, "payment", :single}]})
    {:ok, "['payment']"}

# `vocabulary_available?`

```elixir
@spec vocabulary_available?() :: boolean()
```

Whether the resolved predicator exposes `Predicator.Vocabulary`.

The component stamps this on the rendered input so a host - or a test - can
tell "no grammar completions offered" from "grammar completions offered and
none matched".

## Examples

    iex> is_boolean(StatifierUI.Expression.vocabulary_available?())
    true

---

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