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

Value casting and validation for facets ([Spec 018](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-018-rich-facet-types.md)).

One implementation, two entry points:

  * `cast/4` — the successful path, returning `{:ok, operator, value}`. The
    operator comes back because casting can *refine* it: `:` on a range
    facet resolves to `:between`, and on a `multiple?: true` facet resolves
    to `:in` or stays `:eq` depending on whether the literal has commas.
  * `validate/4` — the same work, discarding the value and returning
    `:ok | {:error, {reason, params}}`.

Because they share an implementation, a validation reason can never exist
without the cast having rejected the value that produced it, and vice
versa — which is what
[ADR-012](https://github.com/team-alembic/flicker/blob/main/docs/adrs/adr-012-parse-reports-invalid-facet-tokens.md)
needs to report a known facet's bad value instead of silently degrading it.

## The grammar this casts

Locale-invariant throughout, per
[ADR-013](https://github.com/team-alembic/flicker/blob/main/docs/adrs/adr-013-canonical-tokens-localised-display.md):
ISO 8601 dates, `.` as the decimal separator, `..` for ranges, `,` for
lists, canonical preset and enum ids. No locale reaches this module.

  * **Scalars** — `42`, `1.5`, `true`, `2026-07-01`, `2026-07-01T09:30:00Z`,
    plus relative dates (`7d`, `2w`, `1m`, `1y`) and named dates (`today`,
    `yesterday`).
  * **Durations** — `90s`, `15m`, `2h30m`, `1d`, cast to integer seconds.
  * **Ranges** — `from..to`, `..to`, `from..`, or a bare scalar (a
    single-day range for `:date_range`; the whole UTC day for
    `:datetime_range`), or a preset token.
  * **Lists** — `a,b,c`, only on a `multiple?: true` facet.

## Reference date and week start

`cast/4` and `validate/4` take `:today` and `:first_day_of_week` opts
rather than reading the clock or a locale, so relative dates and
week-sensitive presets are testable at a fixed point. They default to
`Date.utc_today/0` and `1` (Monday, the ISO 8601 default) — which is what
`Flicker.Query.parse/2` passes when a caller says nothing.

# `reason`

```elixir
@type reason() :: {atom(), map()}
```

Why a value was rejected, and the details a message needs.

# `cast`

```elixir
@spec cast(Flicker.Facet.t(), String.t(), Flicker.Facet.operator(), keyword()) ::
  {:ok, Flicker.Facet.operator(), term()} | {:error, reason()}
```

Casts `raw` for `facet` under the requested `operator`.

Returns `{:ok, resolved_operator, value}`, or `{:error, {reason, params}}`
— never raises, for any input.

## Examples

    iex> facet = %Flicker.Facet{key: :price, type: :integer}
    ...> Flicker.Facet.Cast.cast(facet, "42", :eq)
    {:ok, :eq, 42}

    iex> facet = %Flicker.Facet{key: :price, type: :number_range}
    ...> Flicker.Facet.Cast.cast(facet, "10..50", :between)
    {:ok, :between, %Flicker.Facet.Range{from: 10, to: 50}}

    iex> facet = %Flicker.Facet{key: :price, type: :integer}
    ...> Flicker.Facet.Cast.cast(facet, "abc", :eq)
    {:error, {:bad_integer, %{value: "abc"}}}

# `message_key`

```elixir
@spec message_key(atom()) :: atom()
```

The `Flicker.Messages` key that renders `reason`.

Unknown reasons — a host's own `:validate` returning something bespoke —
fall back to `:invalid_value`, so a custom reason renders a generic message
instead of crashing the render.

## Examples

    iex> Flicker.Facet.Cast.message_key(:bad_integer)
    :invalid_integer

    iex> Flicker.Facet.Cast.message_key(:not_in_values)
    :invalid_not_in_values

    iex> Flicker.Facet.Cast.message_key(:something_a_host_invented)
    :invalid_value

# `range_types`

```elixir
@spec range_types() :: [Flicker.Facet.type()]
```

The range-typed facet types — the ones whose cast value is a
`Flicker.Facet.Range`.

## Examples

    iex> Flicker.Facet.Cast.range_types()
    [:date_range, :datetime_range, :number_range]

# `reasons`

```elixir
@spec reasons() :: [atom()]
```

Every reason a value can be rejected with.

The single source of truth for the failure taxonomy, so a test can sweep it
and prove `Flicker.Messages` answers for each — the same way
`Flicker.ThemeTest` sweeps theme parts. A new reason that nobody wrote a
message for fails that test rather than rendering an empty error.

## Examples

    iex> :not_in_values in Flicker.Facet.Cast.reasons()
    true

# `validate`

```elixir
@spec validate(Flicker.Facet.t(), String.t(), Flicker.Facet.operator(), keyword()) ::
  :ok | {:error, reason()}
```

`cast/4` with the value discarded — `:ok`, or the reason it failed.

## Examples

    iex> facet = %Flicker.Facet{key: :price, type: :integer}
    ...> Flicker.Facet.Cast.validate(facet, "42", :eq)
    :ok

    iex> facet = %Flicker.Facet{key: :price, type: :integer, bounds: %{min: 0, max: 100}}
    ...> Flicker.Facet.Cast.validate(facet, "900", :eq)
    {:error, {:out_of_bounds, %{min: 0, max: 100, value: 900}}}

---

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