@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.
Files changed (87) hide show
  1. package/README.md +129 -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 +209 -0
  9. package/dist/Matcher.d.ts.map +1 -0
  10. package/dist/Matcher.js +633 -0
  11. package/dist/Parser.d.ts +92 -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 +32 -73
  30. package/src/AST.ts +166 -0
  31. package/src/CurrentRoute.ts +30 -331
  32. package/src/Matcher.test.ts +496 -0
  33. package/src/Matcher.ts +1375 -325
  34. package/src/Parser.ts +276 -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 +33 -0
  40. package/src/Uri.ts +214 -0
  41. package/src/index.ts +4 -28
  42. package/CurrentRoute/package.json +0 -6
  43. package/LICENSE +0 -21
  44. package/MatchInput/package.json +0 -6
  45. package/Matcher/package.json +0 -6
  46. package/RouteGuard/package.json +0 -6
  47. package/RouteMatch/package.json +0 -6
  48. package/dist/cjs/CurrentRoute.js +0 -170
  49. package/dist/cjs/CurrentRoute.js.map +0 -1
  50. package/dist/cjs/MatchInput.js +0 -96
  51. package/dist/cjs/MatchInput.js.map +0 -1
  52. package/dist/cjs/Matcher.js +0 -138
  53. package/dist/cjs/Matcher.js.map +0 -1
  54. package/dist/cjs/RouteGuard.js +0 -78
  55. package/dist/cjs/RouteGuard.js.map +0 -1
  56. package/dist/cjs/RouteMatch.js +0 -49
  57. package/dist/cjs/RouteMatch.js.map +0 -1
  58. package/dist/cjs/index.js +0 -53
  59. package/dist/cjs/index.js.map +0 -1
  60. package/dist/dts/CurrentRoute.d.ts +0 -94
  61. package/dist/dts/CurrentRoute.d.ts.map +0 -1
  62. package/dist/dts/MatchInput.d.ts +0 -143
  63. package/dist/dts/MatchInput.d.ts.map +0 -1
  64. package/dist/dts/Matcher.d.ts +0 -121
  65. package/dist/dts/Matcher.d.ts.map +0 -1
  66. package/dist/dts/RouteGuard.d.ts +0 -94
  67. package/dist/dts/RouteGuard.d.ts.map +0 -1
  68. package/dist/dts/RouteMatch.d.ts +0 -50
  69. package/dist/dts/RouteMatch.d.ts.map +0 -1
  70. package/dist/dts/index.d.ts +0 -24
  71. package/dist/dts/index.d.ts.map +0 -1
  72. package/dist/esm/CurrentRoute.js +0 -152
  73. package/dist/esm/CurrentRoute.js.map +0 -1
  74. package/dist/esm/MatchInput.js +0 -79
  75. package/dist/esm/MatchInput.js.map +0 -1
  76. package/dist/esm/Matcher.js +0 -130
  77. package/dist/esm/Matcher.js.map +0 -1
  78. package/dist/esm/RouteGuard.js +0 -57
  79. package/dist/esm/RouteGuard.js.map +0 -1
  80. package/dist/esm/RouteMatch.js +0 -29
  81. package/dist/esm/RouteMatch.js.map +0 -1
  82. package/dist/esm/index.js +0 -24
  83. package/dist/esm/index.js.map +0 -1
  84. package/dist/esm/package.json +0 -4
  85. package/src/MatchInput.ts +0 -303
  86. package/src/RouteGuard.ts +0 -217
  87. 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
+ }
@@ -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
+ });