# `Texture.UriTemplate`
[🔗](https://github.com/lud/texture/blob/main/lib/texture/uri_template.ex#L1)

URI Template parser implementation following RFC 6570 (levels 1–4).

Parsing returns a `%Texture.UriTemplate{}` struct. Use `render/2` to expand it
with variable values provided either as atom or binary keys.

## Parsing

    {:ok, template} = Texture.UriTemplate.parse("/users/{id}")
    %Texture.UriTemplate{}

An invalid template returns an error tuple:

    iex> Texture.UriTemplate.parse("/x/{not_closed")
    {:error, {:invalid_value, "{not_closed"}}

## Rendering

Provide a map whose keys are either atoms or binaries. Values are coerced
to strings; lists and exploded maps are supported per RFC 6570.

    iex> {:ok, t} = Texture.UriTemplate.parse("https://ex.com{/ver}{/res*}{?q,lang}{&page}")
    iex> Texture.UriTemplate.render(t, %{ver: "v1", res: ["users", 42], q: "café", lang: :fr, page: 2})
    "https://ex.com/v1/users/42?q=caf%C3%A9&lang=fr&page=2"

Reserved expansion keeps reserved characters (e.g. '+'):

    iex> {:ok, t} = Texture.UriTemplate.parse("/files{+path}")
    iex> Texture.UriTemplate.render(t, %{path: "/a/b c"})
    "/files/a/b%20c"

Simple expansion percent-encodes reserved characters:

    iex> {:ok, t} = Texture.UriTemplate.parse("/files/{path}")
    iex> Texture.UriTemplate.render(t, %{path: "/a/b c"})
    "/files/%2Fa%2Fb%20c"

Exploded list path segments:

    iex> {:ok, t} = Texture.UriTemplate.parse("/api{/segments*}")
    iex> Texture.UriTemplate.render(t, %{segments: ["v1", "users", 42]})
    "/api/v1/users/42"

Query continuation & omission of undefined variables:

    iex> {:ok, t} = Texture.UriTemplate.parse("?fixed=1{&x,y}")
    iex> Texture.UriTemplate.render(t, %{x: 2})
    "?fixed=1&x=2"

Fragment expansion with unicode & prefix modifier:

    iex> {:ok, t} = Texture.UriTemplate.parse("{#frag:6}")
    iex> Texture.UriTemplate.render(t, %{frag: "café-bar"})
    "#caf%C3%A9-b"

Empty list omits expression:

    iex> {:ok, t} = Texture.UriTemplate.parse("/s{?list}")
    iex> Texture.UriTemplate.render(t, %{list: []})
    "/s"

## Notes

* Undefined variables are silently omitted.
* Empty string values may contribute a key without '=' (for certain operators like ';').
* Order of exploded map query parameters is not guaranteed (maps are unordered).

# `t`

```elixir
@type t() :: %Texture.UriTemplate{parts: term(), raw: binary()}
```

# `match`

```elixir
@spec match(t(), binary()) ::
  {:ok, %{required(binary()) =&gt; term()}} | {:error, term()}
```

Extracts variables from a URL based on a parsed URI template.

Returns `{:ok, map}` on success or `{:error, exception}` on failure.

See `match!/2` for examples and detailed documentation.

# `match!`

```elixir
@spec match!(t(), binary()) :: %{required(binary()) =&gt; term()}
```

Same as `match/2` but raises `Texture.UriTemplate.TemplateMatchError` on failure.

This implementation has **limited support** and is designed for
straightforward, simple templates. Use it for basic path and query parameter
extraction. Rendering is a lossy operation, so the reverse operation cannot
always regenerate original values.

## Supported Operators

Only three operators are supported:

* **Default** (no operator): `{foo}`
* **Path segment** (`/`): `{/foo}`
* **Query** (`?`): `{?foo}`

Other operators like `+`, `#`, `.`, `;`, `&` are **not supported** for
matching.

## Unsupported Features

* **Prefix modifier** (`:n`): Templates with prefix modifiers like `{var:3}`
  are not supported for matching and will raise an error

## Basic Examples

    iex> t = Texture.UriTemplate.parse!("http://example.com/{foo}")
    iex> Texture.UriTemplate.match!(t, "http://example.com/hello")
    %{"foo" => "hello"}

    iex> t = Texture.UriTemplate.parse!("http://example.com/{foo}/{bar}")
    iex> Texture.UriTemplate.match!(t, "http://example.com/hello/world")
    %{"foo" => "hello", "bar" => "world"}

    iex> t = Texture.UriTemplate.parse!("http://example.com{/version,resource}")
    iex> Texture.UriTemplate.match!(t, "http://example.com/v1/users")
    %{"version" => "v1", "resource" => "users"}

    iex> t = Texture.UriTemplate.parse!("http://example.com/api{?foo,bar}")
    iex> Texture.UriTemplate.match!(t, "http://example.com/api?foo=1&bar=2")
    %{"foo" => "1", "bar" => "2"}

## More Complex Examples

    iex> t = Texture.UriTemplate.parse!("http://example.com/search{?foo*,bar}")
    iex> Texture.UriTemplate.match!(t, "http://example.com/search?foo=1&foo=2&foo=3&bar=hello")
    %{"foo" => ["1", "2", "3"], "bar" => "hello"}

    iex> t = Texture.UriTemplate.parse!("http://example.com/api{?map*,simple}")
    iex> Texture.UriTemplate.match!(t, "http://example.com/api?a=1&b=&simple=value")
    %{"map" => %{"a" => "1", "b" => ""}, "simple" => "value"}

## Behavior Details

### Empty Values

* Empty parameter values return `nil`
* Lists containing empty strings preserve them: `["", "b", ""]`
* Empty values in maps preserve empty keys or values

### Value Types

* All extracted values are strings (including numeric-like values)
* Unicode characters are properly decoded
* Percent-encoding is handled automatically

### List Matching

* Lists are comma-separated in default and query operators
* With other operators,lLists are comma-separated only when the parameter is
  not exploded .
* With multiple parameters, the last accumulates remaining values as a list
* Insufficient values assign `nil` to remaining parameters

Examples:

    # Lists with comma separator
    iex> t = Texture.UriTemplate.parse!("{foo}")
    iex> Texture.UriTemplate.match!(t, "1,2,3")
    %{"foo" => ["1", "2", "3"]}

    # Multiple params share list values
    iex> t = Texture.UriTemplate.parse!("{foo,bar}")
    iex> Texture.UriTemplate.match!(t, "1,2,3")
    %{"foo" => "1", "bar" => ["2", "3"]}

### Exploded Parameters (`*`)

* Exploded lists take all matching items into a list
* Exploded maps take all `key=value` pairs into a map
* Non-exploded maps are ambiguous and parsed as lists

Examples:

    # Path segments with exploded list
    iex> t = Texture.UriTemplate.parse!("{/foo*}")
    iex> Texture.UriTemplate.match!(t, "/a/b/c")
    %{"foo" => ["a", "b", "c"]}

### Query Parameters

* Parameters are matched by name, not position
* Order doesn't matter for query parameters
* Duplicate names in exploded lists accumulate into a list
* First occurrence wins for duplicate non-exploded parameters

Examples:

Query parameters (`{?foo,bar*,baz*}`) use a three-phase matching algorithm:

1. Each non-exploded parameter takes its matching `key=value` pair from the
   URL by name
2. Exploded parameters that have matching names in the URL collect all
   occurrences into a list (e.g., `foo=1&foo=2` → `["1", "2"]`)
3. The first exploded parameter that hasn't matched any names takes all
   remaining `key=value` pairs as a map

        iex> t = Texture.UriTemplate.parse!("{?none,simple,items*,rest*,none_expl*}")
        iex> Texture.UriTemplate.match!(t, "?extra=1&other=2&items=a&items=b&simple=value")
        %{
          "none" => nil,
          "simple" => "value",
          "items" => ["a", "b"],
          "rest" => %{"extra" => "1", "other" => "2"},
          "none_expl" => nil
        }

### Parameter Skipping

Extra query parameters that don't match any template variable are silently
ignored. This allows matching URLs with tracking parameters added by external
tools

### Value Encoding

* Percent-encoding is handled automatically
* Unicode characters are properly decoded

Examples:

    # Percent-encoded values
    iex> t = Texture.UriTemplate.parse!("{foo}")
    iex> Texture.UriTemplate.match!(t, "hello%20world")
    %{"foo" => "hello world"}

    # Query with empty parameter
    iex> t = Texture.UriTemplate.parse!("{?foo,bar}")
    iex> Texture.UriTemplate.match!(t, "?foo=&bar=value")
    %{"foo" => nil, "bar" => "value"}

### Duplicate Parameters

When the same parameter name appears multiple times in a template, the first
occurrence is preserved. This ensures path parameters are not overridden by
query parameters.

Example:

    # Duplicate parameter names (first wins)
    iex> t = Texture.UriTemplate.parse!("{foo}/{foo}")
    iex> Texture.UriTemplate.match!(t, "first/second")
    %{"foo" => "first"}

### Error Cases

Raises `Texture.UriTemplate.TemplateMatchError` when:

* Non-exploded parameter receives dict syntax unexpectedly
* Extra path segment values don't match template structure
* Invalid parameter syntax (e.g., `foo==bar`)
* Lists treated as keys in wrong context

# `parse`

```elixir
@spec parse(binary()) :: {:ok, t()} | {:error, term()}
```

Parses an URI template into an internal representation.

# `parse!`

```elixir
@spec parse!(binary()) :: t()
```

# `render`

```elixir
@spec render(t(), %{optional(atom()) =&gt; term(), optional(binary()) =&gt; term()}) ::
  binary()
```

Expands a URI template with provided variable values.

Returns a rendered URI string by expanding all template expressions with the
given parameters. Variables are looked up by name (as atoms or binaries) and
values are automatically coerced to strings. Follows RFC 6570 levels 1–4.

## Supported Operators

All RFC 6570 operators are supported:

* **Default** (no operator): `{var}` – Simple string expansion with
  percent-encoding
* **Reserved** (`+`): `{+var}` – Reserved expansion, keeps `/`, `?`, `&`, etc.
* **Fragment** (`#`): `{#var}` – Fragment identifier expansion
* **Label** (`.`): `{.var}` – Dot-prefixed label expansion
* **Path segment** (`/`): `{/var}` – Path segment expansion
* **Path-style parameter** (`;`): `{;var}` – Semicolon-prefixed parameters
* **Query** (`?`): `{?var}` – Form-style query parameters
* **Query continuation** (`&`): `{&var}` – Query continuation

## Basic Examples

    iex> t = Texture.UriTemplate.parse!("http://example.com/users/{id}")
    iex> Texture.UriTemplate.render(t, %{id: "42"})
    "http://example.com/users/42"

    iex> t = Texture.UriTemplate.parse!("http://example.com/users/{id}")
    iex> Texture.UriTemplate.render(t, %{"id" => "42"})
    "http://example.com/users/42"

    iex> t = Texture.UriTemplate.parse!("http://example.com{?q,lang}")
    iex> Texture.UriTemplate.render(t, %{q: "café", lang: "fr"})
    "http://example.com?q=caf%C3%A9&lang=fr"

    iex> t = Texture.UriTemplate.parse!("http://example.com/api{/version,resource}")
    iex> Texture.UriTemplate.render(t, %{version: "v1", resource: "users"})
    "http://example.com/api/v1/users"

## Reserved vs Simple Expansion

Simple expansion percent-encodes reserved characters:

    iex> t = Texture.UriTemplate.parse!("http://example.com/files/{path}")
    iex> Texture.UriTemplate.render(t, %{path: "/a/b c"})
    "http://example.com/files/%2Fa%2Fb%20c"

Reserved expansion (`+`) keeps reserved characters like `/`:

    iex> t = Texture.UriTemplate.parse!("http://example.com/files{+path}")
    iex> Texture.UriTemplate.render(t, %{path: "/a/b c"})
    "http://example.com/files/a/b%20c"

## List Expansion

Lists are expanded differently based on the operator and explode modifier:

    # Simple expansion (comma-separated)
    iex> t = Texture.UriTemplate.parse!("http://example.com/{segments}")
    iex> Texture.UriTemplate.render(t, %{segments: ["v1", "users", "42"]})
    "http://example.com/v1,users,42"

    # Path segment expansion with explode
    iex> t = Texture.UriTemplate.parse!("http://example.com/api{/segments*}")
    iex> Texture.UriTemplate.render(t, %{segments: ["v1", "users", "42"]})
    "http://example.com/api/v1/users/42"

    # Query parameters with explode
    iex> t = Texture.UriTemplate.parse!("http://example.com{?list*}")
    iex> Texture.UriTemplate.render(t, %{list: ["red", "green"]})
    "http://example.com?list=red&list=green"

    # Query parameters without explode (comma-separated)
    iex> t = Texture.UriTemplate.parse!("http://example.com{?list}")
    iex> Texture.UriTemplate.render(t, %{list: ["red", "green"]})
    "http://example.com?list=red,green"

## Map Expansion

Maps and keyword lists can be expanded with the explode modifier:

    # Query with exploded map
    iex> t = Texture.UriTemplate.parse!("http://example.com{?map*}")
    iex> Texture.UriTemplate.render(t, %{map: %{a: "1", b: "2"}})
    "http://example.com?a=1&b=2"

    # Semicolon parameters with exploded keyword list
    iex> t = Texture.UriTemplate.parse!("http://example.com{;params*}")
    iex> Texture.UriTemplate.render(t, %{params: [x: "1", y: "2"]})
    "http://example.com;x=1;y=2"

    # Non-exploded map (comma-separated key,value pairs)
    iex> t = Texture.UriTemplate.parse!("http://example.com/{map}")
    iex> Texture.UriTemplate.render(t, %{map: %{a: "1", b: "2"}})
    "http://example.com/a,1,b,2"

## Prefix Modifier

The prefix modifier (`:n`) truncates values to a maximum length:

    iex> t = Texture.UriTemplate.parse!("http://example.com/p/{var:3}")
    iex> Texture.UriTemplate.render(t, %{var: "abcdef"})
    "http://example.com/p/abc"

    # Works with reserved expansion
    iex> t = Texture.UriTemplate.parse!("http://example.com{+path:5}")
    iex> Texture.UriTemplate.render(t, %{path: "/a/b/c"})
    "http://example.com/a/b/"

    # Works with fragment expansion
    iex> t = Texture.UriTemplate.parse!("http://example.com{#frag:6}")
    iex> Texture.UriTemplate.render(t, %{frag: "café-bar"})
    "http://example.com#caf%C3%A9-b"

## Undefined Variables

Undefined variables are silently omitted from the output:

    iex> t = Texture.UriTemplate.parse!("http://example.com{/ver}{?q,lang}")
    iex> Texture.UriTemplate.render(t, %{q: "search"})
    "http://example.com?q=search"

    iex> t = Texture.UriTemplate.parse!("http://example.com/users{/id}")
    iex> Texture.UriTemplate.render(t, %{})
    "http://example.com/users"

## Empty Values

Empty strings contribute differently based on the operator:

    # Simple expansion: empty string outputs nothing
    iex> t = Texture.UriTemplate.parse!("http://example.com/a{empty}b")
    iex> Texture.UriTemplate.render(t, %{empty: ""})
    "http://example.com/ab"

    # Query parameter: key with equals sign
    iex> t = Texture.UriTemplate.parse!("http://example.com{?x}")
    iex> Texture.UriTemplate.render(t, %{x: ""})
    "http://example.com?x="

    # Semicolon parameter: key without equals sign
    iex> t = Texture.UriTemplate.parse!("http://example.com{;id}")
    iex> Texture.UriTemplate.render(t, %{id: ""})
    "http://example.com;id"

Empty lists and maps omit the entire expression:

    iex> t = Texture.UriTemplate.parse!("http://example.com/s{?list*}")
    iex> Texture.UriTemplate.render(t, %{list: []})
    "http://example.com/s"

    iex> t = Texture.UriTemplate.parse!("http://example.com/p{;map*}")
    iex> Texture.UriTemplate.render(t, %{map: %{}})
    "http://example.com/p"

## Unicode and Encoding

Unicode characters are properly percent-encoded:

    iex> t = Texture.UriTemplate.parse!("http://example.com/q/{term}")
    iex> Texture.UriTemplate.render(t, %{term: "café"})
    "http://example.com/q/caf%C3%A9"

    # Even in reserved expansion (non-ASCII must be encoded per RFC 6570)
    iex> t = Texture.UriTemplate.parse!("http://example.com/u/{+term}")
    iex> Texture.UriTemplate.render(t, %{term: "東京/渋谷"})
    "http://example.com/u/%E6%9D%B1%E4%BA%AC/%E6%B8%8B%E8%B0%B7"

    iex> t = Texture.UriTemplate.parse!("http://example.com{?emoji}")
    iex> Texture.UriTemplate.render(t, %{emoji: "🙂"})
    "http://example.com?emoji=%F0%9F%99%82"

## Value Coercion

Non-string values are automatically coerced to strings:

    iex> t = Texture.UriTemplate.parse!("http://example.com/t/{num}/{bool}")
    iex> Texture.UriTemplate.render(t, %{num: 0, bool: false})
    "http://example.com/t/0/false"

## Complex Examples

    # Mixed expressions with multiple operators
    iex> t = Texture.UriTemplate.parse!("http://example.com{/ver}{/res*}{?q,lang}{&page}")
    iex> Texture.UriTemplate.render(t, %{ver: "v1", res: ["users", 42], q: "café", lang: "fr", page: 2})
    "http://example.com/v1/users/42?q=caf%C3%A9&lang=fr&page=2"

    # Query continuation with partial matches
    iex> t = Texture.UriTemplate.parse!("http://example.com?fixed=1{&x,y}")
    iex> Texture.UriTemplate.render(t, %{x: 2})
    "http://example.com?fixed=1&x=2"

## Implementation Notes

* Parameter keys can be atoms or binaries – both `%{id: "42"}` and `%{"id" =>
  "42"}` work
* Literal parts of templates (outside `{` `}`) are returned as-is, not
  percent-encoded
* Map key order is not guaranteed in exploded expansions
* Empty lists are treated as undefined values and omit the expression
* Exploding scalar values (e.g., `{var*}`) wraps them in a list
* Lists of tuples (including keyword lists) are rendered as maps when exploded
* Tuples as standalone values are not supported

---

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