StatifierUI.Expression (StatifierUI v0.3.0)

Copy Markdown View Source

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.

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(["order.total"])
...> |> Enum.find(&(&1.insert == "order.total"))
%{label: "order.total", insert: "order.total", kind: "path", detail: "declared path"}

Summary

Types

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

Functions

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

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

Whether the resolved predicator exposes Predicator.Vocabulary.

Types

completion()

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

Functions

completions(candidates \\ [], opts \\ [])

@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(completions)

@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

vocabulary_available?()

@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