# `Flicker.Query`
[🔗](https://github.com/team-alembic/flicker/blob/v0.1.0/lib/flicker/query.ex#L1)

A search request passed to `c:Flicker.Provider.search/2`, and the result
of parsing faceted-search input ([Spec 003](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-003-faceted-search.md)).

## Fields

  * `:text` — the raw free-text portion, as typed (facet tokens
    stripped out, quotes on whole-token free text unwrapped). Providers
    decide how to match it (the built-in `Flicker.Providers.AshResource`
    runs `ilike`-style matching over its configured search fields); an
    empty string is a valid query (the picker's open-with-no-input
    state — providers typically return a default listing rather than an
    empty result set).
  * `:facets` — active facet filters, as `{key, operator, value}`
    tuples, in the order they appeared in the input. The same key can
    appear more than once (`parse/2` doesn't deduplicate) — repeated
    instances of the same facet are meant to OR together (see
    `to_filter/1`).
  * `:input` — the verbatim string `parse/2` was called with, untouched
    (facet tokens still in place, quoting/casing/whitespace exactly as
    typed). `:text` and `:facets` are *derived* from it and lossy in
    both directions (facet tokens are stripped from `:text`;
    `:facets`' cast values can't reproduce the original literal, e.g.
    `true`/`TRUE`, `7d` vs. its resolved date); `:input` is the one
    field a round-trip (e.g. [Spec 009](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-009-cinder-interop.md)
    Level 2's URL-state serialisation) can re-`parse/2` and get back the
    exact same query — the source of truth to persist, never `:text`
    or a reconstruction of `:facets`.

# `facet_match`

```elixir
@type facet_match() :: {atom(), Flicker.Facet.operator(), term()}
```

A parsed facet filter: `{key, operator, cast value}`.

# `t`

```elixir
@type t() :: %Flicker.Query{
  facets: [facet_match()],
  input: String.t(),
  invalid: [Flicker.Query.Invalid.t()],
  text: String.t()
}
```

A search request / parsed query.

# `parse`

```elixir
@spec parse(String.t(), [Flicker.Facet.t()], keyword()) :: t()
```

Parses `input` into a `Flicker.Query`, given the facets it should
recognise.

This is a pure function: the same `(input, facets)` pair always produces
the same result, and it never raises — arbitrary typing (unterminated
quotes, unknown keys, malformed operators, stray backslashes, mid-token
unicode) always degrades to free text rather than erroring. `facets` is
a list of `Flicker.Facet.t()`; keys not present in it are never
recognised as facets.

## Grammar

Input is tokenised on whitespace, except whitespace inside a
double-quoted span (`worker:"Casey Nguyen"` is one token). Inside quotes,
`\"` is a literal quote and `\\` is a literal backslash; any other
backslash is kept as-is. An unterminated quote absorbs the rest of the
input rather than erroring — the whole thing degrades to free text.

A token matches the facet grammar when it looks like `key<op>value`,
where `key` is one or more Unicode letters/digits/underscores (a trailing
`?`, as in `active?`, is allowed too), `op` is
one of `:` `>=` `<=` `!=` `>` `<`, and `value` is either a quoted span or
a run of non-whitespace characters. `key:value` is a match only when:

  * `key` names a facet in `facets`,
  * `op` is `:` (mapped to that facet's `:default_op`) or an operator in
    that facet's `:operators` list,
  * `value` casts cleanly to that facet's `:type` (see `Flicker.Facet`).

Any failure at any of those steps — unknown key, disallowed operator,
uncastable value, malformed quoting — degrades the *entire* token to
free text (appended to `:text` as typed, with a whole-token surrounding
quote pair stripped and unescaped).

## Examples

    iex> facets = [
    ...>   %Flicker.Facet{key: :status, type: :enum, values: [:active, :inactive]},
    ...>   %Flicker.Facet{key: :worker}
    ...> ]
    ...>
    ...> Flicker.Query.parse(~s(status:active worker:"Casey Nguyen" visit notes), facets)
    %Flicker.Query{
      text: "visit notes",
      facets: [{:status, :eq, :active}, {:worker, :eq, "Casey Nguyen"}],
      input: ~s(status:active worker:"Casey Nguyen" visit notes)
    }

    iex> Flicker.Query.parse("unknown:value free text", [])
    %Flicker.Query{text: "unknown:value free text", facets: [], input: "unknown:value free text"}

# `to_filter`

```elixir
@spec to_filter(t(), [Flicker.Facet.t()]) :: map()
```

Builds an Ash `filter_input`-shaped map from `query.facets`: distinct
facet keys AND together, repeated instances of the same facet key OR
together. Returns `%{}` (a no-op filter) when there are no facets.

This only compiles when `ash` is present — the boundary
[ADR-006](https://github.com/team-alembic/flicker/blob/main/docs/adrs/adr-006-core-depends-only-on-provider.md)
draws around Ash-specific capabilities. It shapes plain data (no
resource, no live query, no `Ash.*` call) for
`Ash.Query.filter_input/2` to consume — e.g.
`Ash.Query.filter_input(query, Flicker.Query.to_filter(parsed))`.

`facets` (a list of `Flicker.Facet.t()`, typically the facet registry
passed to `parse/2`) resolves each matched key to its
`Flicker.Facet.target/1` — a relationship path, aggregate, or
expression-calc name builds a nested filter map instead of a flat
attribute clause. A key with no matching facet (or the default `[]`)
falls back to using the key as the attribute name directly.

## Examples

    iex> query = %Flicker.Query{text: "", facets: [{:status, :eq, :active}]}
    ...> Flicker.Query.to_filter(query)
    %{"status" => %{"eq" => :active}}

    iex> query = %Flicker.Query{
    ...>   text: "",
    ...>   facets: [{:city, :eq, "Melbourne"}, {:city, :eq, "Sydney"}]
    ...> }
    ...>
    ...> Flicker.Query.to_filter(query)
    %{"or" => [%{"city" => %{"eq" => "Melbourne"}}, %{"city" => %{"eq" => "Sydney"}}]}

    iex> query = %Flicker.Query{text: "", facets: [{:worker, :eq, "id-123"}]}
    ...> facets = [%Flicker.Facet{key: :worker, target: [:worker, :full_name]}]
    ...> Flicker.Query.to_filter(query, facets)
    %{"worker" => %{"full_name" => %{"eq" => "id-123"}}}

# `valid?`

```elixir
@spec valid?(t()) :: boolean()
```

Whether every recognised facet token in this query cast cleanly.

A convenience for callers who want the all-or-nothing reading. It is
deliberately *not* how components gate dispatch: validation is per-facet, so
one broken token contributes no filter clause while every other facet and the
free text still run
([ADR-012](https://github.com/team-alembic/flicker/blob/main/docs/adrs/adr-012-parse-reports-invalid-facet-tokens.md)).

## Examples

    iex> Flicker.Query.valid?(Flicker.Query.parse("anything", []))
    true

    iex> facets = [Flicker.Facet.new(key: :price, type: :integer)]
    ...> Flicker.Query.parse("price:abc", facets) |> Flicker.Query.valid?()
    false

---

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