@typed/router 0.31.0 → 1.0.0-beta.0

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.
Files changed (88) hide show
  1. package/README.md +111 -2
  2. package/dist/AST.d.ts +96 -0
  3. package/dist/AST.d.ts.map +1 -0
  4. package/dist/AST.js +32 -0
  5. package/dist/CurrentRoute.d.ts +18 -0
  6. package/dist/CurrentRoute.d.ts.map +1 -0
  7. package/dist/CurrentRoute.js +18 -0
  8. package/dist/Matcher.d.ts +191 -0
  9. package/dist/Matcher.d.ts.map +1 -0
  10. package/dist/Matcher.js +597 -0
  11. package/dist/Parser.d.ts +96 -0
  12. package/dist/Parser.d.ts.map +1 -0
  13. package/dist/Parser.js +1 -0
  14. package/dist/Path.d.ts +216 -0
  15. package/dist/Path.d.ts.map +1 -0
  16. package/dist/Path.js +248 -0
  17. package/dist/Route.d.ts +57 -0
  18. package/dist/Route.d.ts.map +1 -0
  19. package/dist/Route.js +151 -0
  20. package/dist/Router.d.ts +9 -0
  21. package/dist/Router.d.ts.map +1 -0
  22. package/dist/Router.js +8 -0
  23. package/dist/Uri.d.ts +115 -0
  24. package/dist/Uri.d.ts.map +1 -0
  25. package/dist/Uri.js +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +4 -0
  29. package/package.json +25 -69
  30. package/src/AST.ts +166 -0
  31. package/src/CurrentRoute.ts +30 -331
  32. package/src/Matcher.test.ts +476 -0
  33. package/src/Matcher.ts +1269 -328
  34. package/src/Parser.ts +282 -0
  35. package/src/Path.test.ts +318 -0
  36. package/src/Path.ts +691 -0
  37. package/src/Route.test.ts +268 -0
  38. package/src/Route.ts +316 -0
  39. package/src/Router.ts +31 -0
  40. package/src/Uri.ts +214 -0
  41. package/src/index.ts +4 -28
  42. package/tsconfig.json +6 -0
  43. package/CurrentRoute/package.json +0 -6
  44. package/LICENSE +0 -21
  45. package/MatchInput/package.json +0 -6
  46. package/Matcher/package.json +0 -6
  47. package/RouteGuard/package.json +0 -6
  48. package/RouteMatch/package.json +0 -6
  49. package/dist/cjs/CurrentRoute.js +0 -170
  50. package/dist/cjs/CurrentRoute.js.map +0 -1
  51. package/dist/cjs/MatchInput.js +0 -96
  52. package/dist/cjs/MatchInput.js.map +0 -1
  53. package/dist/cjs/Matcher.js +0 -138
  54. package/dist/cjs/Matcher.js.map +0 -1
  55. package/dist/cjs/RouteGuard.js +0 -78
  56. package/dist/cjs/RouteGuard.js.map +0 -1
  57. package/dist/cjs/RouteMatch.js +0 -49
  58. package/dist/cjs/RouteMatch.js.map +0 -1
  59. package/dist/cjs/index.js +0 -53
  60. package/dist/cjs/index.js.map +0 -1
  61. package/dist/dts/CurrentRoute.d.ts +0 -94
  62. package/dist/dts/CurrentRoute.d.ts.map +0 -1
  63. package/dist/dts/MatchInput.d.ts +0 -135
  64. package/dist/dts/MatchInput.d.ts.map +0 -1
  65. package/dist/dts/Matcher.d.ts +0 -121
  66. package/dist/dts/Matcher.d.ts.map +0 -1
  67. package/dist/dts/RouteGuard.d.ts +0 -94
  68. package/dist/dts/RouteGuard.d.ts.map +0 -1
  69. package/dist/dts/RouteMatch.d.ts +0 -50
  70. package/dist/dts/RouteMatch.d.ts.map +0 -1
  71. package/dist/dts/index.d.ts +0 -24
  72. package/dist/dts/index.d.ts.map +0 -1
  73. package/dist/esm/CurrentRoute.js +0 -152
  74. package/dist/esm/CurrentRoute.js.map +0 -1
  75. package/dist/esm/MatchInput.js +0 -79
  76. package/dist/esm/MatchInput.js.map +0 -1
  77. package/dist/esm/Matcher.js +0 -130
  78. package/dist/esm/Matcher.js.map +0 -1
  79. package/dist/esm/RouteGuard.js +0 -57
  80. package/dist/esm/RouteGuard.js.map +0 -1
  81. package/dist/esm/RouteMatch.js +0 -29
  82. package/dist/esm/RouteMatch.js.map +0 -1
  83. package/dist/esm/index.js +0 -24
  84. package/dist/esm/index.js.map +0 -1
  85. package/dist/esm/package.json +0 -4
  86. package/src/MatchInput.ts +0 -282
  87. package/src/RouteGuard.ts +0 -217
  88. package/src/RouteMatch.ts +0 -104
