# `Ankusa.Routes.Matcher`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/routes/matcher.ex#L1)

Path patterns: the grammar (`segments/1`), the compiled form
(`compile/1`), matching (`match?/2`), and request-path normalization
(`normalize/2`).

A compiled pattern is a list of `t:segment/0`s — no regex, no backtracking,
no way for a crafted request path to cost more than its own length. Literals
compare with `==`; `:param` consumes exactly one segment; `:wildcard` (always
last) requires **one or more** remaining segments, so `/hooks/shopify/*` is
`/hooks/shopify/<something>` and a bare `/hooks/shopify` needs its own route.

## Normalization

`normalize/2` is the only place a request path is turned into matchable
segments, so both the guard and the dry run agree on what "the same path"
means:

  * a percent-encoded slash (`%2F`, any case) is a rejection, not a segment
    boundary — otherwise `/hooks/a%2Fb` would authorize as `/hooks/a/b`;
  * `.` and `..` segments are rejections, not resolved (`/hooks/a/../b` must
    not become `/hooks/b`);
  * empty segments from `//` or a trailing `/` are dropped, and a path with
    nothing left is a rejection.

Plug does **not** percent-decode `path_info`: `Plug.Conn.Adapter` only splits
the path on `/`, so the segments the guard sees are the raw, still-encoded
ones and a `%2F` never turns into a segment boundary here. It is rejected all
the same — an encoded slash is never a legitimate route path, and a later stage
that did decode it would see a different path than the one that was authorized
— and it is detected on the raw `request_path`.

Because segments are matched raw, a literal in a route pattern is limited to
characters that never need percent-encoding (see `segments/1`), so a pattern
that no real request could match is refused when the route is written rather
than accepted and silently dead. An encoded request segment matches only a
`:param` or the wildcard, never a literal: `/hooks/%73tripe` is not
`/hooks/stripe`. Encoded dots (`%2e%2e`) are ordinary opaque segments, not
`..`. Nothing in Ankusa decodes a path, so nothing in Ankusa resolves one —
but the request path is recorded verbatim in the envelope and reaches whatever
consumes it, so a consumer that decodes it owns that step. The guard's job is
only that the route it matched is the one the sender addressed.

# `segment`

```elixir
@type segment() :: {:literal, String.t()} | :param | :wildcard
```

# `compile`

```elixir
@spec compile(Ankusa.Routes.Route.t()) :: [segment()]
```

Compile a route's path pattern. Raises if the pattern is invalid, which can
only happen for a `%Route{}` that was not built through
`Ankusa.Routes.Route.from_attrs/2`.

# `match?`

```elixir
@spec match?([segment()], [String.t()]) :: boolean()
```

Does a compiled pattern match these request segments?

Both sides are non-empty-segment lists (`normalize/2` and `segments/1` drop
empties), so a `:param` never matches an empty string.

# `normalize`

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

Normalize request-path input into matchable segments, or `:error` for a path
no route may match.

# `overlap?`

```elixir
@spec overlap?([segment()], [segment()]) :: boolean()
```

Can one request path match both compiled patterns? Literals must agree at
every position, a `:param` agrees with any single segment, and a `:wildcard`
(always last, one or more segments) agrees with whatever remains of the
other pattern as long as something does.

This is what "two routes shadow each other" means; the route table's
`Ankusa.Routes.Snapshot.overlaps/4` finds the candidates through its prefix
index and asks this about each.

# `segments`

```elixir
@spec segments(String.t()) :: {:ok, [segment()]} | {:error, String.t()}
```

Validate a path pattern and compile it to segments.

`{:error, message}` is the message that goes straight into a route
validation error, so it is phrased as a field complaint.

# `specificity`

```elixir
@spec specificity([segment()]) :: {non_neg_integer(), boolean()}
```

The sort key for candidate ordering: more literal segments first, then
patterns without a wildcard. Ties are broken by route id, which
`Ankusa.Routes.Snapshot` does.

---

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