# Custom Claim Parsers

Ltix parses standard OIDC and LTI claims from the launch JWT
automatically. Unusable optional metadata becomes `nil`, while valid
fields remain available. Identity and required launch fields remain
validated. Any unrecognized claims are put in the `extensions` map as raw
values.

```elixir
context.claims.extensions
#=> %{"https://myplatform.example.com/claim/analytics" => %{"session_id" => "abc123"}}
```

Register custom claim parsers to transform these raw values into
structured data.

## Writing a claim parser

A claim parser is a module implementing the `Ltix.LaunchClaims.Parser` behavior. Its `parse/1` callback takes the raw claim value and returns `{:ok, parsed}` or `{:error, reason}`.

```elixir
defmodule MyApp.Lti.AnalyticsClaim do
  use Ltix.LaunchClaims.Parser

  defstruct [:session_id, :tracking_enabled]

  @impl Ltix.LaunchClaims.Parser
  def parse(%{"session_id" => session_id} = raw) do
    {:ok,
     %__MODULE__{
       session_id: session_id,
       tracking_enabled: Map.get(raw, "tracking_enabled", false)
     }}
  end

  def parse(_), do: {:error, "missing session_id in analytics claim"}
end
```

A 1-arity function with the same contract works too, such as `fn raw -> {:ok, raw} end`.

## Registering parsers

Claim parsers are registered as a map of claim key to parser module or
function. The key is the full claim name as it appears in the JWT
(usually a namespaced URI for extension claims).

**Via application config** (recommended for parsers that apply globally)

```elixir
# config/config.exs
config :ltix, Ltix.LaunchClaims,
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim
  }
```

**Via `handle_callback/3`** (for per-call control)

```elixir
Ltix.handle_callback(params, state,
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim
  }
)
```

**Via `LaunchClaims.from_json/2`** (for direct parsing)

```elixir
Ltix.LaunchClaims.from_json(claims,
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim
  }
)
```

Per-call parsers merge with application config, with per-call taking
priority for overlapping keys.

To replace a standard parser, register its claim key. The parsed value
appears in the corresponding `LaunchClaims` field.

## Accessing parsed claims

Parsed extension claims remain in the `extensions` map, and their
values are your parsed values.

```elixir
{:ok, context} = Ltix.handle_callback(params, state)

context.claims.extensions["https://myplatform.example.com/claim/analytics"]
#=> %MyApp.Lti.AnalyticsClaim{session_id: "abc123", tracking_enabled: true}
```

## Missing claims

A parser runs only for claims present in the token. An absent extension claim is left out of `extensions`, and an absent standard claim leaves its `LaunchClaims` field `nil`. A claim sent as JSON `null` is passed to the parser as `nil`.

## Error handling

When a parser returns `{:error, reason}`, the launch fails with `Ltix.Errors.Invalid.InvalidClaim`, whose `message` is the reason (a string as given, an exception's message, or any other term as `inspect/1` shows it). A reason that is already an Ltix error is returned unchanged.

```elixir
{:error, %Ltix.Errors.Invalid.InvalidClaim{} = error} = Ltix.handle_callback(params, state)

error.claim
#=> "https://myplatform.example.com/claim/analytics"

error.message
#=> "missing session_id in analytics claim"
```

## Registering multiple parsers

You can register claim parsers and role parsers together in application
config.

```elixir
config :ltix, Ltix.LaunchClaims,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => MyApp.Lti.PlatformRoleParser
  },
  claim_parsers: %{
    "https://myplatform.example.com/claim/analytics" => MyApp.Lti.AnalyticsClaim,
    "https://myplatform.example.com/claim/proctoring" => MyApp.Lti.ProctoringClaim
  }
```

## Next steps

- [Custom Role Parsers](custom-role-parsers.md) for handling
  proprietary role vocabularies
- `Ltix.LaunchClaims` for the full claims struct and `from_json/2` options
- [Error Handling](error-handling.md) for how parser errors are returned
