# `Predicator.Simple`
[🔗](https://github.com/riddler/predicator-ex/blob/v9.4.1/lib/predicator/simple.ex#L1)

The subset of the expression language a picklist-style editor can render.

A structured authoring surface - the row of dropdowns that reads
*field / operator / value*, repeated down a form - cannot render an
arbitrary expression. It has no place to put a parenthesis, no second
precedence level, and no way to draw `NOT`. What it can render is a flat
list of comparisons joined by one connective, and that is exactly what this
module names as a value.

`t:t/0` is that value: a list of `t:clause/0`s joined by one
`t:connective/0`. A clause is `{path, op, value}` - a field path, a
comparison or membership operator, and a literal (or a list of literals).
Nothing else is in the subset.

    iex> {:ok, simple} = Predicator.Simple.from_source("status == 'active' AND amount >= 500")
    iex> simple.connective
    :and
    iex> Predicator.Simple.to_source(simple)
    "status == 'active' AND amount >= 500"

## Outside the subset

Everything the editor cannot draw answers `:outside` - a plain atom, not an
error, because being outside the subset is an ordinary and expected answer
about a perfectly valid expression. Mixed `AND`/`OR`, parentheses, `NOT`,
arithmetic, function calls, casts, and object literals are all outside, by
decision rather than by omission.

    iex> Predicator.Simple.from_source("status == 'active' AND (amount >= 500 OR plan == 'pro')")
    :outside

    iex> Predicator.Simple.from_source("amount + 1 >= 500")
    :outside

`:outside` is not a judgement about the expression. `from_source/1` keeps
`{:error, error}` for source that does not parse at all, so a caller can
tell "this is a valid expression my editor cannot draw" from "this is not an
expression".

    iex> {:error, error} = Predicator.Simple.from_source("status == ==")
    iex> error.position
    {1, 11}

## Round-tripping

Two laws hold for every well-formed `t:t/0`, and
`test/predicator/simple_test.exs` exercises both over an enumerated corpus:

- `from_ast(to_ast(simple)) == {:ok, simple}`
- `to_source(simple)` parses to `to_ast(simple)`, modulo source positions

The second law is what constrains the subset's edges. It is why the subset
admits `:equal_equal` but not `:eq`, admits `{:string_literal, value, style}`
but not a bare binary `{:literal, value}`, and admits only non-negative
numbers - in each case the excluded shape decompiles to source that parses
back as something else, so a value carrying it could not survive a trip
through the editor. See "What the subset admits" below.

## What the subset admits

A `t:path/0` is an identifier, optionally followed by property and bracket
accesses: `status`, `card.brand`, `cart['items']`. A bracket key is a string
or a non-negative integer, never a computed expression.

A `t:value/0` is one scalar or a list of scalars. A scalar is a non-negative
integer, a non-negative float, a boolean, a string (carrying the quote style
it was written with), a date, a datetime, a duration, or a relative date.

Two exclusions are deliberate and each has a reason:

| Excluded | Why |
|---|---|
| Negative numbers | The parser reads `-5` as a `unary` node, never as a negative literal, so a negative literal could not have come from a parse and does not survive one |
| `:eq`, and a bare binary `{:literal, "text"}` | Both decompile to source that parses back as a different node (`:equal_equal` and `:string_literal`), breaking the source round-trip |

Float literals were a third exclusion until px-gv1. It was contingent, not
structural: `Predicator.decompile/2` had no clause for a float and raised,
so `to_source/2` could not have stayed total. px-ggb gave the writer that
clause, so the reason went and the exclusion went with it.

## Integers and floats are one kind, and two shapes

A float is its own `t:scalar/0` shape, `{:float, 19.99}`, because the AST
literal it builds differs from an integer's and the round-trip laws have to
preserve which one an author wrote.

    iex> Predicator.Simple.from_source("card.amount >= 19.99")
    {:ok, %Predicator.Simple{connective: nil, clauses: [{[root: "card", property: "amount"], :gte, {:float, 19.99}}]}}

It is **not** its own `t:Predicator.Vocabulary.value_kind/0`. For choosing an
operator both are `:number`, since every operator worth offering beside
`19.99` is one worth offering beside `500`. See `operators/1`.

## Which operators an editor offers

`operators/1` answers, for one kind of value, which operators a row of the
form should offer. `value_kind/1` answers which kind a value is, so
the pair composes into the whole question a row asks and a consumer keeps
no translation of its own. The answer is read from `Predicator.Vocabulary` - the
labels, the arities, the AST atoms and the per-kind admissions all live on
the operator entries there, so this module holds no second copy of the
grammar and a change to the language reaches the editor without one.

    iex> Predicator.Simple.operators(:list)
    [%{op: :in, lexeme: "IN", label: "is one of", arity: 2}]

The lexeme is the spelling `to_source/2` renders, not merely a spelling the
lexer accepts: a word operator is enumerated in both cases (`in` and `IN`),
and offering the one the decompiler does not write would put a label beside
a row that renders differently the moment it is saved.

## Purity

Nothing here evaluates, and nothing here raises on input.
`from_ast/1` is total over every `t:Predicator.Parser.ast/0`: the answer to a
node it does not recognise is `:outside`, never an exception.

# `clause`

```elixir
@type clause() :: {path(), op(), value()}
```

One row of the editor: a field path, an operator, and a value.

# `connective`

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

The connective joining the clauses.

`nil` for a single clause, which is joined to nothing. A `t:t/0` carrying one
clause and a non-`nil` connective is not well-formed - see `well_formed?/1`.

# `op`

```elixir
@type op() ::
  :gt
  | :gte
  | :lt
  | :lte
  | :equal_equal
  | :ne
  | :strict_eq
  | :strict_ne
  | :in
  | :contains
```

A comparison or membership operator, spelled as the AST spells it.

# `path`

```elixir
@type path() :: [segment(), ...]
```

A field path: a root identifier and the accesses that follow it.

# `scalar`

```elixir
@type scalar() ::
  {:integer, non_neg_integer()}
  | {:float, float()}
  | {:boolean, boolean()}
  | {:string, binary(), :single | :double}
  | {:date, Date.t()}
  | {:datetime, DateTime.t()}
  | {:duration, [{non_neg_integer(), binary()}, ...]}
  | {:relative_date, [{non_neg_integer(), binary()}, ...],
     :ago | :future | :next | :last}
```

A single value in a clause.

A string carries the quote style it was written with, so `plan == 'pro'`
decompiles back to single quotes rather than switching house style under the
author.

# `segment`

```elixir
@type segment() ::
  {:root, binary()}
  | {:property, binary()}
  | {:key, binary() | non_neg_integer()}
```

One step along a field path.

The first segment of a path is always `{:root, name}` and no later segment
ever is. A `{:key, ...}` segment is a bracket access and a `{:property, ...}`
segment is a dotted one; the two are kept apart because `cart['items']` and
`cart.items` are different source text for the same lookup.

# `t`

```elixir
@type t() :: %Predicator.Simple{clauses: [clause(), ...], connective: connective()}
```

The subset value: clauses joined by one connective.

`:clauses` is never empty. `:connective` is `nil` exactly when there is one
clause.

# `value`

```elixir
@type value() :: scalar() | {:list, [scalar()]}
```

The right-hand side of a clause: one scalar, or a list of them.

# `duration_units`

```elixir
@spec duration_units() :: [binary()]
```

The duration units a `t:scalar/0` duration may use.

Read from `Predicator.Vocabulary`, which is the grammar's single enumerated
source (px-15q), so a unit added to the lexer reaches this subset without a
second list to update by hand.

## Examples

    iex> "d" in Predicator.Simple.duration_units()
    true

# `from_ast`

```elixir
@spec from_ast(Predicator.Parser.ast()) :: {:ok, t()} | :outside
```

Reads a `t:t/0` out of an AST, or answers `:outside`.

Total: every `t:Predicator.Parser.ast/0` gets an answer, and an AST outside
the subset gets `:outside` rather than an exception. Source positions are
ignored, so a hand-built node carrying `nil` reads the same as a parsed one.

## Examples

    iex> {:ok, ast} = Predicator.parse("amount >= 500")
    iex> Predicator.Simple.from_ast(ast)
    {:ok, %Predicator.Simple{connective: nil, clauses: [{[root: "amount"], :gte, {:integer, 500}}]}}

    iex> {:ok, ast} = Predicator.parse("NOT plan == 'pro'")
    iex> Predicator.Simple.from_ast(ast)
    :outside

# `from_source`

```elixir
@spec from_source(binary()) ::
  {:ok, t()} | :outside | {:error, Predicator.Errors.ParseError.t()}
```

Parses source and reads a `t:t/0` out of it.

Adds one arm to `from_ast/1`'s two: `{:error, error}` for source that does
not parse, carrying a `t:Predicator.Errors.ParseError.t/0` with the position
and span of the failure (ADR-0015). The three arms answer three different
questions - in the subset, a valid expression outside it, and not an
expression - and a caller that collapses any two of them loses information
an editor needs.

## Examples

    iex> Predicator.Simple.from_source("step in ['payment', 'review']")
    {:ok,
     %Predicator.Simple{
       connective: nil,
       clauses: [{[root: "step"], :in, {:list, [{:string, "payment", :single}, {:string, "review", :single}]}}]
     }}

    iex> Predicator.Simple.from_source("len(step) > 0")
    :outside

    iex> {:error, %Predicator.Errors.ParseError{}} = Predicator.Simple.from_source("step in [")
    iex> :ok
    :ok

# `operators`

```elixir
@spec operators(Predicator.Vocabulary.value_kind()) :: [
  %{op: op(), lexeme: binary(), label: binary(), arity: 2}
]
```

The operators an editor should offer for a value of `kind`.

One entry per operator, in the order `Predicator.Vocabulary` enumerates
them, carrying everything a picklist row needs: `:op` is the atom to put in
a `t:clause/0`, `:lexeme` is the spelling `to_source/2` renders, `:label` is
the phrase to show, and `:arity` is how many operands the operator takes -
always 2 here, since every operator in the subset is a comparison or a
membership test.

Nothing is enumerated locally, and nothing about the vocabulary is re-derived
here. The admissions are the `:value_kinds` on the vocabulary's operator
entries, the atoms are its `:ast_op`s, and the choice between the two
spellings of a word operator is its `:canonical` marker - so an operator this
subset does not admit, or one the grammar does not admit for this kind of
value, is never offered, and neither is the lower-case spelling of one
`to_source/2` would render in upper case.
`test/predicator/simple_test.exs` holds that as an invariant over every kind.

`kind` is guarded against `Predicator.Vocabulary.value_kinds/0`, so a
misspelled kind raises `FunctionClauseError` rather than answering with the
empty list that a kind with no operators would answer with. `:number` is the
kind for an integer and for a float alike - there is no `:float` kind to ask
for, by the decision recorded above.

## Examples

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

    iex> Predicator.Simple.operators(:number) |> Enum.find(&(&1.op == :gte))
    %{op: :gte, lexeme: ">=", label: "is at least", arity: 2}

    iex> Predicator.Simple.operators(:list) |> Enum.map(& &1.lexeme)
    ["IN"]

# `to_ast`

```elixir
@spec to_ast(t()) :: Predicator.Parser.ast()
```

Builds the AST for a `t:t/0`.

Every node carries `nil` in its trailing position slot - the value records
what the expression means, not where it was written. Clauses are joined
left-associatively, which is the shape `Predicator.Parser.parse/2` produces
for the same source.

Expects a well-formed value; see `well_formed?/1`.

## Examples

    iex> {:ok, simple} = Predicator.Simple.from_source("plan == 'pro'")
    iex> Predicator.Simple.to_ast(simple)
    {:comparison, :equal_equal, {:identifier, "plan", nil}, {:string_literal, "pro", :single, nil}, nil}

# `to_source`

```elixir
@spec to_source(
  t(),
  keyword()
) :: binary()
```

Renders a `t:t/0` back to source, through `Predicator.decompile/2`.

`opts` are that function's formatting options - `:parentheses` and
`:spacing` - so a caller that renders the rest of its expressions one way
renders these the same way.

## Examples

    iex> {:ok, simple} = Predicator.Simple.from_source("status == 'active' AND amount >= 500")
    iex> Predicator.Simple.to_source(simple)
    "status == 'active' AND amount >= 500"

    iex> {:ok, simple} = Predicator.Simple.from_source("plan == 'pro'")
    iex> Predicator.Simple.to_source(simple, spacing: :compact)
    "plan=='pro'"

# `value_kind`

```elixir
@spec value_kind(value()) :: Predicator.Vocabulary.value_kind()
```

The `t:Predicator.Vocabulary.value_kind/0` that governs `value`.

`operators/1` answers which operators belong beside a kind of value.
This answers which kind a value is, so the two compose into the whole
question an editor row asks - `value |> value_kind() |> operators()` - and
no consumer has to keep its own translation from the subset's scalar tags
to the vocabulary's kinds. Two consumers writing that translation
separately could disagree about the same value; there is one answer here
instead, and it is this package's.

The mapping is not one-to-one, and the two places it is not are decisions
rather than omissions.

An integer and a float are two `t:scalar/0` shapes and one kind, `:number`,
for the reason recorded above: every operator worth offering beside `19.99`
is one worth offering beside `500`.

    iex> Predicator.Simple.value_kind({:integer, 500})
    :number

    iex> Predicator.Simple.value_kind({:float, 19.99})
    :number

A relative date is `:datetime`. There is no `:relative_date` kind and this
is not a gap left for a consumer to judge: `30d ago` is a point in time by
the time anything compares it - `Predicator.Evaluator` resolves it against
`DateTime.utc_now/0` and pushes a `t:DateTime.t/0` - so the operators that
apply to it are the datetime operators, and a kind of its own would name
the same list under a second name.

    iex> Predicator.Simple.value_kind({:relative_date, [{30, "d"}], :ago})
    :datetime

    iex> Predicator.Simple.operators(:datetime) == Predicator.Simple.operators(:date)
    true

A list is `:list` whatever its members are, which is what makes `IN` the
operator offered for it.

    iex> Predicator.Simple.value_kind({:list, [{:string, "payment", :single}]})
    :list

# `well_formed?`

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

Answers whether a value satisfies the invariants `to_ast/1` expects.

Every value `from_ast/1` returns is well-formed; this function is for
values a caller assembled itself - an editor building one from form state.
The invariants are that `:clauses` is a non-empty list of structurally valid
clauses, and that `:connective` is `nil` exactly when there is one clause.

## Examples

    iex> {:ok, simple} = Predicator.Simple.from_source("amount >= 500")
    iex> Predicator.Simple.well_formed?(simple)
    true

    iex> Predicator.Simple.well_formed?(%Predicator.Simple{connective: :and, clauses: [{[root: "amount"], :gte, {:integer, 500}}]})
    false

---

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