package/src/Path.ts ADDED
@@ -0,0 +1,691 @@
1
+ import type { Arg0, TypeLambda1 } from "hkt-core";
2
+
3
+ import * as Schema from "effect/Schema";
4
+ import type { PathAst } from "./AST.js";
5
+ import * as AST from "./AST.js";
6
+ import type { Parser } from "./Parser.js";
7
+
8
+ type PathStopChar = "/" | ":" | "*" | "?";
9
+ type QueryValueStopChar = "&";
10
+
11
+ type TakeWhileNotInternal<
12
+ Input extends string,
13
+ Stop extends string,
14
+ Acc extends string = "",
15
+ > = Input extends `${infer Head}${infer Tail}`
16
+ ? Head extends Stop
17
+ ? readonly [Acc, Input]
18
+ : TakeWhileNotInternal<Tail, Stop, `${Acc}${Head}`>
19
+ : readonly [Acc, Input];
20
+
21
+ type TakeWhileNot1Internal<Input extends string, Stop extends string> =
22
+ TakeWhileNotInternal<Input, Stop> extends readonly [
23
+ infer Taken extends string,
24
+ infer Rest extends string,
25
+ ]
26
+ ? Taken extends ""
27
+ ? never
28
+ : readonly [Taken, Rest]
29
+ : never;
30
+
31
+ interface TakeWhileNot1<Stop extends string> extends Parser<string> {
32
+ readonly return: Arg0<this> extends infer Input extends string
33
+ ? TakeWhileNot1Internal<Input, Stop>
34
+ : never;
35
+ }
36
+
37
+ interface Second extends TypeLambda1 {
38
+ readonly return: Arg0<this> extends readonly [infer _A, infer B] ? B : never;
39
+ }
40
+
41
+ interface RegexBetweenParens extends TypeLambda1 {
42
+ readonly return: Arg0<this> extends readonly ["(", readonly [infer Regex extends string, ")"]]
43
+ ? Regex
44
+ : never;
45
+ }
46
+
47
+ type RegexParser = Parser.Map<
48
+ Parser.Zip<Parser.Char<"(">, Parser.Zip<TakeWhileNot1<")">, Parser.Char<")">>>,
49
+ RegexBetweenParens
50
+ >;
51
+
52
+ type OptionalRegexParser = Parser.Optional<RegexParser>;
53
+ type OptionalQuestionMarkParser = Parser.Optional<Parser.Char<"?">>;
54
+
55
+ type ParameterNameParser = Parser.Zip<Parser.Char<":">, Parser.TakeWhile1<Parser.AlphaNumeric>>;
56
+
57
+ type ParameterPartsParser = Parser.Zip<
58
+ ParameterNameParser,
59
+ Parser.Zip<OptionalRegexParser, OptionalQuestionMarkParser>
60
+ >;
61
+
62
+ type ParameterAst<
63
+ Name extends string,
64
+ Regex extends string | undefined,
65
+ OptionalMark extends "?" | undefined,
66
+ > = [
67
+ { readonly type: "parameter"; readonly name: Name } & ([Regex] extends [string]
68
+ ? { regex: Regex }
69
+ : {}) &
70
+ ([OptionalMark] extends ["?"] ? { optional: true } : {}),
71
+ ] extends [infer Ast]
72
+ ? ToReadonlyRecord<Ast>
73
+ : never;
74
+
75
+ interface ToParameterAst extends TypeLambda1 {
76
+ readonly return: Arg0<this> extends readonly [
77
+ readonly [":", infer Name extends string],
78
+ readonly [infer Regex, infer OptionalMark],
79
+ ]
80
+ ? ParameterAst<
81
+ Name,
82
+ Regex extends string ? Regex : undefined,
83
+ OptionalMark extends "?" ? OptionalMark : undefined
84
+ >
85
+ : never;
86
+ }
87
+
88
+ type ParameterParser = Parser.Map<ParameterPartsParser, ToParameterAst>;
89
+
90
+ interface ToWildcardAst extends TypeLambda1 {
91
+ readonly return: { readonly type: "wildcard" };
92
+ }
93
+
94
+ type WildcardParser = Parser.Map<Parser.Char<"*">, ToWildcardAst>;
95
+
96
+ interface ToLiteralAst extends TypeLambda1 {
97
+ readonly return: Arg0<this> extends infer Value extends string
98
+ ? { readonly type: "literal"; readonly value: Value }
99
+ : never;
100
+ }
101
+
102
+ type PathLiteralParser = Parser.Map<TakeWhileNot1<PathStopChar>, ToLiteralAst>;
103
+
104
+ type QueryLiteralParser = Parser.Map<TakeWhileNot1<QueryValueStopChar>, ToLiteralAst>;
105
+
106
+ type QueryValueParser = Parser.OrElse<
107
+ ParameterParser,
108
+ Parser.OrElse<WildcardParser, QueryLiteralParser>
109
+ >;
110
+
111
+ interface ToQueryParamAst extends TypeLambda1 {
112
+ readonly return: Arg0<this> extends readonly [
113
+ infer Name extends string,
114
+ readonly ["=", infer Value extends PathAst],
115
+ ]
116
+ ? { readonly type: "query-param"; readonly name: Name; readonly value: Value }
117
+ : never;
118
+ }
119
+
120
+ type QueryParamParser = Parser.Map<
121
+ Parser.Zip<
122
+ Parser.TakeWhile1<Parser.AlphaNumeric>,
123
+ Parser.Zip<Parser.Char<"=">, QueryValueParser>
124
+ >,
125
+ ToQueryParamAst
126
+ >;
127
+
128
+ type QueryParamTailParser = Parser.Map<Parser.Zip<Parser.Char<"&">, QueryParamParser>, Second>;
129
+
130
+ interface PrependToTuple extends TypeLambda1 {
131
+ readonly return: Arg0<this> extends readonly [
132
+ infer Head,
133
+ infer Tail extends ReadonlyArray<unknown>,
134
+ ]
135
+ ? readonly [Head, ...Tail]
136
+ : never;
137
+ }
138
+
139
+ type QueryParamListParser = Parser.Map<
140
+ Parser.Zip<QueryParamParser, Parser.Many<QueryParamTailParser>>,
141
+ PrependToTuple
142
+ >;
143
+
144
+ interface ToQueryParamsAst extends TypeLambda1 {
145
+ readonly return: Arg0<this> extends readonly [
146
+ "?",
147
+ infer Params extends ReadonlyArray<PathAst.QueryParam>,
148
+ ]
149
+ ? {
150
+ readonly type: "query-params";
151
+ readonly value: Params;
152
+ }
153
+ : never;
154
+ }
155
+
156
+ type QueryParamsParser = Parser.Map<
157
+ Parser.Zip<Parser.Char<"?">, QueryParamListParser>,
158
+ ToQueryParamsAst
159
+ >;
160
+
161
+ type PathAtomParser = Parser.OrElse<
162
+ QueryParamsParser,
163
+ Parser.OrElse<ParameterParser, Parser.OrElse<WildcardParser, PathLiteralParser>>
164
+ >;
165
+
166
+ type SkipSlashesParser = Parser.Optional<Parser.Many1<Parser.Char<"/">>>;
167
+
168
+ export type PathParser = Parser.Map<Parser.Zip<SkipSlashesParser, PathAtomParser>, Second>;
169
+
170
+ type SlashAst = { readonly type: "slash" };
171
+
172
+ type PathChunk = readonly [PathAst] | readonly [SlashAst, PathAst];
173
+
174
+ interface ToPathChunk extends TypeLambda1 {
175
+ readonly return: Arg0<this> extends readonly [infer Slashes, infer Ast extends PathAst]
176
+ ? [Slashes] extends [undefined]
177
+ ? readonly [Ast]
178
+ : readonly [SlashAst, Ast]
179
+ : never;
180
+ }
181
+
182
+ type PathChunkParser = Parser.Map<Parser.Zip<SkipSlashesParser, PathAtomParser>, ToPathChunk>;
183
+
184
+ type FlattenChunks<
185
+ Chunks extends ReadonlyArray<PathChunk>,
186
+ Acc extends ReadonlyArray<PathAst> = readonly [],
187
+ > = Chunks extends readonly [
188
+ infer Head extends PathChunk,
189
+ ...infer Tail extends ReadonlyArray<PathChunk>,
190
+ ]
191
+ ? FlattenChunks<Tail, readonly [...Acc, ...Head]>
192
+ : Acc;
193
+
194
+ type CombineChunks<First, Chunks extends ReadonlyArray<PathChunk>> = [First] extends [PathAst]
195
+ ? readonly [First, ...FlattenChunks<Chunks>]
196
+ : FlattenChunks<Chunks>;
197
+
198
+ interface ToAsts extends TypeLambda1 {
199
+ readonly return: Arg0<this> extends readonly [
200
+ infer First,
201
+ infer Chunks extends ReadonlyArray<PathChunk>,
202
+ ]
203
+ ? CombineChunks<First, Chunks>
204
+ : never;
205
+ }
206
+
207
+ type ParseAstsResult<Input extends string> = Parser.Run<
208
+ Parser.Map<Parser.Zip<Parser.Optional<PathParser>, Parser.Many<PathChunkParser>>, ToAsts>,
209
+ Input
210
+ >;
211
+
212
+ type GetAsts<R> = [R] extends [never]
213
+ ? never
214
+ : R extends readonly [infer Asts extends ReadonlyArray<PathAst>, infer _Rest extends string]
215
+ ? Asts
216
+ : never;
217
+
218
+ export type ParseAsts<Input extends string> = GetAsts<ParseAstsResult<Input>>;
219
+
220
+ type ParamsOfAst<T> = T extends {
221
+ type: "parameter";
222
+ name: infer Name extends string;
223
+ optional: true;
224
+ }
225
+ ? { [K in Name]?: string }
226
+ : T extends { readonly type: "parameter"; readonly name: infer Name extends string }
227
+ ? { [K in Name]: string }
228
+ : T extends { readonly type: "wildcard" }
229
+ ? { "*": string }
230
+ : T extends {
231
+ readonly type: "query-params";
232
+ readonly value: infer Values extends ReadonlyArray<PathAst.QueryParam>;
233
+ }
234
+ ? ParamsOfQueryParams<Values>
235
+ : {};
236
+
237
+ type ParamsOfQueryParams<
238
+ T extends ReadonlyArray<PathAst.QueryParam>,
239
+ Acc = {},
240
+ > = T extends readonly [infer Head, ...infer Tail extends ReadonlyArray<PathAst.QueryParam>]
241
+ ? ParamsOfQueryParams<Tail, Acc & ParamsOfQueryParam<Head>>
242
+ : Acc;
243
+
244
+ type ParamsOfQueryParam<T> = T extends {
245
+ readonly type: "query-param";
246
+ readonly value: infer Value extends PathAst;
247
+ }
248
+ ? ParamsOfAst<Value>
249
+ : {};
250
+
251
+ type GetParams<T extends ReadonlyArray<PathAst>, Acc = {}> = T extends readonly [
252
+ infer Head,
253
+ ...infer Tail extends ReadonlyArray<PathAst>,
254
+ ]
255
+ ? GetParams<Tail, Acc & ParamsOfAst<Head>>
256
+ : Acc;
257
+
258
+ type PathParamsOfAst<T> = T extends {
259
+ type: "parameter";
260
+ name: infer Name extends string;
261
+ optional: true;
262
+ }
263
+ ? { [K in Name]?: string }
264
+ : T extends { readonly type: "parameter"; readonly name: infer Name extends string }
265
+ ? { [K in Name]: string }
266
+ : T extends { readonly type: "wildcard" }
267
+ ? { "*": string }
268
+ : {};
269
+
270
+ type QueryParamsOfAst<T> = T extends {
271
+ readonly type: "query-params";
272
+ readonly value: infer Values extends ReadonlyArray<PathAst.QueryParam>;
273
+ }
274
+ ? ParamsOfQueryParams<Values>
275
+ : {};
276
+
277
+ type GetPathParams<T extends ReadonlyArray<PathAst>, Acc = {}> = T extends readonly [
278
+ infer Head,
279
+ ...infer Tail extends ReadonlyArray<PathAst>,
280
+ ]
281
+ ? GetPathParams<Tail, Acc & PathParamsOfAst<Head>>
282
+ : Acc;
283
+
284
+ type GetQueryParams<T extends ReadonlyArray<PathAst>, Acc = {}> = T extends readonly [
285
+ infer Head,
286
+ ...infer Tail extends ReadonlyArray<PathAst>,
287
+ ]
288
+ ? GetQueryParams<Tail, Acc & QueryParamsOfAst<Head>>
289
+ : Acc;
290
+
291
+ type ToReadonlyRecord<T> = [T] extends [infer T2] ? { readonly [K in keyof T2]: T2[K] } : never;
292
+
293
+ export type PathParams<P extends string> =
294
+ ParseAsts<P> extends infer Asts
295
+ ? [Asts] extends [never]
296
+ ? never
297
+ : Asts extends ReadonlyArray<PathAst>
298
+ ? ToReadonlyRecord<GetPathParams<Asts>>
299
+ : never
300
+ : never;
301
+
302
+ export type QueryParams<P extends string> =
303
+ ParseAsts<P> extends infer Asts
304
+ ? [Asts] extends [never]
305
+ ? never
306
+ : Asts extends ReadonlyArray<PathAst>
307
+ ? ToReadonlyRecord<GetQueryParams<Asts>>
308
+ : never
309
+ : never;
310
+
311
+ export type Params<P extends string> =
312
+ ParseAsts<P> extends infer Asts
313
+ ? [Asts] extends [never]
314
+ ? never
315
+ : Asts extends ReadonlyArray<PathAst>
316
+ ? ToReadonlyRecord<GetParams<Asts>>
317
+ : never
318
+ : never;
319
+
320
+ export type RuntimeParseResult = readonly [asts: ReadonlyArray<PathAst>, rest: string];
321
+
322
+ export function parseWithRest(input: string): RuntimeParseResult {
323
+ let index = 0;
324
+ const asts: Array<PathAst> = [];
325
+
326
+ while (index < input.length) {
327
+ const start = index;
328
+ let hasSlash = false;
329
+ while (index < input.length && input[index] === "/") {
330
+ hasSlash = true;
331
+ index++;
332
+ }
333
+
334
+ const atom = parseAtom(input, index);
335
+ if (atom === undefined) {
336
+ index = start;
337
+ break;
338
+ }
339
+
340
+ if (hasSlash && asts.length > 0) {
341
+ asts.push(AST.slash());
342
+ }
343
+ asts.push(atom.ast);
344
+ index = atom.index;
345
+ }
346
+
347
+ return [asts, input.slice(index)];
348
+ }
349
+
350
+ export function parse<const P extends string>(input: P): ParseAsts<P> {
351
+ const [asts, rest] = parseWithRest(input);
352
+ for (let i = 0; i < rest.length; i++) {
353
+ if (rest[i] !== "/") {
354
+ const index = input.length - rest.length;
355
+ throw new Error(`Failed to parse path at index ${index}`);
356
+ }
357
+ }
358
+ return asts as ParseAsts<P>;
359
+ }
360
+
361
+ export type Join<Parts extends ReadonlyArray<PathAst>> = `/${StringJoin<
362
+ {
363
+ [K in keyof Parts]: FormatAst<Parts[K]>;
364
+ },
365
+ ""
366
+ >}`;
367
+
368
+ type StringJoin<
369
+ Input extends ReadonlyArray<string>,
370
+ R extends string = "",
371
+ > = Input extends readonly [infer A extends string, ...infer Rest extends ReadonlyArray<string>]
372
+ ? StringJoin<Rest, `${R}${A}`>
373
+ : R;
374
+
375
+ type FormatAst<T extends PathAst> = [T] extends [PathAst.Literal]
376
+ ? T["value"]
377
+ : [T] extends [PathAst.Parameter]
378
+ ? FormatParameterAst<T>
379
+ : [T] extends [PathAst.Wildcard]
380
+ ? `*`
381
+ : [T] extends [PathAst.Slash]
382
+ ? `/`
383
+ : [T] extends [PathAst.QueryParams]
384
+ ? FormatQueryParamsAst<T["value"]>
385
+ : never;
386
+
387
+ type FormatParameterAst<T extends PathAst.Parameter> =
388
+ `:${T["name"]}${T["optional"] extends true ? "?" : ""}`;
389
+ type FormatQueryParamsAst<
390
+ T extends ReadonlyArray<PathAst.QueryParam>,
391
+ R extends string = "?",
392
+ > = T extends readonly [
393
+ infer Head extends PathAst.QueryParam,
394
+ ...infer Tail extends ReadonlyArray<PathAst.QueryParam>,
395
+ ]
396
+ ? FormatQueryParamsAst<Tail, `${R}${R extends "?" ? "" : "&"}${FormatQueryParamAst<Head>}`>
397
+ : R;
398
+
399
+ type FormatQueryParamAst<T extends PathAst.QueryParam> = `${T["name"]}=${FormatAst<T["value"]>}`;
400
+
401
+ export function join<const Parts extends ReadonlyArray<PathAst>>(asts: Parts): Join<Parts> {
402
+ return `/${asts.map(formatAst).join("")}` as Join<Parts>;
403
+ }
404
+
405
+ function formatAst(ast: PathAst): string {
406
+ switch (ast.type) {
407
+ case "literal":
408
+ return ast.value;
409
+ case "parameter":
410
+ return `:${ast.name}`;
411
+ case "wildcard":
412
+ return "*";
413
+ case "slash":
414
+ return "/";
415
+ case "query-params":
416
+ return `?${ast.value.map(getQueryParamAst).join("&")}`;
417
+ }
418
+ }
419
+
420
+ function getQueryParamAst(ast: PathAst.QueryParam): string {
421
+ return `${ast.name}=${formatAst(ast.value)}`;
422
+ }
423
+
424
+ type Atom = {
425
+ readonly ast: PathAst;
426
+ readonly index: number;
427
+ };
428
+
429
+ function parseAtom(input: string, index: number): Atom | undefined {
430
+ const char = input[index];
431
+
432
+ if (char === undefined) {
433
+ return undefined;
434
+ }
435
+
436
+ if (char === "?") {
437
+ return parseQueryParams(input, index);
438
+ }
439
+
440
+ if (char === ":") {
441
+ return parseParameter(input, index);
442
+ }
443
+
444
+ if (char === "*") {
445
+ return { ast: AST.wildcard(), index: index + 1 };
446
+ }
447
+
448
+ return parsePathLiteral(input, index);
449
+ }
450
+
451
+ function parseParameter(input: string, index: number): Atom | undefined {
452
+ if (input[index] !== ":") {
453
+ return undefined;
454
+ }
455
+
456
+ let i = index + 1;
457
+ let name = "";
458
+ while (i < input.length && isAlphaNumeric(input[i])) {
459
+ name += input[i];
460
+ i++;
461
+ }
462
+
463
+ if (name.length === 0) {
464
+ return undefined;
465
+ }
466
+
467
+ let regex: string | undefined = undefined;
468
+ if (input[i] === "(") {
469
+ i++;
470
+ const start = i;
471
+ while (i < input.length && input[i] !== ")") {
472
+ i++;
473
+ }
474
+ if (i >= input.length) {
475
+ return undefined;
476
+ }
477
+ if (i === start) {
478
+ return undefined;
479
+ }
480
+ regex = input.slice(start, i);
481
+ i++;
482
+ }
483
+
484
+ let optional: true | undefined = undefined;
485
+ if (input[i] === "?") {
486
+ optional = true;
487
+ i++;
488
+ }
489
+
490
+ return { ast: AST.parameter(name, optional, regex), index: i };
491
+ }
492
+
493
+ function parsePathLiteral(input: string, index: number): Atom | undefined {
494
+ const char = input[index];
495
+ if (char === undefined || isPathStopChar(char)) {
496
+ return undefined;
497
+ }
498
+
499
+ let i = index;
500
+ while (i < input.length && !isPathStopChar(input[i])) {
501
+ i++;
502
+ }
503
+
504
+ return { ast: AST.literal(input.slice(index, i)), index: i };
505
+ }
506
+
507
+ function parseQueryParams(input: string, index: number): Atom | undefined {
508
+ if (input[index] !== "?") {
509
+ return undefined;
510
+ }
511
+
512
+ const first = parseQueryParam(input, index + 1);
513
+ if (first === undefined) {
514
+ return undefined;
515
+ }
516
+
517
+ let i = first.index;
518
+ const params: Array<PathAst.QueryParam> = [first.ast];
519
+
520
+ while (i < input.length && input[i] === "&") {
521
+ const start = i;
522
+ const next = parseQueryParam(input, i + 1);
523
+ if (next === undefined) {
524
+ i = start;
525
+ break;
526
+ }
527
+ params.push(next.ast);
528
+ i = next.index;
529
+ }
530
+
531
+ return { ast: AST.queryParams(params), index: i };
532
+ }
533
+
534
+ type QueryParamResult = {
535
+ readonly ast: PathAst.QueryParam;
536
+ readonly index: number;
537
+ };
538
+
539
+ function parseQueryParam(input: string, index: number): QueryParamResult | undefined {
540
+ let i = index;
541
+ let name = "";
542
+ while (i < input.length && isAlphaNumeric(input[i])) {
543
+ name += input[i];
544
+ i++;
545
+ }
546
+
547
+ if (name.length === 0) {
548
+ return undefined;
549
+ }
550
+
551
+ if (input[i] !== "=") {
552
+ return undefined;
553
+ }
554
+ i++;
555
+
556
+ const value = parseQueryValue(input, i);
557
+ if (value === undefined) {
558
+ return undefined;
559
+ }
560
+
561
+ return { ast: AST.queryParam(name, value.ast), index: value.index };
562
+ }
563
+
564
+ function parseQueryValue(input: string, index: number): Atom | undefined {
565
+ const char = input[index];
566
+
567
+ if (char === undefined) {
568
+ return undefined;
569
+ }
570
+
571
+ if (char === ":") {
572
+ return parseParameter(input, index);
573
+ }
574
+
575
+ if (char === "*") {
576
+ return { ast: AST.wildcard(), index: index + 1 };
577
+ }
578
+
579
+ let i = index;
580
+ while (i < input.length && input[i] !== "&") {
581
+ i++;
582
+ }
583
+
584
+ if (i === index) {
585
+ return undefined;
586
+ }
587
+
588
+ return { ast: AST.literal(input.slice(index, i)), index: i };
589
+ }
590
+
591
+ function isPathStopChar(char: string): boolean {
592
+ return char === "/" || char === ":" || char === "*" || char === "?";
593
+ }
594
+
595
+ function isAlphaNumeric(char: string): boolean {
596
+ return (
597
+ (char >= "0" && char <= "9") || (char >= "a" && char <= "z") || (char >= "A" && char <= "Z")
598
+ );
599
+ }
600
+
601
+ /**
602
+ * @internal
603
+ */
604
+ export function getSchemaFields<const Parts extends ReadonlyArray<PathAst>>(parts: Parts) {
605
+ const requiredFields: Array<[string, Schema.Top]> = [];
606
+ const optionalFields: Array<[Schema.Record.Key, Schema.Top]> = [];
607
+ const queryParams: Array<
608
+ [
609
+ string,
610
+ {
611
+ readonly requiredFields: Array<[string, Schema.Top]>;
612
+ readonly optionalFields: Array<[Schema.Record.Key, Schema.Top]>;
613
+ },
614
+ ]
615
+ > = [];
616
+
617
+ function addParameter(param: PathAst.Parameter) {
618
+ const base = param.regex
619
+ ? Schema.String.pipe(Schema.check(Schema.isPattern(new RegExp(param.regex))))
620
+ : Schema.String;
621
+
622
+ if (param.optional) {
623
+ optionalFields.push([Schema.optionalKey(Schema.Literal(param.name)), Schema.optional(base)]);
624
+ } else {
625
+ requiredFields.push([param.name, base]);
626
+ }
627
+ }
628
+
629
+ function addQueryParams(params: ReadonlyArray<PathAst.QueryParam>) {
630
+ for (const param of params) {
631
+ const {
632
+ optionalFields,
633
+ queryParams: _queryParams,
634
+ requiredFields,
635
+ } = getSchemaFields([param.value]);
636
+ queryParams.push([param.name, { optionalFields, requiredFields }]);
637
+ // eslint-disable-next-line no-restricted-syntax
638
+ queryParams.push(..._queryParams);
639
+ }
640
+ }
641
+
642
+ function addParts(parts: ReadonlyArray<PathAst>) {
643
+ for (const part of parts) {
644
+ if (part.type === "parameter") {
645
+ addParameter(part);
646
+ } else if (part.type === "wildcard") {
647
+ requiredFields.push(["*", Schema.String]);
648
+ } else if (part.type === "query-params") {
649
+ addQueryParams(part.value);
650
+ }
651
+ }
652
+ }
653
+
654
+ addParts(parts);
655
+
656
+ return {
657
+ requiredFields,
658
+ optionalFields,
659
+ queryParams,
660
+ };
661
+ }
662
+
663
+ export function getSchemas<const Parts extends ReadonlyArray<PathAst>>(parts: Parts) {
664
+ const { optionalFields, queryParams, requiredFields } = getSchemaFields(parts);
665
+ const pathFields = Object.fromEntries(requiredFields);
666
+ const queryFields = Object.fromEntries(
667
+ queryParams.map(([name, { optionalFields, requiredFields }]) => [
668
+ name,
669
+ Schema.StructWithRest(
670
+ Schema.Struct(Object.fromEntries(requiredFields)),
671
+ optionalFields.map(([key, value]) => Schema.Record(key, value)),
672
+ ),
673
+ ]),
674
+ );
675
+
676
+ const pathSchema = Schema.StructWithRest(
677
+ Schema.Struct(pathFields),
678
+ optionalFields.map(([key, value]) => Schema.Record(key, value)),
679
+ );
680
+ const querySchema = Schema.Struct(queryFields);
681
+ const paramsSchema = Schema.StructWithRest(
682
+ Schema.Struct({ ...pathFields, ...queryFields }),
683
+ optionalFields.map(([key, value]) => Schema.Record(key, value)),
684
+ );
685
+
686
+ return {
687
+ pathSchema,
688
+ querySchema,
689
+ paramsSchema,
690
+ } as const;
691
+ }