@typed/router 0.32.0 → 1.0.0-beta.1
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/README.md +129 -2
- package/dist/AST.d.ts +96 -0
- package/dist/AST.d.ts.map +1 -0
- package/dist/AST.js +32 -0
- package/dist/CurrentRoute.d.ts +18 -0
- package/dist/CurrentRoute.d.ts.map +1 -0
- package/dist/CurrentRoute.js +18 -0
- package/dist/Matcher.d.ts +209 -0
- package/dist/Matcher.d.ts.map +1 -0
- package/dist/Matcher.js +633 -0
- package/dist/Parser.d.ts +92 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +1 -0
- package/dist/Path.d.ts +216 -0
- package/dist/Path.d.ts.map +1 -0
- package/dist/Path.js +248 -0
- package/dist/Route.d.ts +57 -0
- package/dist/Route.d.ts.map +1 -0
- package/dist/Route.js +151 -0
- package/dist/Router.d.ts +9 -0
- package/dist/Router.d.ts.map +1 -0
- package/dist/Router.js +8 -0
- package/dist/Uri.d.ts +115 -0
- package/dist/Uri.d.ts.map +1 -0
- package/dist/Uri.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/package.json +32 -73
- package/src/AST.ts +166 -0
- package/src/CurrentRoute.ts +30 -331
- package/src/Matcher.test.ts +496 -0
- package/src/Matcher.ts +1375 -325
- package/src/Parser.ts +276 -0
- package/src/Path.test.ts +318 -0
- package/src/Path.ts +691 -0
- package/src/Route.test.ts +268 -0
- package/src/Route.ts +316 -0
- package/src/Router.ts +33 -0
- package/src/Uri.ts +214 -0
- package/src/index.ts +4 -28
- package/CurrentRoute/package.json +0 -6
- package/LICENSE +0 -21
- package/MatchInput/package.json +0 -6
- package/Matcher/package.json +0 -6
- package/RouteGuard/package.json +0 -6
- package/RouteMatch/package.json +0 -6
- package/dist/cjs/CurrentRoute.js +0 -170
- package/dist/cjs/CurrentRoute.js.map +0 -1
- package/dist/cjs/MatchInput.js +0 -96
- package/dist/cjs/MatchInput.js.map +0 -1
- package/dist/cjs/Matcher.js +0 -138
- package/dist/cjs/Matcher.js.map +0 -1
- package/dist/cjs/RouteGuard.js +0 -78
- package/dist/cjs/RouteGuard.js.map +0 -1
- package/dist/cjs/RouteMatch.js +0 -49
- package/dist/cjs/RouteMatch.js.map +0 -1
- package/dist/cjs/index.js +0 -53
- package/dist/cjs/index.js.map +0 -1
- package/dist/dts/CurrentRoute.d.ts +0 -94
- package/dist/dts/CurrentRoute.d.ts.map +0 -1
- package/dist/dts/MatchInput.d.ts +0 -143
- package/dist/dts/MatchInput.d.ts.map +0 -1
- package/dist/dts/Matcher.d.ts +0 -121
- package/dist/dts/Matcher.d.ts.map +0 -1
- package/dist/dts/RouteGuard.d.ts +0 -94
- package/dist/dts/RouteGuard.d.ts.map +0 -1
- package/dist/dts/RouteMatch.d.ts +0 -50
- package/dist/dts/RouteMatch.d.ts.map +0 -1
- package/dist/dts/index.d.ts +0 -24
- package/dist/dts/index.d.ts.map +0 -1
- package/dist/esm/CurrentRoute.js +0 -152
- package/dist/esm/CurrentRoute.js.map +0 -1
- package/dist/esm/MatchInput.js +0 -79
- package/dist/esm/MatchInput.js.map +0 -1
- package/dist/esm/Matcher.js +0 -130
- package/dist/esm/Matcher.js.map +0 -1
- package/dist/esm/RouteGuard.js +0 -57
- package/dist/esm/RouteGuard.js.map +0 -1
- package/dist/esm/RouteMatch.js +0 -29
- package/dist/esm/RouteMatch.js.map +0 -1
- package/dist/esm/index.js +0 -24
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/package.json +0 -4
- package/src/MatchInput.ts +0 -303
- package/src/RouteGuard.ts +0 -217
- package/src/RouteMatch.ts +0 -104
package/src/Parser.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { Arg0, Pipe, TypeLambda, TypeLambda1 } from "hkt-core";
|
|
2
|
+
|
|
3
|
+
export type Result<Value, Rest extends string> = readonly [value: Value, rest: Rest];
|
|
4
|
+
|
|
5
|
+
export interface Parser<Output = unknown> extends TypeLambda<
|
|
6
|
+
[input: string],
|
|
7
|
+
Result<Output, string>
|
|
8
|
+
> {}
|
|
9
|
+
|
|
10
|
+
export type Parse<P extends Parser<unknown>, Input extends string> = Pipe<Input, P>;
|
|
11
|
+
|
|
12
|
+
type IsStringLiteral<T extends string> = string extends T ? false : true;
|
|
13
|
+
|
|
14
|
+
type StrictEquals<A, B> =
|
|
15
|
+
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
|
|
16
|
+
|
|
17
|
+
type IsNoProgress<Input extends string, Rest extends string> =
|
|
18
|
+
IsStringLiteral<Input> extends true ? StrictEquals<Input, Rest> : false;
|
|
19
|
+
|
|
20
|
+
export declare namespace Parser {
|
|
21
|
+
export type Any = Parser<unknown>;
|
|
22
|
+
|
|
23
|
+
export type Run<P extends Any, Input extends string> = Pipe<Input, P>;
|
|
24
|
+
|
|
25
|
+
export interface Succeed<A> extends TypeLambda<[input: string], Result<A, string>> {
|
|
26
|
+
readonly return: Arg0<this> extends infer Input extends string ? readonly [A, Input] : never;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Fail extends TypeLambda<[input: string], never> {
|
|
30
|
+
readonly return: never;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Char<C extends string> extends TypeLambda<[input: string], Result<C, string>> {
|
|
34
|
+
readonly return: Arg0<this> extends `${C}${infer Rest}` ? readonly [C, Rest] : never;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface String<S extends string> extends TypeLambda<[input: string], Result<S, string>> {
|
|
38
|
+
readonly return: Arg0<this> extends `${S}${infer Rest}` ? readonly [S, Rest] : never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type LowercaseAlphabet =
|
|
42
|
+
| "a"
|
|
43
|
+
| "b"
|
|
44
|
+
| "c"
|
|
45
|
+
| "d"
|
|
46
|
+
| "e"
|
|
47
|
+
| "f"
|
|
48
|
+
| "g"
|
|
49
|
+
| "h"
|
|
50
|
+
| "i"
|
|
51
|
+
| "j"
|
|
52
|
+
| "k"
|
|
53
|
+
| "l"
|
|
54
|
+
| "m"
|
|
55
|
+
| "n"
|
|
56
|
+
| "o"
|
|
57
|
+
| "p"
|
|
58
|
+
| "q"
|
|
59
|
+
| "r"
|
|
60
|
+
| "s"
|
|
61
|
+
| "t"
|
|
62
|
+
| "u"
|
|
63
|
+
| "v"
|
|
64
|
+
| "w"
|
|
65
|
+
| "x"
|
|
66
|
+
| "y"
|
|
67
|
+
| "z";
|
|
68
|
+
|
|
69
|
+
export type UppercaseAlphabet =
|
|
70
|
+
| "A"
|
|
71
|
+
| "B"
|
|
72
|
+
| "C"
|
|
73
|
+
| "D"
|
|
74
|
+
| "E"
|
|
75
|
+
| "F"
|
|
76
|
+
| "G"
|
|
77
|
+
| "H"
|
|
78
|
+
| "I"
|
|
79
|
+
| "J"
|
|
80
|
+
| "K"
|
|
81
|
+
| "L"
|
|
82
|
+
| "M"
|
|
83
|
+
| "N"
|
|
84
|
+
| "O"
|
|
85
|
+
| "P"
|
|
86
|
+
| "Q"
|
|
87
|
+
| "R"
|
|
88
|
+
| "S"
|
|
89
|
+
| "T"
|
|
90
|
+
| "U"
|
|
91
|
+
| "V"
|
|
92
|
+
| "W"
|
|
93
|
+
| "X"
|
|
94
|
+
| "Y"
|
|
95
|
+
| "Z";
|
|
96
|
+
|
|
97
|
+
export type Alphabet = LowercaseAlphabet | UppercaseAlphabet;
|
|
98
|
+
|
|
99
|
+
export type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
100
|
+
|
|
101
|
+
export type AlphaNumeric = Alphabet | Digit;
|
|
102
|
+
|
|
103
|
+
type TakeWhileInternal<
|
|
104
|
+
Input extends string,
|
|
105
|
+
Allowed extends string,
|
|
106
|
+
Acc extends string = "",
|
|
107
|
+
> = Input extends `${infer Head}${infer Tail}`
|
|
108
|
+
? Head extends Allowed
|
|
109
|
+
? TakeWhileInternal<Tail, Allowed, `${Acc}${Head}`>
|
|
110
|
+
: readonly [Acc, Input]
|
|
111
|
+
: readonly [Acc, Input];
|
|
112
|
+
|
|
113
|
+
type TakeWhile1Internal<Input extends string, Allowed extends string> =
|
|
114
|
+
TakeWhileInternal<Input, Allowed> extends readonly [
|
|
115
|
+
infer Taken extends string,
|
|
116
|
+
infer Rest extends string,
|
|
117
|
+
]
|
|
118
|
+
? Taken extends ""
|
|
119
|
+
? never
|
|
120
|
+
: readonly [Taken, Rest]
|
|
121
|
+
: never;
|
|
122
|
+
|
|
123
|
+
export interface TakeWhile<Allowed extends string> extends TypeLambda<
|
|
124
|
+
[input: string],
|
|
125
|
+
Result<string, string>
|
|
126
|
+
> {
|
|
127
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
128
|
+
? TakeWhileInternal<Input, Allowed>
|
|
129
|
+
: never;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface TakeWhile1<Allowed extends string> extends TypeLambda<
|
|
133
|
+
[input: string],
|
|
134
|
+
Result<string, string>
|
|
135
|
+
> {
|
|
136
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
137
|
+
? TakeWhile1Internal<Input, Allowed>
|
|
138
|
+
: never;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Map<P extends Any, F extends TypeLambda1> extends Parser<unknown> {
|
|
142
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
143
|
+
? Pipe<Input, P> extends infer R
|
|
144
|
+
? [R] extends [never]
|
|
145
|
+
? never
|
|
146
|
+
: R extends readonly [infer Value, infer Rest extends string]
|
|
147
|
+
? readonly [Pipe<Value, F>, Rest]
|
|
148
|
+
: never
|
|
149
|
+
: never
|
|
150
|
+
: never;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface FlatMap<P extends Any, F extends TypeLambda1> extends Parser<unknown> {
|
|
154
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
155
|
+
? Pipe<Input, P> extends infer R
|
|
156
|
+
? [R] extends [never]
|
|
157
|
+
? never
|
|
158
|
+
: R extends readonly [infer Value, infer Rest extends string]
|
|
159
|
+
? Pipe<Value, F> extends infer Next
|
|
160
|
+
? [Next] extends [never]
|
|
161
|
+
? never
|
|
162
|
+
: Next extends Any
|
|
163
|
+
? Pipe<Rest, Next>
|
|
164
|
+
: never
|
|
165
|
+
: never
|
|
166
|
+
: never
|
|
167
|
+
: never
|
|
168
|
+
: never;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface Zip<P extends Any, Q extends Any> extends Parser<unknown> {
|
|
172
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
173
|
+
? Pipe<Input, P> extends infer R1
|
|
174
|
+
? [R1] extends [never]
|
|
175
|
+
? never
|
|
176
|
+
: R1 extends readonly [infer Value1, infer Rest1 extends string]
|
|
177
|
+
? Pipe<Rest1, Q> extends infer R2
|
|
178
|
+
? [R2] extends [never]
|
|
179
|
+
? never
|
|
180
|
+
: R2 extends readonly [infer Value2, infer Rest2 extends string]
|
|
181
|
+
? readonly [readonly [Value1, Value2], Rest2]
|
|
182
|
+
: never
|
|
183
|
+
: never
|
|
184
|
+
: never
|
|
185
|
+
: never
|
|
186
|
+
: never;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface OrElse<P extends Any, Q extends Any> extends Parser<unknown> {
|
|
190
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
191
|
+
? Pipe<Input, P> extends infer R
|
|
192
|
+
? [R] extends [never]
|
|
193
|
+
? Pipe<Input, Q>
|
|
194
|
+
: R extends readonly [infer Value, infer Rest extends string]
|
|
195
|
+
? readonly [Value, Rest]
|
|
196
|
+
: never
|
|
197
|
+
: never
|
|
198
|
+
: never;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface Optional<P extends Any> extends Parser<unknown> {
|
|
202
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
203
|
+
? Pipe<Input, P> extends infer R
|
|
204
|
+
? [R] extends [never]
|
|
205
|
+
? readonly [undefined, Input]
|
|
206
|
+
: R extends readonly [infer Value, infer Rest extends string]
|
|
207
|
+
? readonly [Value, Rest]
|
|
208
|
+
: never
|
|
209
|
+
: never
|
|
210
|
+
: never;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type ManyInternal<
|
|
214
|
+
P extends Any,
|
|
215
|
+
Input extends string,
|
|
216
|
+
Acc extends ReadonlyArray<unknown> = readonly [],
|
|
217
|
+
> =
|
|
218
|
+
Pipe<Input, P> extends infer R
|
|
219
|
+
? [R] extends [never]
|
|
220
|
+
? readonly [Acc, Input]
|
|
221
|
+
: R extends readonly [infer Value, infer Rest extends string]
|
|
222
|
+
? IsNoProgress<Input, Rest> extends true
|
|
223
|
+
? never
|
|
224
|
+
: ManyInternal<P, Rest, readonly [...Acc, Value]>
|
|
225
|
+
: never
|
|
226
|
+
: never;
|
|
227
|
+
|
|
228
|
+
type Many1Internal<P extends Any, Input extends string> =
|
|
229
|
+
Pipe<Input, P> extends infer R
|
|
230
|
+
? [R] extends [never]
|
|
231
|
+
? never
|
|
232
|
+
: R extends readonly [infer Value, infer Rest extends string]
|
|
233
|
+
? IsNoProgress<Input, Rest> extends true
|
|
234
|
+
? never
|
|
235
|
+
: ManyInternal<P, Rest, readonly [Value]>
|
|
236
|
+
: never
|
|
237
|
+
: never;
|
|
238
|
+
|
|
239
|
+
export interface Many<P extends Any> extends Parser<unknown> {
|
|
240
|
+
readonly return: Arg0<this> extends infer Input extends string ? ManyInternal<P, Input> : never;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface Many1<P extends Any> extends Parser<unknown> {
|
|
244
|
+
readonly return: Arg0<this> extends infer Input extends string
|
|
245
|
+
? Many1Internal<P, Input>
|
|
246
|
+
: never;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface MapTo<F extends TypeLambda1> extends TypeLambda1 {
|
|
250
|
+
readonly return: Arg0<this> extends infer P extends Any ? Map<P, F> : never;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface FlatMapTo<F extends TypeLambda1> extends TypeLambda1 {
|
|
254
|
+
readonly return: Arg0<this> extends infer P extends Any ? FlatMap<P, F> : never;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface ZipWith<Q extends Any> extends TypeLambda1 {
|
|
258
|
+
readonly return: Arg0<this> extends infer P extends Any ? Zip<P, Q> : never;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface OrElseWith<Q extends Any> extends TypeLambda1 {
|
|
262
|
+
readonly return: Arg0<this> extends infer P extends Any ? OrElse<P, Q> : never;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface OptionalOf extends TypeLambda1 {
|
|
266
|
+
readonly return: Arg0<this> extends infer P extends Any ? Optional<P> : never;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface ManyOf extends TypeLambda1 {
|
|
270
|
+
readonly return: Arg0<this> extends infer P extends Any ? Many<P> : never;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface Many1Of extends TypeLambda1 {
|
|
274
|
+
readonly return: Arg0<this> extends infer P extends Any ? Many1<P> : never;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/Path.test.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import * as AST from "./AST.js";
|
|
4
|
+
import * as Path from "./Path.js";
|
|
5
|
+
|
|
6
|
+
describe("typed/router/Path", () => {
|
|
7
|
+
describe("parseWithRest", () => {
|
|
8
|
+
it("parses literals and parameters", () => {
|
|
9
|
+
const [asts, rest] = Path.parseWithRest("/users/:id");
|
|
10
|
+
|
|
11
|
+
expect(asts).toEqual([AST.literal("users"), AST.slash(), AST.parameter("id")]);
|
|
12
|
+
expect(rest).toEqual("");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("parses wildcard segments", () => {
|
|
16
|
+
const [asts, rest] = Path.parseWithRest("/files/*");
|
|
17
|
+
|
|
18
|
+
expect(asts).toEqual([AST.literal("files"), AST.slash(), AST.wildcard()]);
|
|
19
|
+
expect(rest).toEqual("");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses parameters with regex and optional mark", () => {
|
|
23
|
+
const [asts, rest] = Path.parseWithRest("/:id(\\d+)?");
|
|
24
|
+
|
|
25
|
+
expect(asts).toEqual([AST.parameter("id", true, "\\d+")]);
|
|
26
|
+
expect(rest).toEqual("");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("emits slash AST nodes only when a '/' actually separates atoms", () => {
|
|
30
|
+
expect(Path.parseWithRest("*foo")[0]).toEqual([AST.wildcard(), AST.literal("foo")]);
|
|
31
|
+
expect(Path.parseWithRest("*/foo")[0]).toEqual([
|
|
32
|
+
AST.wildcard(),
|
|
33
|
+
AST.slash(),
|
|
34
|
+
AST.literal("foo"),
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("parses query params with literal, parameter, and wildcard values", () => {
|
|
39
|
+
const [asts, rest] = Path.parseWithRest("/search?term=:term&limit=10&rest=*");
|
|
40
|
+
|
|
41
|
+
expect(asts).toEqual([
|
|
42
|
+
AST.literal("search"),
|
|
43
|
+
AST.queryParams([
|
|
44
|
+
AST.queryParam("term", AST.parameter("term")),
|
|
45
|
+
AST.queryParam("limit", AST.literal("10")),
|
|
46
|
+
AST.queryParam("rest", AST.wildcard()),
|
|
47
|
+
]),
|
|
48
|
+
]);
|
|
49
|
+
expect(rest).toEqual("");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("stops parsing query params when a tail param can't be parsed, leaving '&' for the outer parser", () => {
|
|
53
|
+
const [asts, rest] = Path.parseWithRest("/?a=b&");
|
|
54
|
+
|
|
55
|
+
expect(asts).toEqual([
|
|
56
|
+
AST.queryParams([AST.queryParam("a", AST.literal("b"))]),
|
|
57
|
+
AST.literal("&"),
|
|
58
|
+
]);
|
|
59
|
+
expect(rest).toEqual("");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("parses multiple path segments", () => {
|
|
63
|
+
const [asts, rest] = Path.parseWithRest("/api/v1/users/:userId/posts/:postId");
|
|
64
|
+
|
|
65
|
+
expect(asts).toEqual([
|
|
66
|
+
AST.literal("api"),
|
|
67
|
+
AST.slash(),
|
|
68
|
+
AST.literal("v1"),
|
|
69
|
+
AST.slash(),
|
|
70
|
+
AST.literal("users"),
|
|
71
|
+
AST.slash(),
|
|
72
|
+
AST.parameter("userId"),
|
|
73
|
+
AST.slash(),
|
|
74
|
+
AST.literal("posts"),
|
|
75
|
+
AST.slash(),
|
|
76
|
+
AST.parameter("postId"),
|
|
77
|
+
]);
|
|
78
|
+
expect(rest).toEqual("");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("parses parameter with regex only (no optional)", () => {
|
|
82
|
+
const [asts, rest] = Path.parseWithRest("/:id(\\d+)");
|
|
83
|
+
|
|
84
|
+
expect(asts).toEqual([AST.parameter("id", undefined, "\\d+")]);
|
|
85
|
+
expect(rest).toEqual("");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("parses optional parameter without regex", () => {
|
|
89
|
+
const [asts, rest] = Path.parseWithRest("/:id?");
|
|
90
|
+
|
|
91
|
+
expect(asts).toEqual([AST.parameter("id", true)]);
|
|
92
|
+
expect(rest).toEqual("");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("parses consecutive parameters", () => {
|
|
96
|
+
const [asts, rest] = Path.parseWithRest("/:a:b");
|
|
97
|
+
|
|
98
|
+
expect(asts).toEqual([AST.parameter("a"), AST.parameter("b")]);
|
|
99
|
+
expect(rest).toEqual("");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("parses mixed slashes", () => {
|
|
103
|
+
const [asts, rest] = Path.parseWithRest("//users///profile");
|
|
104
|
+
|
|
105
|
+
expect(asts).toEqual([AST.literal("users"), AST.slash(), AST.literal("profile")]);
|
|
106
|
+
expect(rest).toEqual("");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("parses empty string", () => {
|
|
110
|
+
const [asts, rest] = Path.parseWithRest("");
|
|
111
|
+
|
|
112
|
+
expect(asts).toEqual([]);
|
|
113
|
+
expect(rest).toEqual("");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("handles trailing slashes", () => {
|
|
117
|
+
const [asts, rest] = Path.parseWithRest("/users/");
|
|
118
|
+
|
|
119
|
+
expect(asts).toEqual([AST.literal("users")]);
|
|
120
|
+
expect(rest).toEqual("/");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("parses single query param", () => {
|
|
124
|
+
const [asts, rest] = Path.parseWithRest("?foo=bar");
|
|
125
|
+
|
|
126
|
+
expect(asts).toEqual([AST.queryParams([AST.queryParam("foo", AST.literal("bar"))])]);
|
|
127
|
+
expect(rest).toEqual("");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("parses multiple query params", () => {
|
|
131
|
+
const [asts, rest] = Path.parseWithRest("?foo=bar&baz=qux");
|
|
132
|
+
|
|
133
|
+
expect(asts).toEqual([
|
|
134
|
+
AST.queryParams([
|
|
135
|
+
AST.queryParam("foo", AST.literal("bar")),
|
|
136
|
+
AST.queryParam("baz", AST.literal("qux")),
|
|
137
|
+
]),
|
|
138
|
+
]);
|
|
139
|
+
expect(rest).toEqual("");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("parses path with query params containing parameters", () => {
|
|
143
|
+
const [asts, rest] = Path.parseWithRest("/users?id=:id&name=:name");
|
|
144
|
+
|
|
145
|
+
expect(asts).toEqual([
|
|
146
|
+
AST.literal("users"),
|
|
147
|
+
AST.queryParams([
|
|
148
|
+
AST.queryParam("id", AST.parameter("id")),
|
|
149
|
+
AST.queryParam("name", AST.parameter("name")),
|
|
150
|
+
]),
|
|
151
|
+
]);
|
|
152
|
+
expect(rest).toEqual("");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("parse", () => {
|
|
157
|
+
it("accepts a root path", () => {
|
|
158
|
+
expect(Path.parse("/")).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("throws when unparsed rest contains non-slash characters", () => {
|
|
162
|
+
expect(() => Path.parse("/:")).toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("parses simple path", () => {
|
|
166
|
+
expect(Path.parse("/users")).toEqual([AST.literal("users")]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("parses path with parameter", () => {
|
|
170
|
+
expect(Path.parse("/users/:id")).toEqual([
|
|
171
|
+
AST.literal("users"),
|
|
172
|
+
AST.slash(),
|
|
173
|
+
AST.parameter("id"),
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("parses path with wildcard", () => {
|
|
178
|
+
expect(Path.parse("/files/*")).toEqual([AST.literal("files"), AST.slash(), AST.wildcard()]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("accepts trailing slashes", () => {
|
|
182
|
+
expect(Path.parse("/users/")).toEqual([AST.literal("users")]);
|
|
183
|
+
expect(Path.parse("/users///")).toEqual([AST.literal("users")]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("throws on invalid parameter syntax", () => {
|
|
187
|
+
expect(() => Path.parse("/users/:")).toThrow();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("throws on unclosed regex", () => {
|
|
191
|
+
expect(() => Path.parse("/:id(abc")).toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("join", () => {
|
|
196
|
+
it("joins literal ASTs", () => {
|
|
197
|
+
expect(Path.join([AST.literal("users")])).toEqual("/users");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("joins literal with slash and parameter", () => {
|
|
201
|
+
expect(Path.join([AST.literal("users"), AST.slash(), AST.parameter("id")])).toEqual(
|
|
202
|
+
"/users/:id",
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("joins with wildcard", () => {
|
|
207
|
+
expect(Path.join([AST.literal("files"), AST.slash(), AST.wildcard()])).toEqual("/files/*");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("joins with optional parameter", () => {
|
|
211
|
+
const ast = AST.parameter("id", true);
|
|
212
|
+
// Note: formatAst doesn't include the optional marker in output
|
|
213
|
+
expect(Path.join([ast])).toEqual("/:id");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("joins with query params", () => {
|
|
217
|
+
expect(
|
|
218
|
+
Path.join([
|
|
219
|
+
AST.literal("search"),
|
|
220
|
+
AST.queryParams([
|
|
221
|
+
AST.queryParam("term", AST.parameter("term")),
|
|
222
|
+
AST.queryParam("limit", AST.literal("10")),
|
|
223
|
+
]),
|
|
224
|
+
]),
|
|
225
|
+
).toEqual("/search?term=:term&limit=10");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("joins empty array", () => {
|
|
229
|
+
expect(Path.join([])).toEqual("/");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("joins multiple slashes", () => {
|
|
233
|
+
expect(Path.join([AST.slash(), AST.literal("a"), AST.slash(), AST.slash()])).toEqual("//a//");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("getSchemaFields", () => {
|
|
238
|
+
it("extracts parameter fields", () => {
|
|
239
|
+
const result = Path.getSchemaFields([AST.parameter("id"), AST.parameter("name")]);
|
|
240
|
+
|
|
241
|
+
expect(result.requiredFields.map(([name]) => name)).toEqual(["id", "name"]);
|
|
242
|
+
expect(result.optionalFields).toEqual([]);
|
|
243
|
+
expect(result.queryParams).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("extracts optional parameter fields", () => {
|
|
247
|
+
const result = Path.getSchemaFields([AST.parameter("id", true)]);
|
|
248
|
+
|
|
249
|
+
expect(result.requiredFields).toEqual([]);
|
|
250
|
+
expect(result.optionalFields.length).toEqual(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("extracts wildcard as required field", () => {
|
|
254
|
+
const result = Path.getSchemaFields([AST.wildcard()]);
|
|
255
|
+
|
|
256
|
+
expect(result.requiredFields.map(([name]) => name)).toEqual(["*"]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("extracts query params", () => {
|
|
260
|
+
const result = Path.getSchemaFields([
|
|
261
|
+
AST.queryParams([AST.queryParam("foo", AST.parameter("bar"))]),
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
expect(result.queryParams.length).toEqual(1);
|
|
265
|
+
expect(result.queryParams[0][0]).toEqual("foo");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("ignores literals and slashes", () => {
|
|
269
|
+
const result = Path.getSchemaFields([AST.literal("users"), AST.slash()]);
|
|
270
|
+
|
|
271
|
+
expect(result.requiredFields).toEqual([]);
|
|
272
|
+
expect(result.optionalFields).toEqual([]);
|
|
273
|
+
expect(result.queryParams).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("handles parameter with regex", () => {
|
|
277
|
+
const result = Path.getSchemaFields([AST.parameter("id", undefined, "\\d+")]);
|
|
278
|
+
|
|
279
|
+
expect(result.requiredFields.length).toEqual(1);
|
|
280
|
+
expect(result.requiredFields[0][0]).toEqual("id");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("getSchemas", () => {
|
|
285
|
+
it("creates path schema for parameters", () => {
|
|
286
|
+
const result = Path.getSchemas([AST.parameter("id")]);
|
|
287
|
+
|
|
288
|
+
expect(result.pathSchema).toBeDefined();
|
|
289
|
+
expect(result.querySchema).toBeDefined();
|
|
290
|
+
expect(result.paramsSchema).toBeDefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("creates schemas for path with query params", () => {
|
|
294
|
+
const result = Path.getSchemas([
|
|
295
|
+
AST.literal("search"),
|
|
296
|
+
AST.queryParams([AST.queryParam("term", AST.parameter("term"))]),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
expect(result.pathSchema).toBeDefined();
|
|
300
|
+
expect(result.querySchema).toBeDefined();
|
|
301
|
+
expect(result.paramsSchema).toBeDefined();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("creates schemas for wildcard", () => {
|
|
305
|
+
const result = Path.getSchemas([AST.wildcard()]);
|
|
306
|
+
|
|
307
|
+
expect(result.pathSchema).toBeDefined();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("creates schemas for empty path", () => {
|
|
311
|
+
const result = Path.getSchemas([]);
|
|
312
|
+
|
|
313
|
+
expect(result.pathSchema).toBeDefined();
|
|
314
|
+
expect(result.querySchema).toBeDefined();
|
|
315
|
+
expect(result.paramsSchema).toBeDefined();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|