@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/Parser.ts ADDED
@@ -0,0 +1,282 @@
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<
34
+ [input: string],
35
+ Result<C, string> | never
36
+ > {
37
+ readonly return: Arg0<this> extends `${C}${infer Rest}` ? readonly [C, Rest] : never;
38
+ }
39
+
40
+ export interface String<S extends string> extends TypeLambda<
41
+ [input: string],
42
+ Result<S, string> | never
43
+ > {
44
+ readonly return: Arg0<this> extends `${S}${infer Rest}` ? readonly [S, Rest] : never;
45
+ }
46
+
47
+ export type LowercaseAlphabet =
48
+ | "a"
49
+ | "b"
50
+ | "c"
51
+ | "d"
52
+ | "e"
53
+ | "f"
54
+ | "g"
55
+ | "h"
56
+ | "i"
57
+ | "j"
58
+ | "k"
59
+ | "l"
60
+ | "m"
61
+ | "n"
62
+ | "o"
63
+ | "p"
64
+ | "q"
65
+ | "r"
66
+ | "s"
67
+ | "t"
68
+ | "u"
69
+ | "v"
70
+ | "w"
71
+ | "x"
72
+ | "y"
73
+ | "z";
74
+
75
+ export type UppercaseAlphabet =
76
+ | "A"
77
+ | "B"
78
+ | "C"
79
+ | "D"
80
+ | "E"
81
+ | "F"
82
+ | "G"
83
+ | "H"
84
+ | "I"
85
+ | "J"
86
+ | "K"
87
+ | "L"
88
+ | "M"
89
+ | "N"
90
+ | "O"
91
+ | "P"
92
+ | "Q"
93
+ | "R"
94
+ | "S"
95
+ | "T"
96
+ | "U"
97
+ | "V"
98
+ | "W"
99
+ | "X"
100
+ | "Y"
101
+ | "Z";
102
+
103
+ export type Alphabet = LowercaseAlphabet | UppercaseAlphabet;
104
+
105
+ export type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
106
+
107
+ export type AlphaNumeric = Alphabet | Digit;
108
+
109
+ type TakeWhileInternal<
110
+ Input extends string,
111
+ Allowed extends string,
112
+ Acc extends string = "",
113
+ > = Input extends `${infer Head}${infer Tail}`
114
+ ? Head extends Allowed
115
+ ? TakeWhileInternal<Tail, Allowed, `${Acc}${Head}`>
116
+ : readonly [Acc, Input]
117
+ : readonly [Acc, Input];
118
+
119
+ type TakeWhile1Internal<Input extends string, Allowed extends string> =
120
+ TakeWhileInternal<Input, Allowed> extends readonly [
121
+ infer Taken extends string,
122
+ infer Rest extends string,
123
+ ]
124
+ ? Taken extends ""
125
+ ? never
126
+ : readonly [Taken, Rest]
127
+ : never;
128
+
129
+ export interface TakeWhile<Allowed extends string> extends TypeLambda<
130
+ [input: string],
131
+ Result<string, string>
132
+ > {
133
+ readonly return: Arg0<this> extends infer Input extends string
134
+ ? TakeWhileInternal<Input, Allowed>
135
+ : never;
136
+ }
137
+
138
+ export interface TakeWhile1<Allowed extends string> extends TypeLambda<
139
+ [input: string],
140
+ Result<string, string> | never
141
+ > {
142
+ readonly return: Arg0<this> extends infer Input extends string
143
+ ? TakeWhile1Internal<Input, Allowed>
144
+ : never;
145
+ }
146
+
147
+ export interface Map<P extends Any, F extends TypeLambda1> extends Parser<unknown> {
148
+ readonly return: Arg0<this> extends infer Input extends string
149
+ ? Pipe<Input, P> extends infer R
150
+ ? [R] extends [never]
151
+ ? never
152
+ : R extends readonly [infer Value, infer Rest extends string]
153
+ ? readonly [Pipe<Value, F>, Rest]
154
+ : never
155
+ : never
156
+ : never;
157
+ }
158
+
159
+ export interface FlatMap<P extends Any, F extends TypeLambda1> extends Parser<unknown> {
160
+ readonly return: Arg0<this> extends infer Input extends string
161
+ ? Pipe<Input, P> extends infer R
162
+ ? [R] extends [never]
163
+ ? never
164
+ : R extends readonly [infer Value, infer Rest extends string]
165
+ ? Pipe<Value, F> extends infer Next
166
+ ? [Next] extends [never]
167
+ ? never
168
+ : Next extends Any
169
+ ? Pipe<Rest, Next>
170
+ : never
171
+ : never
172
+ : never
173
+ : never
174
+ : never;
175
+ }
176
+
177
+ export interface Zip<P extends Any, Q extends Any> extends Parser<unknown> {
178
+ readonly return: Arg0<this> extends infer Input extends string
179
+ ? Pipe<Input, P> extends infer R1
180
+ ? [R1] extends [never]
181
+ ? never
182
+ : R1 extends readonly [infer Value1, infer Rest1 extends string]
183
+ ? Pipe<Rest1, Q> extends infer R2
184
+ ? [R2] extends [never]
185
+ ? never
186
+ : R2 extends readonly [infer Value2, infer Rest2 extends string]
187
+ ? readonly [readonly [Value1, Value2], Rest2]
188
+ : never
189
+ : never
190
+ : never
191
+ : never
192
+ : never;
193
+ }
194
+
195
+ export interface OrElse<P extends Any, Q extends Any> extends Parser<unknown> {
196
+ readonly return: Arg0<this> extends infer Input extends string
197
+ ? Pipe<Input, P> extends infer R
198
+ ? [R] extends [never]
199
+ ? Pipe<Input, Q>
200
+ : R extends readonly [infer Value, infer Rest extends string]
201
+ ? readonly [Value, Rest]
202
+ : never
203
+ : never
204
+ : never;
205
+ }
206
+
207
+ export interface Optional<P extends Any> extends Parser<unknown> {
208
+ readonly return: Arg0<this> extends infer Input extends string
209
+ ? Pipe<Input, P> extends infer R
210
+ ? [R] extends [never]
211
+ ? readonly [undefined, Input]
212
+ : R extends readonly [infer Value, infer Rest extends string]
213
+ ? readonly [Value, Rest]
214
+ : never
215
+ : never
216
+ : never;
217
+ }
218
+
219
+ type ManyInternal<
220
+ P extends Any,
221
+ Input extends string,
222
+ Acc extends ReadonlyArray<unknown> = readonly [],
223
+ > =
224
+ Pipe<Input, P> extends infer R
225
+ ? [R] extends [never]
226
+ ? readonly [Acc, Input]
227
+ : R extends readonly [infer Value, infer Rest extends string]
228
+ ? IsNoProgress<Input, Rest> extends true
229
+ ? never
230
+ : ManyInternal<P, Rest, readonly [...Acc, Value]>
231
+ : never
232
+ : never;
233
+
234
+ type Many1Internal<P extends Any, Input extends string> =
235
+ Pipe<Input, P> extends infer R
236
+ ? [R] extends [never]
237
+ ? never
238
+ : R extends readonly [infer Value, infer Rest extends string]
239
+ ? IsNoProgress<Input, Rest> extends true
240
+ ? never
241
+ : ManyInternal<P, Rest, readonly [Value]>
242
+ : never
243
+ : never;
244
+
245
+ export interface Many<P extends Any> extends Parser<unknown> {
246
+ readonly return: Arg0<this> extends infer Input extends string ? ManyInternal<P, Input> : never;
247
+ }
248
+
249
+ export interface Many1<P extends Any> extends Parser<unknown> {
250
+ readonly return: Arg0<this> extends infer Input extends string
251
+ ? Many1Internal<P, Input>
252
+ : never;
253
+ }
254
+
255
+ export interface MapTo<F extends TypeLambda1> extends TypeLambda1 {
256
+ readonly return: Arg0<this> extends infer P extends Any ? Map<P, F> : never;
257
+ }
258
+
259
+ export interface FlatMapTo<F extends TypeLambda1> extends TypeLambda1 {
260
+ readonly return: Arg0<this> extends infer P extends Any ? FlatMap<P, F> : never;
261
+ }
262
+
263
+ export interface ZipWith<Q extends Any> extends TypeLambda1 {
264
+ readonly return: Arg0<this> extends infer P extends Any ? Zip<P, Q> : never;
265
+ }
266
+
267
+ export interface OrElseWith<Q extends Any> extends TypeLambda1 {
268
+ readonly return: Arg0<this> extends infer P extends Any ? OrElse<P, Q> : never;
269
+ }
270
+
271
+ export interface OptionalOf extends TypeLambda1 {
272
+ readonly return: Arg0<this> extends infer P extends Any ? Optional<P> : never;
273
+ }
274
+
275
+ export interface ManyOf extends TypeLambda1 {
276
+ readonly return: Arg0<this> extends infer P extends Any ? Many<P> : never;
277
+ }
278
+
279
+ export interface Many1Of extends TypeLambda1 {
280
+ readonly return: Arg0<this> extends infer P extends Any ? Many1<P> : never;
281
+ }
282
+ }
@@ -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
+ });