Snodo.Resource.Template (snodo v0.4.1)

Copy Markdown View Source

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.

Summary

Functions

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

Matches a concrete URI, returning the bound variables.

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

Types

expansion()

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

part()

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

t()

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

variables()

@type variables() :: %{optional(String.t()) => String.t()}

Functions

compile(uri_template)

@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(template, uri)

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

Matches a concrete URI, returning the bound variables.

variables(template)

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

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