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

Pure validators for Layer A input shapes.

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. 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 not accepted.

## Field-error vocabulary — `invalid_part_type` extension

The field-error tuple `{:content, :invalid_part_type}` retains its
atom-only second element so existing pattern-matching callers continue to
match. Phase 21.1 carries the structured detail on the surrounding
`%ValidationError{}`'s `:metadata` map:

    %ALLM.Error.ValidationError{
      errors: [{:content, :invalid_part_type}],
      metadata: %{
        invalid_part_type: %{
          expected: [ALLM.TextPart, ALLM.ImagePart],
          got: <module>
        }
      },
      message: "validation failed: content element is not %ALLM.TextPart{} or %ALLM.ImagePart{} (got: <module>)"
    }

`<module>` is the offending element's struct module, or `Map` when the
element is a plain map. The structured `:metadata` makes the failure
machine-readable; `Exception.message/1` makes it human-readable.

Validators are opt-in: constructors like `ALLM.Request.new/2` do not call
these functions. Users invoke `request/1`, `message/1`, `tool/1`,
`thread/1`, `session/1`, `image_request/1`, or `embedding_request/1`
explicitly when they need a check before dispatch.

# `embedding_request`

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

Validate an `%ALLM.EmbeddingRequest{}`.

Returns `:ok` when every rule passes, or
`{:error, %ALLM.Error.ValidationError{reason: :invalid_embedding_request, errors: [...]}}`.

All rules accumulate into one error list except `{:input, :invalid_shape}`,
which hard-rejects: every `[:input, idx]` rule presupposes a list, so
evaluating them against a non-list would be meaningless.

Field rules: `:input` a list of non-empty binaries (an empty list is
rejected — a provider would 400 on it); `:dimensions` `nil` or a positive
integer; `:task_type` `nil` or a member of `t:ALLM.EmbeddingRequest.task_type/0`;
`:truncate` a boolean; `:model` `nil` or a binary.

Per-model dimension caps are deliberately NOT checked here — that is
`ALLM.Capability.preflight_embedding/2`'s job, which has the model
metadata this validator lacks.

## Examples

    iex> ALLM.Validate.embedding_request(ALLM.EmbeddingRequest.new(input: ["a chunk"]))
    :ok

    iex> req = ALLM.EmbeddingRequest.new(input: [])
    iex> {:error, err} = ALLM.Validate.embedding_request(req)
    iex> err.reason
    :invalid_embedding_request
    iex> {:input, :empty} in err.errors
    true

# `image_request`

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

Validate an `%ALLM.ImageRequest{}`.

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}` — raw maps are not
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 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*
