# `ALLM.Validate`
[🔗](https://github.com/cykod/ALLM/blob/v0.3.0/lib/allm/validate.ex#L1)

Pure validators for Layer A input shapes. See spec §16 and Phase 1 design
sub-phase 1.4.

Every validator returns `:ok` or `{:error, %ALLM.Error.ValidationError{}}`
with a machine-readable `:errors` list of `{field, reason}` tuples. The
`field` is either a single atom (top-level) or a path of atoms/indices
(e.g. `[:messages, 2, :role]`) when the failure is nested.

Multimodal content parts use the `%ALLM.TextPart{}` and `%ALLM.ImagePart{}`
Layer A structs (spec §35.6, Phase 14.4). A content list whose elements are
not one of those two structs is rejected with
`{:content, :invalid_part_type}` — raw maps in content lists are no longer
accepted (Phase 14.4 Decision #11; previously the v0.2 validator accepted
any list of maps and short-circuited image-typed maps with the now-removed
`:vision_not_in_v0_2` reason).

Validators are opt-in: constructors like `ALLM.Request.new/2` do not call
these functions (see Phase 1 design non-obvious decision #7). Users invoke
`request/1`, `message/1`, `tool/1`, `thread/1`, `session/1`, or
`image_request/1` (Phase 13.3, spec §35.2.2) explicitly when they need a
check before dispatch.

# `image_request`

```elixir
@spec image_request(ALLM.ImageRequest.t()) ::
  :ok | {:error, ALLM.Error.ValidationError.t()}
```

Validate an `%ALLM.ImageRequest{}` (spec §35.2.2).

Returns `:ok` when every rule passes, or
`{:error, %ALLM.Error.ValidationError{reason: :invalid_image_request, errors: [...]}}`
accumulating ALL failed rules — no hard-reject (matches `request/1`'s
accumulator pattern).

Operation-arity rules:

  * `:generate` requires non-empty `:prompt` AND `:input_images == []`.
  * `:edit` requires non-empty `:prompt` AND `length(:input_images) in 1..2`.
  * `:variation` requires `:prompt in [nil, ""]` AND `length(:input_images) == 1`.

Field rules: `:n` integer ≥ 1; `:response_format in [:binary, :base64, :url]`;
`:size in {pos_integer, pos_integer} | String.t() | :auto | nil`;
`:input_images` a list of `%ALLM.Image{}`; `:mask` `%ALLM.Image{}` or `nil`.

## Examples

    iex> ALLM.Validate.image_request(ALLM.ImageRequest.new(prompt: "a kestrel"))
    :ok

    iex> req = ALLM.ImageRequest.new(prompt: nil, operation: :generate)
    iex> {:error, err} = ALLM.Validate.image_request(req)
    iex> err.reason
    :invalid_image_request
    iex> {:prompt, :required_for_operation} in err.errors
    true

# `message`

```elixir
@spec message(ALLM.Message.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
```

Validate an `%ALLM.Message{}`.

Returns `:ok` or `{:error, %ALLM.Error.ValidationError{}}`. A content list
whose elements are not `%ALLM.TextPart{}` or `%ALLM.ImagePart{}` is
rejected with `{:content, :invalid_part_type}` (Phase 14.4 Decision #11);
raw maps are no longer accepted in content lists.

## Examples

    iex> ALLM.Validate.message(%ALLM.Message{role: :user, content: "hi"})
    :ok

    iex> {:error, err} = ALLM.Validate.message(%ALLM.Message{role: :tool, content: "ok"})
    iex> err.reason
    :invalid_message
    iex> {:tool_call_id, :required} in err.errors
    true

# `request`

```elixir
@spec request(ALLM.Request.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
```

Validate an `%ALLM.Request{}`.

Returns `:ok` when every field is well-formed, or
`{:error, %ALLM.Error.ValidationError{reason: :invalid_request, errors: [...]}}`.

## Examples

    iex> req = ALLM.Request.new([%ALLM.Message{role: :user, content: "hi"}])
    iex> ALLM.Validate.request(req)
    :ok

    iex> req = ALLM.Request.new([])
    iex> {:error, err} = ALLM.Validate.request(req)
    iex> err.reason
    :invalid_request
    iex> {:messages, :empty} in err.errors
    true

# `session`

```elixir
@spec session(ALLM.Session.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
```

Validate an `%ALLM.Session{}`.

Enforces status/`pending_*` invariants (spec §5.7) and recursively validates
the embedded thread. Thread errors carry a `[:thread, :messages, idx, :field]`
path prefix.

## Examples

    iex> ALLM.Validate.session(%ALLM.Session{})
    :ok

    iex> {:error, err} = ALLM.Validate.session(%ALLM.Session{status: :awaiting_user, pending_question: nil})
    iex> err.reason
    :invalid_session
    iex> {:pending_question, :required_for_status} in err.errors
    true

# `thread`

```elixir
@spec thread(ALLM.Thread.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
```

Validate an `%ALLM.Thread{}`.

Every message must pass `message/1`. Errors from nested messages carry a
`[:messages, idx, :field]` path prefix so callers can locate the offender.

## Examples

    iex> t = ALLM.Thread.from_messages([%ALLM.Message{role: :user, content: "hi"}])
    iex> ALLM.Validate.thread(t)
    :ok

    iex> t = ALLM.Thread.from_messages([%ALLM.Message{role: :bogus, content: "x"}])
    iex> {:error, err} = ALLM.Validate.thread(t)
    iex> err.reason
    :invalid_thread
    iex> {[:messages, 0, :role], :unknown} in err.errors
    true

# `tool`

```elixir
@spec tool(ALLM.Tool.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
```

Validate an `%ALLM.Tool{}`.

Returns `:ok` or `{:error, %ALLM.Error.ValidationError{}}`. The top-level
shape of `:schema` is intentionally not checked — providers differ on
whether `"type" => "object"` is required — but non-map schemas are rejected.

## Examples

    iex> tool = %ALLM.Tool{name: "weather", description: "d", schema: %{}}
    iex> ALLM.Validate.tool(tool)
    :ok

    iex> {:error, err} = ALLM.Validate.tool(%ALLM.Tool{name: "", description: "d", schema: %{}})
    iex> err.reason
    :invalid_tool
    iex> {:name, :empty} in err.errors
    true

---

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