# Custom Role Parsers

Ltix parses standard LIS role URIs by default, but some platforms
send proprietary role vocabularies with their own URI namespaces. Without
a custom parser, those URIs are left as raw strings in `unrecognized_roles`.

```elixir
context.claims.unrecognized_roles
#=> ["https://myplatform.example.com/roles/CourseAdmin"]
```

Register a custom role parser to parse them.

## Implementing the behaviour

A role parser is a module that implements `Ltix.LaunchClaims.Role.Parser`.
The only required callback is `parse/1`, which receives a role URI string
and returns `{:ok, %Role{}}` or `:error`.

```elixir
defmodule MyApp.Lti.PlatformRoleParser do
  use Ltix.LaunchClaims.Role.Parser

  alias Ltix.LaunchClaims.Role

  @base "https://myplatform.example.com/roles/"

  @roles %{
    "CourseAdmin" => :course_admin,
    "Facilitator" => :facilitator,
    "Grader" => :grader
  }

  @impl true
  def parse(uri) do
    suffix = String.replace_leading(uri, @base, "")

    case Map.fetch(@roles, suffix) do
      {:ok, name} ->
        {:ok, %Role{type: :context, name: name, sub_role: nil, uri: uri}}

      :error ->
        :error
    end
  end
end
```

The parser only receives URIs that match its registered prefix (see
[Registration](#registering-parsers) below), so you don't need to check
the prefix yourself.

## Optional: `to_uri/1`

If you need to convert `%Role{}` structs back to URI strings (e.g. for
building NRPS responses or test fixtures), implement the optional
`to_uri/1` callback. For a role without a URI, `Role.to_uri/2`
and `Role.to_uri!/2` try the same parsers `Role.parse/2` would, in the
same order, and take the same `:role_parsers` option.

```elixir
@roles_inverse Map.new(@roles, fn {k, v} -> {v, k} end)

@impl true
def to_uri(%Role{type: :context, name: name, sub_role: nil}) do
  case Map.fetch(@roles_inverse, name) do
    {:ok, suffix} -> {:ok, @base <> suffix}
    :error -> :error
  end
end

def to_uri(_), do: :error
```

## Registering parsers

Role parsers are registered as a map of URI prefix to parser module. The
prefix determines which URIs are passed to your parser. Ltix tries each
registered parser whose prefix matches the URI, longest prefix first,
then the built-in LIS parser.

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

```elixir
# config/config.exs
config :ltix, Ltix.LaunchClaims,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => MyApp.Lti.PlatformRoleParser
  }
```

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

```elixir
Ltix.handle_callback(params, state,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => MyApp.Lti.PlatformRoleParser
  }
)
```

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

```elixir
Ltix.LaunchClaims.from_json(claims,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => MyApp.Lti.PlatformRoleParser
  }
)
```

**Via `Role.parse/2`** (for parsing individual URIs)

```elixir
Role.parse(uri,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => MyApp.Lti.PlatformRoleParser
  }
)
```

Per-call parsers merge with application config, with per-call taking
priority for overlapping prefixes. Registered parsers, including ones
under an LIS prefix, take precedence over the built-in LIS parser.

## Using function parsers

For a one-off parser in a single call, you can pass an anonymous
function or captured function as the parser.

```elixir
Role.parse(uri,
  role_parsers: %{
    "https://myplatform.example.com/roles/" => fn uri ->
      case uri do
        "https://myplatform.example.com/roles/CourseAdmin" ->
          {:ok, %Role{type: :context, name: :course_admin, sub_role: nil, uri: uri}}

        _ ->
          :error
      end
    end
  }
)
```

Register parsers in application configuration as modules, because a release can't store an anonymous function in its configuration.

> #### Function parsers can't convert back to URIs {: .warning}
>
> For a role without a URI, `Role.to_uri/2` and `Role.to_uri!/2`
> only use module-based parsers that implement the `to_uri/1` callback.
> Function parsers are skipped during URI conversion.

## Working with custom roles

Once registered, custom roles are parsed into `%Role{}` structs just
like standard LIS roles. They appear in `context.claims.roles` and work
with all the filtering and checking functions.

```elixir
Role.context_roles(context.claims.roles)
Role.has_role?(context.claims.roles, :context, :course_admin)
```

The built-in predicates (`instructor?/1`, `learner?/1`, etc.) won't
match custom role names. Use `has_role?/4` for custom roles.

## Next steps

- [Working with Roles](working-with-roles.md) for standard role predicates
  and platform differences
- [Custom Claim Parsers](custom-claim-parsers.md) for parsing
  vendor-specific extension claims
- `Ltix.LaunchClaims.Role.Parser` for the full behaviour reference
- `Ltix.LaunchClaims.Role.LIS` as a reference implementation
