# `Formata.Schema.Native`
[🔗](https://github.com/gsmlg-dev/formata/blob/v0.1.0/lib/formata/schema/native.ex#L1)

Native schemas: `Formata.Backplane.Schema` definitions used through Formata.

## Creating schemas

A native schema is plain Elixir data in the `Formata.Backplane.Schema` vocabulary:

    definition = %{
      name: {:required, {:string, min: 1}},
      age: {:integer, gte: 0},
      role: {:enum, ["admin", "member"]},
      tags: {:list, :string},
      address: %{city: {:required, :string}}
    }

    {:ok, schema} = Formata.Schema.native(definition)

Atom schema keys match atom or string data keys, so the definition above
validates string-keyed documents decoded by Formata.

### Reusable definitions

Because definitions are data, they can be bound to variables and composed
(`merge/2`, `select/2`, `except/2` delegate to `Formata.Backplane.Schema`). Named,
reusable and recursive definitions use `Formata.Backplane.Schema.defschema/3` and
`{:ref, {Module, name}}`:

    defmodule MyApp.Schemas do
      import Formata.Backplane.Schema

      defschema :address, %{city: {:required, :string}}
      defschema :person, %{
        name: {:required, :string},
        address: {:ref, :address},
        friends: {:list, {:ref, :person}}
      }
    end

    {:ok, schema} = Formata.Schema.native({:ref, {MyApp.Schemas, :person}})

## Pure validation (`Formata.validate/3`)

`Formata.Backplane.Schema.validate/3` both validates and transforms (defaults,
coercion, transforms, and strict-mode stripping/renaming of keys). Formata's
pure validation therefore runs a preflight over the definition, including
every reachable `{:ref, ...}` target, and supports this non-transforming
subset:

  * primitive types, `{:literal, v}`, `{:enum, ...}`, `{:required, ...}`,
    `{:meta, ...}`
  * string/integer/float constraints (`min`, `max`, `regex`, `eq`, `neq`,
    `gt`, `gte`, `lt`, `lte`, `range`, `multiple_of`)
  * nested maps, keyword schemas, `{:list, ...}`, `{:map, ...}`,
    `{:tuple, ...}`, `{:schema, ...}` (including `:additional_keys`)
  * `{:either, ...}`, `{:oneof, ...}` (first matching branch, native
    semantics), `{:multi, ...}`, `{:cond, ...}`, `{:dependent, field, check, type}`
  * `{:custom, ...}` validators that return `:ok` or `{:error, ...}`
  * `{:ref, {Module, name}}`, including recursive references

These constructs are rejected before anything runs, with an `:unsupported` /
`:pure_validation_unsupported` error naming the construct and its schema
path: `{type, {:default, _}}`, `{type, {:transform, _}}`,
`{type, {:encode, _}}`, every `{:coerce, ...}` form, and the dynamic
`{:dependent, callback}` forms (their schemas are only known at run time).
Use `Formata.cast/3` for those.

Custom validators run inside the native engine. If one returns anything
other than `:ok` or an error during pure validation (for example `{:ok, new_value}`),
validation fails with `:unsupported` / `:non_pure_custom` instead of
accepting a modified value.

The input is never modified, and `validate` returns only `:ok` or errors.

### Absent versus `nil`

The native engine treats an explicit `nil` like an absent field: an
optional field accepts both, and `{:required, type}` rejects both.

## Unknown fields

| Option             | `validate/3`        | `cast/3`                         |
| ------------------ | ------------------- | -------------------------------- |
| `unknown: :allow`  | default; ignored    | kept in the result               |
| `unknown: :reject` | `:unknown_field` error per field | `:unknown_field` error per field |
| `unknown: :strip`  | not accepted        | default; removed from the result |

Unknown-field rejection is enforced by the native engine (Formata adds an
`:additional_keys` rule to each map schema). It is not available for keyword
list schemas.

## Casting (`Formata.cast/3`)

`cast` runs the full native engine (`Formata.Backplane.Schema.validate/3`) and returns
the transformed data. Modifiers wrap types, and each field is processed from
the outermost modifier inwards:

  1. `{type, {:default, value}}` replaces an absent or `nil` value with
     `value`; the default is then processed by `type` like any input;
  2. `{:coerce, ...}` converts the source representation (for example
     `"42"` to `42`) and passes values that already have the target type;
  3. the type, its constraints, `{:custom, ...}` validators and nested
     schemas are checked on the (possibly defaulted/coerced) value;
  4. `{type, {:transform, fun}}` maps the validated value. Transforms only
     run for present, non-`nil` values, so write
     `{{type, {:transform, fun}}, {:default, value}}` to transform a default.

A default can never satisfy `{:required, ...}`: `Formata.Backplane.Schema` rejects
that combination when the schema is compiled.

Unknown fields are then handled by the `:unknown` option. With `:strip` (the
default) the result contains only declared fields, keyed by the schema's keys
(an atom-keyed schema returns atom keys). With `:allow`, the result keeps the
input keys: a declared field is written back under the key present in the
data (an atom schema key matching a string data key keeps the string key),
undeclared fields are kept unchanged, and a field added by a default uses the
schema's key.

Coercion to `:atom!` is rejected (`:unsafe_atom_coercion`) because it would
create atoms from input data.

# `compile`

```elixir
@spec compile(term(), keyword()) ::
  {:ok, Formata.Schema.t()} | {:error, [Formata.Error.t(), ...]}
```

Checks and compiles a native definition.

Invalid definitions return `:schema` / `:invalid_schema` errors. A definition
that is valid but outside the pure-validation subset still compiles; the
limitation is reported when `Formata.validate/3` is called.

# `except`

# `merge`

# `select`

---

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