@fedify/uri-template 2.0.0-pr.475.1 → 2.3.0-dev.1145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +250 -44
- package/dist/mod.cjs +1901 -0
- package/dist/mod.d.cts +795 -0
- package/dist/mod.d.ts +795 -0
- package/dist/mod.js +1873 -0
- package/package.json +42 -29
- package/dist/_virtual/rolldown_runtime.cjs +0 -30
- package/dist/ast.d.cts +0 -39
- package/dist/ast.d.ts +0 -39
- package/dist/compile.cjs +0 -30
- package/dist/compile.d.cts +0 -80
- package/dist/compile.d.ts +0 -80
- package/dist/compile.js +0 -30
- package/dist/error.cjs +0 -29
- package/dist/error.js +0 -28
- package/dist/expand.cjs +0 -94
- package/dist/expand.d.cts +0 -72
- package/dist/expand.d.ts +0 -72
- package/dist/expand.js +0 -94
- package/dist/index.cjs +0 -9
- package/dist/index.d.cts +0 -6
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -6
- package/dist/match.cjs +0 -180
- package/dist/match.d.cts +0 -92
- package/dist/match.d.ts +0 -92
- package/dist/match.js +0 -180
- package/dist/match.spec.cjs +0 -109
- package/dist/match.spec.d.cts +0 -1
- package/dist/match.spec.d.ts +0 -1
- package/dist/match.spec.js +0 -108
- package/dist/parser.cjs +0 -102
- package/dist/parser.d.cts +0 -35
- package/dist/parser.d.ts +0 -35
- package/dist/parser.js +0 -102
- package/dist/rfc6570.spec.cjs +0 -419
- package/dist/rfc6570.spec.d.cts +0 -1
- package/dist/rfc6570.spec.d.ts +0 -1
- package/dist/rfc6570.spec.js +0 -418
- package/dist/spec.cjs +0 -112
- package/dist/spec.js +0 -108
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,78 +1,284 @@
|
|
|
1
1
|
<!-- deno-fmt-ignore-file -->
|
|
2
2
|
|
|
3
|
-
@fedify/uri-template: RFC 6570 URI Template
|
|
4
|
-
|
|
3
|
+
@fedify/uri-template: Round-trip RFC 6570 URI Template library
|
|
4
|
+
==============================================================
|
|
5
5
|
|
|
6
6
|
[![JSR][JSR badge]][JSR]
|
|
7
7
|
[![npm][npm badge]][npm]
|
|
8
8
|
|
|
9
|
-
This package provides [RFC 6570]
|
|
10
|
-
pattern matching
|
|
11
|
-
|
|
9
|
+
This package provides an [RFC 6570] URI Template implementation that performs
|
|
10
|
+
both expansion and pattern matching with round-trip verification. It is part of
|
|
11
|
+
the [Fedify] framework but can be used independently.
|
|
12
12
|
|
|
13
|
-
[JSR]: https://jsr.io/@fedify/uri-template
|
|
14
13
|
[JSR badge]: https://jsr.io/badges/@fedify/uri-template
|
|
15
|
-
[
|
|
14
|
+
[JSR]: https://jsr.io/@fedify/uri-template
|
|
16
15
|
[npm badge]: https://img.shields.io/npm/v/@fedify/uri-template?logo=npm
|
|
16
|
+
[npm]: https://www.npmjs.com/package/@fedify/uri-template
|
|
17
17
|
[RFC 6570]: https://datatracker.ietf.org/doc/html/rfc6570
|
|
18
|
+
[Fedify]: https://fedify.dev/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Why `@fedify/uri-template`?
|
|
22
|
+
---------------------------
|
|
23
|
+
|
|
24
|
+
Fedify previously relied on two independent third-party implementations:
|
|
25
|
+
[url-template] for URI Template expansion and [uri-template-router] for
|
|
26
|
+
route matching. `@fedify/uri-template` replaces both with one strict RFC 6570
|
|
27
|
+
parser and one expansion/matching model.
|
|
28
|
+
|
|
29
|
+
[url-template]: https://www.npmjs.com/package/url-template
|
|
30
|
+
[uri-template-router]: https://www.npmjs.com/package/uri-template-router
|
|
31
|
+
|
|
32
|
+
### Why replacing [url-template] with `Template`?
|
|
33
|
+
|
|
34
|
+
[url-template] describes itself as an RFC 6570 implementation, but its behavior
|
|
35
|
+
is not strict enough for Fedify's URI routing and round-trip matching needs.
|
|
36
|
+
The test in *old/url-template.test.ts* records the differences against
|
|
37
|
+
`npm:url-template@^3.1.1`.
|
|
38
|
+
|
|
39
|
+
The important failures are:
|
|
40
|
+
|
|
41
|
+
- It double-encodes pct-encoded triplets in variable names when named
|
|
42
|
+
operators emit the variable name. For example, `{?abc%20def}` expands to
|
|
43
|
+
`?abc%2520def=spaced` instead of `?abc%20def=spaced`. [RFC 6570 §2.3]
|
|
44
|
+
allows `pct-encoded` inside `varname` and treats it as part of the
|
|
45
|
+
variable name. [RFC 6570 §3.2.8] emits the variable name as a literal
|
|
46
|
+
string, and [RFC 6570 §2.1] permits `pct-encoded` literals. Therefore
|
|
47
|
+
`%20` and `%41` must be preserved, not encoded again as `%2520` and
|
|
48
|
+
`%2541`.
|
|
49
|
+
- It accepts malformed templates instead of reporting syntax errors.
|
|
50
|
+
[RFC 6570 §2] requires expressions to be delimited by matching braces,
|
|
51
|
+
[RFC 6570 §2.1] excludes raw braces, control characters, spaces, raw `%`
|
|
52
|
+
outside a pct-encoded triplet, and other forbidden literal characters, and
|
|
53
|
+
[RFC 6570 §3] says grammar errors should indicate their location and type
|
|
54
|
+
to the invoking application. `@fedify/uri-template` reports these cases as
|
|
55
|
+
typed errors.
|
|
56
|
+
- It applies prefix modifiers to composite values such as lists and
|
|
57
|
+
associative arrays. [RFC 6570 §2.4.1] states that prefix modifiers are not
|
|
58
|
+
applicable to variables with composite values, so `{list:3}`, `{keys:3}`,
|
|
59
|
+
and `{count:2}` must fail.
|
|
60
|
+
|
|
61
|
+
`Template` was written as a new implementation instead of wrapping
|
|
62
|
+
[url-template] because Fedify needs strict RFC 6570 expansion, typed syntax
|
|
63
|
+
errors, and round-trip-checked matching behavior. Applications that need a
|
|
64
|
+
looser parser can opt in explicitly: `strict: false` passes parse and expansion
|
|
65
|
+
errors to `report` without throwing, and a custom `report` function can allow
|
|
66
|
+
all errors or throw only for selected error classes.
|
|
67
|
+
|
|
68
|
+
[RFC 6570 §2.3]: https://datatracker.ietf.org/doc/html/rfc6570#section-2.3
|
|
69
|
+
[RFC 6570 §3.2.8]: https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8
|
|
70
|
+
[RFC 6570 §2.1]: https://datatracker.ietf.org/doc/html/rfc6570#section-2.1
|
|
71
|
+
[RFC 6570 §2]: https://datatracker.ietf.org/doc/html/rfc6570#section-2
|
|
72
|
+
[RFC 6570 §3]: https://datatracker.ietf.org/doc/html/rfc6570#section-3
|
|
73
|
+
[RFC 6570 §2.4.1]: https://datatracker.ietf.org/doc/html/rfc6570#section-2.4.1
|
|
74
|
+
|
|
75
|
+
### Why replacing [uri-template-router] with `Router`?
|
|
76
|
+
|
|
77
|
+
The previous router shape combined two independent third-party
|
|
78
|
+
implementations: [url-template] for building URLs and [uri-template-router] for
|
|
79
|
+
matching URLs. *old/uri-template-router.test.ts* defines that old shape as
|
|
80
|
+
closely as possible so the differences are visible under the same route API.
|
|
81
|
+
|
|
82
|
+
The important differences are:
|
|
83
|
+
|
|
84
|
+
- Build, match, and variable extraction all use the same strict RFC 6570
|
|
85
|
+
parser. The previous router expanded with [url-template] but matched with
|
|
86
|
+
[uri-template-router], so a value could be encoded by one implementation
|
|
87
|
+
and decoded by another with different rules.
|
|
88
|
+
- Route matches are round-trip checked. A candidate route is accepted only
|
|
89
|
+
when the recovered values expand back to the exact input URI. This rejects
|
|
90
|
+
matches that look plausible after decoding but cannot reproduce the
|
|
91
|
+
original URI. By default, matching is exact against the input URI.
|
|
92
|
+
`trailingSlashInsensitive` can be enabled to use a looser path lookup for
|
|
93
|
+
trailing slash differences before applying the same round-trip check.
|
|
94
|
+
- Pct-encoded triplets are preserved where RFC 6570 treats them as syntax.
|
|
95
|
+
Literal triplets, pct-encoded variable names, and named query parameters
|
|
96
|
+
such as `{?abc%20def}` remain `%20` instead of becoming `%2520`.
|
|
97
|
+
- Reserved expansion values keep their encoded form when that is what the
|
|
98
|
+
URI contained. Under the previous matching path, `/files/a%2Fb` could be
|
|
99
|
+
reported as `a/b`, `/files/%30%23` as `0#`, and pct-encoded UTF-8 octets as
|
|
100
|
+
Unicode text. Those values do not round-trip to the original URI under the
|
|
101
|
+
same template.
|
|
102
|
+
- Path templates are validated by `Router.compile()` before registration.
|
|
103
|
+
The standalone router accepts ordinary slash-prefixed paths and the
|
|
104
|
+
leading path-expansion form—a template that begins with a `{/var}`
|
|
105
|
+
expression, such as `{/identifier}/inbox`. Accepting that shape is a
|
|
106
|
+
standalone-router capability and is independent of Fedify: Fedify's own
|
|
107
|
+
dispatcher routes apply a non-empty constraint to required identifiers
|
|
108
|
+
(`nullable` defaults to `false`), so a leading path-expansion route
|
|
109
|
+
registers but only matches when the variable is actually bound, and the
|
|
110
|
+
Fedify builder may reject such a shape for routes whose callback
|
|
111
|
+
contract requires a concrete `identifier`.
|
|
112
|
+
- `Router.variables()` and `Router.compile()` expose variable extraction
|
|
113
|
+
without mutating a router. The legacy `Router.add()` returned variables as
|
|
114
|
+
a side effect of registering the route.
|
|
115
|
+
- Candidate lookup combines a token-level state trie with a fallback prefix
|
|
116
|
+
trie. Indexable path templates—those whose expressions each hold a single
|
|
117
|
+
variable with the `""`, `/`, or `+` operator and never sit directly
|
|
118
|
+
adjacent to another expression—are walked token by token in the state
|
|
119
|
+
trie. Shapes that cannot be safely indexed fall back to a prefix trie
|
|
120
|
+
keyed by the initial literal prefix of each route. Candidates from both
|
|
121
|
+
tries are merged, deduplicated, and ordered deterministically by literal
|
|
122
|
+
length, initial literal prefix length, variable count, and insertion order
|
|
123
|
+
before the round-trip matcher runs.
|
|
124
|
+
- Cloning and route replacement do not depend on copying private mutable
|
|
125
|
+
state from [uri-template-router]. The router stores compiled templates and
|
|
126
|
+
active route entries directly, which keeps the implementation independent
|
|
127
|
+
and dependency-free at runtime.
|
|
128
|
+
|
|
129
|
+
The concrete differences from the previous [url-template] and
|
|
130
|
+
[uri-template-router] libraries are encoded as repository-only compatibility
|
|
131
|
+
tests under *packages/uri-template/old/* in the package's source repository.
|
|
132
|
+
Those tests intentionally fail when run with `deno task test:old` because they
|
|
133
|
+
execute the older libraries against Fedify's expected behavior and document the
|
|
134
|
+
known legacy gaps.
|
|
18
135
|
|
|
19
136
|
|
|
20
137
|
Features
|
|
21
138
|
--------
|
|
22
139
|
|
|
23
|
-
-
|
|
24
|
-
(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
RFC rules
|
|
140
|
+
- Full RFC 6570 expansion for all expression types
|
|
141
|
+
(`{var}`, `{+var}`, `{#var}`, `{.var}`, `{/var}`, `{;var}`, `{?var}`,
|
|
142
|
+
`{&var}`)
|
|
143
|
+
- Round-trip pattern matching that mirrors expansion: when `match(uri)`
|
|
144
|
+
returns values, `expand(values) === uri`
|
|
145
|
+
- Per-variable matching constraints (`nullable`, `multiple`) with safe
|
|
146
|
+
defaults
|
|
147
|
+
- Strict TypeScript types with no `any` in the public surface
|
|
148
|
+
- Zero runtime dependencies
|
|
33
149
|
|
|
34
150
|
|
|
35
|
-
|
|
36
|
-
|
|
151
|
+
Route variable constraints
|
|
152
|
+
--------------------------
|
|
37
153
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
154
|
+
`Router` registers every RFC 6570 operator, but matching is constrained
|
|
155
|
+
per template variable. `Router.add()`, `Router.register()`, the
|
|
156
|
+
constructor, and `Router.from()` accept the route as a
|
|
157
|
+
`[pathOrPattern, name, options?]` tuple, where the optional third element
|
|
158
|
+
is the per-route options object:
|
|
159
|
+
|
|
160
|
+
~~~~ typescript
|
|
161
|
+
import { Router } from "@fedify/uri-template";
|
|
162
|
+
|
|
163
|
+
const router = new Router();
|
|
164
|
+
router.add("/users/{identifier}", "actor");
|
|
165
|
+
router.add("/search{?q}", "search", {
|
|
166
|
+
variables: { q: { nullable: true } },
|
|
167
|
+
});
|
|
44
168
|
~~~~
|
|
45
169
|
|
|
170
|
+
`options.variables` maps a variable name to a partial constraint; any
|
|
171
|
+
field you omit falls back to its default, and any template variable you
|
|
172
|
+
do not list is still constrained with the all-default constraint. The
|
|
173
|
+
constraint fields are:
|
|
46
174
|
|
|
47
|
-
|
|
48
|
-
|
|
175
|
+
- **`nullable`** defaults to `false`: a variable that is unbound or binds
|
|
176
|
+
to an empty value makes the route a no-match (the router falls back to
|
|
177
|
+
the next candidate). Pass `{ nullable: true }` to opt out, so an
|
|
178
|
+
optional operator such as `{?q}` may match with `q` absent. Because
|
|
179
|
+
of this default, optional-operator and leading-path-expansion routes
|
|
180
|
+
register successfully but only match when the variable is actually
|
|
181
|
+
present and non-empty.
|
|
182
|
+
- **`multiple`** is derived from the variable specification: explode
|
|
183
|
+
(`{tags*}`) implies `true` and binds `readonly string[]`; a prefix
|
|
184
|
+
modifier (`{id:3}`) implies `false`; a plain variable defaults to
|
|
185
|
+
`false` (binding `string`) but may be set either way. Specifying a
|
|
186
|
+
`multiple` that contradicts the derived value, or using the same
|
|
187
|
+
variable name with conflicting explode/prefix modifiers (`{x}` and
|
|
188
|
+
`{x*}`), throws `ConflictingVarSpecError` at registration time.
|
|
189
|
+
- **`duplicable`** defaults to `false`: a variable that appears in more
|
|
190
|
+
than one variable specification within the same template throws
|
|
191
|
+
`DuplicateRouteVariableError` at registration time. Set it to `true`
|
|
192
|
+
to allow repeated occurrences; their bindings must still agree when a
|
|
193
|
+
URI is matched.
|
|
194
|
+
- **`prefixable`** defaults to `false`: a `{var:N}` prefix-modifier
|
|
195
|
+
specification throws `DisallowedVarSpecModifierError` unless the
|
|
196
|
+
variable is marked `{ prefixable: true }`.
|
|
197
|
+
- **`explodable`** defaults to `false`: it is a *registration
|
|
198
|
+
permission*, not an output-shape declaration. A `{var*}`
|
|
199
|
+
explode-modifier specification throws `DisallowedVarSpecModifierError`
|
|
200
|
+
unless the variable is marked `{ explodable: true }`; the option by
|
|
201
|
+
itself does not turn a value into a list. A value becomes a
|
|
202
|
+
`readonly string[]` only when the template actually uses the explode
|
|
203
|
+
modifier (`{var*}`), because that varspec is what resolves `multiple`
|
|
204
|
+
to `true`. The same `{ explodable: true }` set on a non-exploded spec
|
|
205
|
+
such as `/users/{id}` still binds a scalar `string` at runtime.
|
|
206
|
+
- **`operatables`** defaults to `[]`, which permits every operator.
|
|
207
|
+
When set to a non-empty list of operators (`""`, `"+"`, `"#"`, `"."`,
|
|
208
|
+
`"/"`, `";"`, `"?"`, `"&"`), using the variable under any operator
|
|
209
|
+
outside the list throws `DisallowedOperatorError` at registration
|
|
210
|
+
time.
|
|
211
|
+
|
|
212
|
+
The options object also accepts **`exact`**, which defaults to `true`:
|
|
213
|
+
when you supply a `variables` object, its keys must match the template's
|
|
214
|
+
variables exactly—every template variable must be listed and no unknown
|
|
215
|
+
key may appear, otherwise registration throws
|
|
216
|
+
`RouteTemplateOptionsNotMatchedError`. Set `{ exact: false }` to relax
|
|
217
|
+
this so unlisted variables keep their defaults and unknown keys are
|
|
218
|
+
ignored. Routes registered without a `variables` object are unaffected
|
|
219
|
+
and keep every default.
|
|
49
220
|
|
|
50
221
|
~~~~ typescript
|
|
51
|
-
|
|
222
|
+
const router = new Router();
|
|
52
223
|
|
|
53
|
-
|
|
224
|
+
// Throws RouteTemplateOptionsNotMatchedError: `id` is not listed.
|
|
225
|
+
router.add("/posts/{slug}/{id}", "post", {
|
|
226
|
+
variables: { slug: { nullable: true } },
|
|
227
|
+
});
|
|
54
228
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
229
|
+
// OK: opt out of the exact-keys check.
|
|
230
|
+
router.add("/posts/{slug}/{id}", "post", {
|
|
231
|
+
exact: false,
|
|
232
|
+
variables: { slug: { nullable: true } },
|
|
233
|
+
});
|
|
58
234
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
235
|
+
// OK: explode requires opting in.
|
|
236
|
+
router.add("/tags{/tags*}", "tags", {
|
|
237
|
+
variables: { tags: { explodable: true } },
|
|
62
238
|
});
|
|
63
|
-
// => { owner: "foo", repo: "hello/world", q: "a b" }
|
|
64
239
|
~~~~
|
|
65
240
|
|
|
66
|
-
|
|
241
|
+
`Router.route()` is generic over the constraint map, so `values` narrows
|
|
242
|
+
accordingly:
|
|
243
|
+
|
|
244
|
+
~~~~ typescript
|
|
245
|
+
const constraints = {
|
|
246
|
+
identifier: { nullable: false, multiple: false },
|
|
247
|
+
} as const;
|
|
248
|
+
router.add("/users/{identifier}", "actor", { variables: constraints });
|
|
249
|
+
|
|
250
|
+
const matched = router.route<typeof constraints>("/users/alice");
|
|
251
|
+
if (matched != null) {
|
|
252
|
+
const id: string = matched.values.identifier;
|
|
253
|
+
}
|
|
254
|
+
~~~~
|
|
255
|
+
|
|
256
|
+
The narrowed type is derived from `multiple` and `nullable` only, never
|
|
257
|
+
from `explodable`. Since `explodable` governs registration rather than
|
|
258
|
+
the resolved value shape, an exploded route must carry `multiple: true`
|
|
259
|
+
in the type argument—not merely `explodable: true`—for `values` to narrow
|
|
260
|
+
to `readonly string[]`:
|
|
67
261
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
262
|
+
~~~~ typescript
|
|
263
|
+
const tagConstraints = {
|
|
264
|
+
tags: { explodable: true, multiple: true },
|
|
265
|
+
} as const;
|
|
266
|
+
router.add("/tags{?tags*}", "tags", { variables: tagConstraints });
|
|
71
267
|
|
|
268
|
+
const matched = router.route<typeof tagConstraints>("/tags?tags=a&tags=b");
|
|
269
|
+
if (matched != null) {
|
|
270
|
+
const tags: readonly string[] = matched.values.tags;
|
|
271
|
+
}
|
|
272
|
+
~~~~
|
|
72
273
|
|
|
73
|
-
Documentation
|
|
74
|
-
-------------
|
|
75
274
|
|
|
76
|
-
|
|
275
|
+
Installation
|
|
276
|
+
------------
|
|
77
277
|
|
|
78
|
-
|
|
278
|
+
~~~~ bash
|
|
279
|
+
deno add jsr:@fedify/uri-template # Deno
|
|
280
|
+
npm add @fedify/uri-template # npm
|
|
281
|
+
pnpm add @fedify/uri-template # pnpm
|
|
282
|
+
yarn add @fedify/uri-template # Yarn
|
|
283
|
+
bun add @fedify/uri-template # Bun
|
|
284
|
+
~~~~
|