# `Snodo.Resource.Template`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.4.1/lib/snodo/resource/template.ex#L1)

An exact matcher for the RFC 6570 URI template shapes that can be matched
without ambiguity.

`Snodo.Resource` compiles a template at build time and generates
`matches?/1` from the result. A template outside the supported shapes is a
compile error naming the shape, unless the module implements `matches?/1`
itself. This module matches URIs against templates; it does not expand them.

## Supported shapes

  * The scheme is a literal, matched case-insensitively.
  * The authority is one literal, one `{var}`, or empty when a literal `/`
    follows it, as in `file:///{+path}`. An empty authority matches only an
    empty host.
  * Each path segment is one literal or one `{var}`. A `{var}` binds exactly
    one whole, non-empty segment.
  * At most one variable-length path expression, anywhere in the path:
    * `/{+var}` (reserved expansion) binds one or more whole segments,
      joined with `/`;
    * `{/var}` (path segment expansion) binds zero or one segment;
    * `{/var*}` (exploded path segments) binds zero or more segments,
      joined with `/`.

    The literal and `{var}` segments before and after it are matched from
    each end, so there is never more than one way to split a URI.
  * Query expansion at the end of the template: one `{?a,b}` followed by any
    number of `{&c,d}`. Each named parameter may appear at most once, in any
    order, and may be absent. A parameter that the template does not name,
    a repeated parameter, a pair without `=`, or an empty query does not
    match. `q=` binds `""`.

A variable that is not bound, an absent `{/var}`, `{/var*}`, or query
parameter, is left out of the returned map, as RFC 6570 leaves undefined
variables out of an expansion.

## Rejected shapes

`compile/1` returns `{:error, reason}`, and the reason names the shape, for:
fragment expansion `{#var}`, label expansion `{.var}`, path-style parameters
`{;var}`, the reserved operators `= , ! @ |`, prefix modifiers `{var:n}`,
explode on simple, reserved, or query variables, more than one variable in a
simple, reserved, or path expression (`{a,b}`, `{/a,b}`: a missing value
cannot be assigned to one variable), more than one variable-length path
expression, an expression that shares a path segment with literal text or
another expression (`v{version}`, `{name}.json`), any expression in the
scheme, a reserved expression in the authority, an empty authority not
followed by `/`, a `{/var}` straight after a literal `/`, a query expression
that is not at the end, a literal query, fragment, port, or userinfo, empty
segments,
variables named `uri` or `_meta` (the request owns those keys), and
malformed literals.

## Values

Matched values are percent-decoded once and must be valid UTF-8. Malformed
percent escapes and empty path segments do not match. `+` is not decoded as
a space. Repeated variables must bind the same decoded value. Literal
authority and path segments match exactly, without percent-decoding or
slash normalization, and dot segments (`.` and `..`) are not removed.

A decoded value can therefore contain `/` (from `%2F`, or from the segments
that `{+var}` and `{/var*}` join), can be `..` or contain `../`, and can
contain a NUL byte (from `%00`). An application that maps a value to a file
must treat it as untrusted: reject values containing NUL, and resolve the
rest with `Path.safe_relative/2` against the directory it serves (or reject
values containing `/`, `\`, or `..` segments) before touching the file
system.

## Limits

A template is at most 1,024 bytes with at most 32 variables, checked
when it compiles. Matching is linear in the length of the URI: the URI is
parsed once, split once on `/`, and each fixed segment and query pair is
examined once, with no backtracking. The span a variable-length expression
binds is checked and decoded as one binary, not segment by segment. A query with more pairs than the template names
is refused before the rest is read. The length of the URI is bounded by the
transport's message size limit.

# `expansion`

```elixir
@type expansion() ::
  {:reserved, String.t()} | {:optional, String.t()} | {:explode, String.t()}
```

# `part`

```elixir
@type part() :: {:literal, String.t()} | {:variable, String.t()}
```

# `t`

```elixir
@type t() :: %Snodo.Resource.Template{
  authority: part(),
  expansion: expansion() | nil,
  query: [String.t()],
  scheme: String.t(),
  segments: [part()],
  suffix: [part()]
}
```

# `variables`

```elixir
@type variables() :: %{optional(String.t()) =&gt; String.t()}
```

# `compile`

```elixir
@spec compile(String.t()) :: {:ok, t()} | {:error, String.t()}
```

Compiles a URI template, or reports why it is outside the supported shapes.

`URI.new/1` rejects the braces, so the template is parsed textually. Its
literals are checked as a concrete URI with safe placeholders for variables,
so an invalid literal cannot produce an unreachable generated matcher.

# `match`

```elixir
@spec match(t(), String.t()) :: {:ok, variables()} | :error
```

Matches a concrete URI, returning the bound variables.

# `variables`

```elixir
@spec variables(t()) :: [String.t()]
```

Returns the variable names a compiled template binds, in template order.

---

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