# Validation Patterns

Advanced validation patterns for dynamic and context-aware schemas.

## Validation Modes

Peri supports two validation modes to control how extra fields are handled:

### Strict Mode (Default)

By default, Peri operates in strict mode, which filters out any fields not defined in the schema:

```elixir
schema = %{
  name: :string,
  age: :integer
}

data = %{name: "John", age: 30, extra: "field"}

{:ok, result} = Peri.validate(schema, data)
# result => %{name: "John", age: 30}
```

### Permissive Mode

Permissive mode preserves all fields from the input data, even those not defined in the schema:

```elixir
{:ok, result} = Peri.validate(schema, data, mode: :permissive)
# result => %{name: "John", age: 30, extra: "field"}
```

### Using Permissive Mode with defschema

You can define schemas that always use permissive mode:

```elixir
defmodule MySchemas do
  import Peri

  # Strict mode (default)
  defschema :user_strict, %{
    name: :string,
    email: {:required, :string}
  }

  # Permissive mode
  defschema :user_permissive, %{
    name: :string,
    email: {:required, :string}
  }, mode: :permissive
end

data = %{name: "John", email: "john@example.com", role: "admin"}

# Strict mode filters out 'role'
{:ok, strict} = MySchemas.user_strict(data)
# strict => %{name: "John", email: "john@example.com"}

# Permissive mode keeps 'role'
{:ok, permissive} = MySchemas.user_permissive(data)
# permissive => %{name: "John", email: "john@example.com", role: "admin"}
```

### Use Cases for Permissive Mode

Permissive mode is useful when:

- Building API gateways that need to forward extra fields
- Implementing progressive validation in layers
- Working with evolving data structures where new fields may be added
- Creating middleware that validates known fields but passes through metadata

**Note**: Fields not defined in the schema are not validated, they are simply passed through unchanged.

## Conditional Validation

Use `:cond` to validate fields based on runtime conditions.

```elixir
defmodule UserSchema do
  import Peri

  defschema :registration, %{
    name: {:required, :string},
    is_premium: {:required, :boolean},
    # Only require payment info if premium
    payment_info: {:cond, & &1.is_premium, {:required, :string}, nil}
  }
end
```

## Dependent Validation

Two forms, with different contracts:

- `{:dependent, field, condition, type}` - `condition` is called with
  `(value, dependent_value)` and must return `:ok` or
  `{:error, message, info}`. On `:ok`, the value is validated against `type`.
- `{:dependent, callback}` - `callback` (fun/1, fun/2, or MFA) must return
  `{:ok, type}` or `{:error, message, info}`. On `{:ok, type}` the field is
  validated against `type`; `{:ok, nil}` means the field must be absent. On
  `{:error, message, info}` the field fails with that error directly.

Returning anything else (e.g. the bare type without the `:ok` wrapper) is a
contract breach and fails validation with an error naming the field.

### Single Field Dependency

Validate a field based on another field's value:

```elixir
defmodule AuthSchema do
  import Peri

  defschema :user, %{
    password: {:required, :string},
    password_confirmation: {:dependent, :password, &match_passwords/2, :string}
  }

  defp match_passwords(password, password), do: :ok
  defp match_passwords(_, _), do: {:error, "passwords must match", []}
end
```

### Dynamic Type via Callback

Compute the field's type from the rest of the data. The callback receives the
root data (fun/1), or the current element and the root data (fun/2), and
returns the type to validate against:

```elixir
defmodule ProfileSchema do
  import Peri

  defschema :user_profile, %{
    contact_type: {:required, {:enum, [:email, :phone, :both]}},
    email: {:string, {:regex, ~r/@/}},
    phone: :string,
    contact_info: {:dependent, &validate_contact/1}
  }

  defp validate_contact(%{contact_type: :email}) do
    {:ok, {:required, %{email: {:required, :string}}}}
  end

  defp validate_contact(%{contact_type: :phone}) do
    {:ok, {:required, %{phone: {:required, :string}}}}
  end

  defp validate_contact(%{contact_type: :both}) do
    {:ok, {:required, %{
      email: {:required, :string},
      phone: {:required, :string}
    }}}
  end
end
```

MFA callbacks work the same way - `{:dependent, {Mod, fun}}` calls
`Mod.fun(root)` (or `Mod.fun(current, root)` if arity 2 is exported), and
`{:dependent, {Mod, fun, args}}` calls `Mod.fun(root, args...)`:

```elixir
defschema :attachment, %{
  kind: {:required, {:enum, [:avatar, :document]}},
  payload: {:dependent, {__MODULE__, :payload_type}}
}

def payload_type(%{kind: :avatar}), do: {:ok, {:required, :string}}
def payload_type(_), do: {:ok, nil}
```

## Custom Validation

### Simple Custom Validator

```elixir
defmodule ProductSchema do
  import Peri

  defschema :product, %{
    price: {:custom, &validate_price/1}
  }

  defp validate_price(price) when price > 0, do: :ok
  defp validate_price(price), do: {:error, "price must be positive, got %{price}", [price: price]}
end
```

### MFA Custom Validators

