# `Mutare.Ecto.Changeset.Routing`
[🔗](https://github.com/foxbenjaminfox/mutare_ecto/blob/v0.4.2/lib/mutare/ecto/changeset/routing.ex#L1)

The per-argument routing classifier for the `Ecto.Changeset` pipeline stages the plugin mutates
(`Mutare.Ecto.Changeset.stages/0` — every validator, constraint, and Repo-time hook it can
drop) — the changeset counterpart of the query classifier `Mutare.Ecto.Host.Routing`. Nothing
here is hosted; the classifier only holds back from core's families the positions where a
core swap would be a **crash, not a mutant**, so a run that lists the plugin next to `:all`
reports no crasher in a changeset stage. The test of each pin is Ecto's own handling of the
swapped value:

  * A **written field atom** in the field position (`validate_length(cs, :name, …)`,
    `unique_constraint(cs, :email)`) names a column — structural, like `field/2`'s name in a
    query — and routes `:raw`. Core's atom family would swap it for `:mutare`, an unknown field
    Ecto raises on. Anything else in that slot — a field *list*
    (`validate_required(cs, [:name, :email])`), a variable, a `prepare_changes` function — routes
    `:expression`, so a list stays reachable for core's list families.
  * The **option keys** of `validate_number` route per-pair `{:keyword, …}`, which leaves every
    key raw while the values stay `:expression`. Ecto rejects unsupported options
    (`ArgumentError: unknown option`), so a swapped key raises — every written key is raw, no
    key set is consulted. The bound literal keeps core's off-by-one mutants, and
    `Mutare.Ecto.ValidationBoundary` swaps strict and non-strict keys.
  * `validate_length`'s keys are **not** held back: Ecto reads `count:`/`is:`/`min:`/`max:` and
    ignores anything else, so core's `min:` → `mutare:` is a live mutant — that one bound gone,
    the call otherwise intact — a more specific mutation than the whole-call `:validation_drop`.
    Only a **written mode atom** in its `count:` value (`:graphemes`/`:codepoints`/`:bytes`) is
    held back, by a keyed refinement (`[:expression, count: :raw]`): `:mutare` is no mode, and
    Ecto's mode dispatch has no clause for it on a string field (a `CaseClauseError`). A
    computed value stays `:expression`, so core can mutate how the mode is selected. The
    options of every other stage (a constraint's `name:`, a `message:` alone) stay
    `:expression` — the plugin defines no option restrictions for unlisted stages.

The field position is the second argument in both spellings: core hands a pipe stage over as
the direct call it is sugar for, so `cs |> validate_length(:name, …)` is classified as
`validate_length(cs, :name, …)`, with the changeset at position 0 (routed `:expression`, so
the upstream pipeline stays mutable).

# `route_arguments`

```elixir
@spec route_arguments(Mutare.CallRouting.Call.t()) ::
  Mutare.CallRouting.ArgumentRoutes.t()
```

`c:Mutare.CallRouting.route_arguments/1` for a `:routing`-registered `Ecto.Changeset` stage:
the per-argument `treatments/2` classification, wrapped as `ArgumentRoutes`.

# `treatments`

```elixir
@spec treatments(atom(), [Macro.t()]) :: [Mutare.CallRouting.treatment()]
```

Per-argument treatment for a changeset stage `name` with arguments `args` (the piped
changeset included, at position 0): every position `:expression`, except a written field atom
in the field position (`:raw`), `validate_number`'s trailing keyword list (`{:keyword, …}`,
every key raw), and `validate_length`'s when its `count:` value is a written atom
(`[:expression, count: :raw]`) — the moduledoc.

---

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