```elixir
defmodule OrderSchema do
  import Peri

  defschema :order, %{
    items: {:list, {:custom, {__MODULE__, :validate_item, [:in_stock]}}},
    total: {:custom, {Calculator, :validate_total}}
  }

  def validate_item(item, :in_stock) do
    if item.stock > 0 do
      :ok
    else
      {:error, "item %{name} is out of stock", [name: item.name]}
    end
  end
end
```

## Callback Arities

### 1-Arity Callbacks (Root Data)

Receives the entire root data structure:

```elixir
%{
  user_type: :premium,
  features: {:cond, fn data -> data.user_type == :premium end,
             {:list, :string},
             {:literal, []}}
}
```

### 2-Arity Callbacks (Current + Root)

Receives current context and root data - useful for lists:

```elixir
defmodule ItemSchema do
  import Peri

  defschema :item, %{
    type: {:required, :string},
    # Validates based on current item's type, not parent data
    value: {:dependent, fn current, _root ->
      case current.type do
        "number" -> {:ok, :integer}
        "text" -> {:ok, :string}
        _ -> {:ok, :any}
      end
    end}
  }

  defschema :collection, %{
    items: {:list, get_schema(:item)}
  }
end

# Each item validates independently
data = %{
  items: [
    %{type: "number", value: 42},
    %{type: "text", value: "hello"}
  ]
}
```

## Schema Composition

### Reusable Schemas

```elixir
defmodule BaseSchemas do
  import Peri

  defschema :address, %{
    street: {:required, :string},
    city: {:required, :string},
    zip: {:string, {:regex, ~r/^\d{5}$/}}
  }

  defschema :person, %{
    name: {:required, :string},
    address: get_schema(:address)
  }

  defschema :company, %{
    name: {:required, :string},
    headquarters: get_schema(:address),
    employees: {:list, get_schema(:person)}
  }
end
```

### Schema Merging

```elixir
defmodule UserSchemas do
  import Peri

  defschema :base_user, %{
    name: {:required, :string},
    email: {:required, :string}
  }

  defschema :admin_user, Map.merge(get_schema(:base_user), %{
    permissions: {:required, {:list, :string}},
    last_login: :datetime
  })
end
```

## Error Context

Custom validators can provide detailed error context:

```elixir
defp validate_complex_rule(value) do
  case expensive_validation(value) do
    :ok -> :ok
    {:error, reason} ->
      {:error, "validation failed: %{reason} for value %{value}",
       [reason: reason, value: inspect(value)]}
  end
end
```

## Decode and Encode

Coercion is a schema directive, so decoding wire data is just validation:
`Peri.decode/3` is an alias of `Peri.validate/3` that reads naturally at the
boundary. `Peri.encode/3` runs the same schema in the opposite direction — it
validates the data against the target types, then produces the wire
representation:

```elixir
schema = %{
  "page" => {:coerce, :string, :integer},
  "since" => {:coerce, :string, :date}
}

{:ok, decoded} = Peri.decode(schema, %{"page" => "2", "since" => "2024-03-15"})
# => {:ok, %{"page" => 2, "since" => ~D[2024-03-15]}}

{:ok, wire} = Peri.encode(schema, decoded)
# => {:ok, %{"page" => "2", "since" => "2024-03-15"}}
```

On encode, built-in `:string` sources render with `to_string/1`
(`to_iso8601/1` for date/time structs). A coerce directive may carry an
`encode:` opt to customize the reverse, and custom function/MFA sources
without an `encode:` opt pass the validated value through unchanged.

Two related directives round out the codec story:

- `{type, {:encode, fun}}` applies `fun` (or an MFA) to the value during
  `Peri.encode/3` only; it is inert under `validate/3` and `decode/3`.
- `{type, {:transform, fun}}` is skipped during `Peri.encode/3` — transforms
  are decode-only.

Note that `Peri.encode/3` still validates: encoding data that does not match
the target types returns the usual `{:error, errors}` tuple.

## Rendering Errors

`Peri.Error.humanize/1` converts an error (or list of errors) into a
nested, data-shaped map of messages, mirroring
`Ecto.Changeset.traverse_errors/2` output. Integer path segments become
integer keys (list indices), and multiple errors at the same path append
to the message list:

```elixir
{:error, errors} = MySchemas.user(%{addresses: [%{street: "x"}]})

Peri.Error.humanize(errors)
# => %{addresses: %{0 => %{street: ["is too short"]}}}
```

This shape drops straight into API error payloads and form rendering.
Because `humanize/1` reads each leaf's final message verbatim, it composes
with `traverse_errors/2`: translate first, then humanize.

Errors without a path come from top-level scalar validations
(`Peri.validate(:string, 5)`); with no field to nest under, they humanize
to a bare list of messages.

## Missing Key Suggestions

When a required field is missing but the data contains a key that looks
like a typo of it (Jaro distance above 0.8, compared across atom and
string key forms), the error message gains a suggestion and the error
content carries it under `:did_you_mean`:

```elixir
{:error, [error]} = Peri.validate(%{email: {:required, :string}}, %{emial: "a@b.c"})

error.message
# => "is required, expected type of :string did you mean :emial?"
error.content[:did_you_mean]
# => :emial
```

This works in both strict and permissive modes, including inside nested
schemas and list elements. A static or MFA `error:` override still
replaces the message; the `:did_you_mean` content remains available.
