effect-start 0.14.0 → 0.16.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 (87) hide show
  1. package/package.json +8 -9
  2. package/src/Commander.test.ts +507 -245
  3. package/src/ContentNegotiation.test.ts +603 -0
  4. package/src/ContentNegotiation.ts +542 -0
  5. package/src/Entity.test.ts +592 -0
  6. package/src/Entity.ts +362 -0
  7. package/src/FileRouter.ts +16 -12
  8. package/src/{FileRouterCodegen.test.ts → FileRouterCodegen.todo.ts} +384 -219
  9. package/src/FileRouterCodegen.ts +6 -6
  10. package/src/FileRouterPattern.test.ts +93 -62
  11. package/src/FileRouter_files.test.ts +5 -5
  12. package/src/FileRouter_path.test.ts +121 -69
  13. package/src/FileRouter_tree.test.ts +62 -56
  14. package/src/FileSystemExtra.test.ts +46 -30
  15. package/src/Http.test.ts +319 -0
  16. package/src/Http.ts +167 -0
  17. package/src/HttpAppExtra.test.ts +39 -20
  18. package/src/HttpAppExtra.ts +0 -1
  19. package/src/HttpUtils.test.ts +35 -18
  20. package/src/HttpUtils.ts +2 -0
  21. package/src/PathPattern.test.ts +648 -0
  22. package/src/PathPattern.ts +485 -0
  23. package/src/Route.ts +266 -1069
  24. package/src/RouteBody.test.ts +234 -0
  25. package/src/RouteBody.ts +193 -0
  26. package/src/RouteHook.test.ts +40 -0
  27. package/src/RouteHook.ts +106 -0
  28. package/src/RouteHttp.test.ts +2906 -0
  29. package/src/RouteHttp.ts +427 -0
  30. package/src/RouteHttpTracer.ts +92 -0
  31. package/src/RouteMount.test.ts +481 -0
  32. package/src/RouteMount.ts +470 -0
  33. package/src/RouteSchema.test.ts +427 -0
  34. package/src/RouteSchema.ts +423 -0
  35. package/src/RouteTree.test.ts +494 -0
  36. package/src/RouteTree.ts +219 -0
  37. package/src/RouteTrie.test.ts +322 -0
  38. package/src/RouteTrie.ts +224 -0
  39. package/src/RouterPattern.test.ts +569 -548
  40. package/src/RouterPattern.ts +7 -7
  41. package/src/Start.ts +3 -3
  42. package/src/StreamExtra.ts +21 -1
  43. package/src/TuplePathPattern.ts +64 -0
  44. package/src/Values.test.ts +263 -0
  45. package/src/Values.ts +76 -0
  46. package/src/bun/BunBundle.test.ts +36 -42
  47. package/src/bun/BunBundle.ts +2 -2
  48. package/src/bun/BunBundle_imports.test.ts +4 -6
  49. package/src/bun/BunHttpServer.test.ts +183 -6
  50. package/src/bun/BunHttpServer.ts +72 -32
  51. package/src/bun/BunHttpServer_web.ts +18 -6
  52. package/src/bun/BunImportTrackerPlugin.test.ts +3 -3
  53. package/src/bun/BunRoute.test.ts +124 -442
  54. package/src/bun/BunRoute.ts +146 -286
  55. package/src/{BundleHttp.test.ts → bundler/BundleHttp.test.ts} +34 -60
  56. package/src/{BundleHttp.ts → bundler/BundleHttp.ts} +1 -2
  57. package/src/client/index.ts +1 -1
  58. package/src/{Effect_HttpRouter.test.ts → effect/HttpRouter.test.ts} +69 -90
  59. package/src/experimental/EncryptedCookies.test.ts +125 -64
  60. package/src/experimental/SseHttpResponse.ts +0 -1
  61. package/src/hyper/Hyper.ts +89 -0
  62. package/src/{HyperHtml.test.ts → hyper/HyperHtml.test.ts} +13 -13
  63. package/src/{HyperHtml.ts → hyper/HyperHtml.ts} +2 -2
  64. package/src/{jsx.d.ts → hyper/jsx.d.ts} +1 -1
  65. package/src/index.ts +3 -4
  66. package/src/middlewares/BasicAuthMiddleware.test.ts +29 -19
  67. package/src/{NodeFileSystem.ts → node/FileSystem.ts} +6 -2
  68. package/src/testing/TestHttpClient.test.ts +26 -26
  69. package/src/testing/TestLogger.test.ts +27 -14
  70. package/src/testing/TestLogger.ts +15 -9
  71. package/src/x/datastar/Datastar.test.ts +47 -48
  72. package/src/x/datastar/Datastar.ts +1 -1
  73. package/src/x/tailwind/TailwindPlugin.test.ts +56 -58
  74. package/src/x/tailwind/plugin.ts +1 -1
  75. package/src/FileHttpRouter.test.ts +0 -239
  76. package/src/FileHttpRouter.ts +0 -194
  77. package/src/Hyper.ts +0 -194
  78. package/src/Route.test.ts +0 -1370
  79. package/src/RouteRender.ts +0 -40
  80. package/src/Router.test.ts +0 -375
  81. package/src/Router.ts +0 -255
  82. package/src/bun/BunRoute_bundles.test.ts +0 -219
  83. /package/src/{Bundle.ts → bundler/Bundle.ts} +0 -0
  84. /package/src/{BundleFiles.ts → bundler/BundleFiles.ts} +0 -0
  85. /package/src/{HyperNode.ts → hyper/HyperNode.ts} +0 -0
  86. /package/src/{jsx-runtime.ts → hyper/jsx-runtime.ts} +0 -0
  87. /package/src/{NodeUtils.ts → node/Utils.ts} +0 -0
package/src/Route.test.ts DELETED
@@ -1,1370 +0,0 @@
1
- import * as HttpApp from "@effect/platform/HttpApp"
2
- import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
3
- import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
4
- import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
5
- import * as t from "bun:test"
6
-
7
- import * as Effect from "effect/Effect"
8
- import * as Function from "effect/Function"
9
- import * as Schema from "effect/Schema"
10
- import * as Route from "./Route.ts"
11
-
12
- t.it("types default routes", () => {
13
- const implicit = Route
14
- .text(
15
- Effect.succeed("hello"),
16
- )
17
- .html(
18
- Effect.succeed(""),
19
- )
20
-
21
- const explicit = Route
22
- .get(
23
- Route
24
- .text(
25
- Effect.succeed("hello"),
26
- )
27
- .html(
28
- Effect.succeed(""),
29
- ),
30
- )
31
-
32
- type Expected = Route.RouteSet<[
33
- Route.Route<"GET", "text/plain">,
34
- Route.Route<"GET", "text/html">,
35
- ]>
36
-
37
- Function.satisfies<Expected>()(implicit)
38
- Function.satisfies<Expected>()(explicit)
39
- })
40
-
41
- t.it("types GET & POST routes", () => {
42
- const implicit = Route
43
- .text(
44
- Effect.succeed("hello"),
45
- )
46
- .html(
47
- Effect.succeed(""),
48
- )
49
- .post(
50
- Route.json(
51
- Effect.succeed({
52
- message: "created",
53
- }),
54
- ),
55
- )
56
-
57
- const explicit = Route
58
- .get(
59
- Route
60
- .text(
61
- Effect.succeed("hello"),
62
- )
63
- .html(
64
- Effect.succeed(""),
65
- ),
66
- )
67
- .post(
68
- Route.json(
69
- Effect.succeed({
70
- message: "created",
71
- }),
72
- ),
73
- )
74
-
75
- type Expected = Route.RouteSet<[
76
- Route.Route<"GET", "text/plain">,
77
- Route.Route<"GET", "text/html">,
78
- Route.Route<"POST", "application/json">,
79
- ]>
80
-
81
- Function.satisfies<Expected>()(implicit)
82
- Function.satisfies<Expected>()(explicit)
83
- })
84
-
85
- t.it("schemaPathParams adds schema to RouteSet", () => {
86
- const IdSchema = Schema.Struct({
87
- id: Schema.String,
88
- })
89
-
90
- const routes = Route
91
- .schemaPathParams(IdSchema)
92
- .text(
93
- Effect.succeed("hello"),
94
- )
95
-
96
- type ExpectedSchemas = {
97
- readonly PathParams: typeof IdSchema
98
- }
99
-
100
- type Expected = Route.RouteSet<
101
- [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
102
- ExpectedSchemas
103
- >
104
-
105
- Function.satisfies<Expected>()(routes)
106
- })
107
-
108
- t.it("schemaPathParams accepts struct fields directly", () => {
109
- const routes = Route
110
- .schemaPathParams({
111
- id: Schema.String,
112
- })
113
- .text(
114
- Effect.succeed("hello"),
115
- )
116
-
117
- type ExpectedSchemas = {
118
- readonly PathParams: Schema.Struct<{
119
- id: typeof Schema.String
120
- }>
121
- }
122
-
123
- type Expected = Route.RouteSet<
124
- [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
125
- ExpectedSchemas
126
- >
127
-
128
- Function.satisfies<Expected>()(routes)
129
- })
130
-
131
- t.it("schemaPathParams with struct fields types context correctly", () => {
132
- Route
133
- .schemaPathParams({
134
- id: Schema.String,
135
- })
136
- .text(
137
- (context) => {
138
- Function.satisfies<string>()(context.pathParams.id)
139
-
140
- return Effect.succeed("hello")
141
- },
142
- )
143
- })
144
-
145
- t.it("schemaPayload propagates to all routes", () => {
146
- const PayloadSchema = Schema.Struct({
147
- name: Schema.String,
148
- age: Schema.Number,
149
- })
150
-
151
- const routes = Route
152
- .schemaPayload(PayloadSchema)
153
- .get(
154
- Route.text(
155
- Effect.succeed("get"),
156
- ),
157
- )
158
- .post(
159
- Route.text(
160
- Effect.succeed("post"),
161
- ),
162
- )
163
-
164
- type ExpectedSchemas = {
165
- readonly Payload: typeof PayloadSchema
166
- }
167
-
168
- type Expected = Route.RouteSet<
169
- [
170
- Route.Route<"GET", "text/plain", any, ExpectedSchemas>,
171
- Route.Route<"POST", "text/plain", any, ExpectedSchemas>,
172
- ],
173
- ExpectedSchemas
174
- >
175
-
176
- Function.satisfies<Expected>()(routes)
177
- })
178
-
179
- t.it(
180
- "context is typed with pathParams when schemaPathParams is provided",
181
- () => {
182
- const IdSchema = Schema.Struct({
183
- id: Schema.String,
184
- })
185
-
186
- Route
187
- .schemaPathParams(IdSchema)
188
- .text(
189
- (context) => {
190
- type ContextType = typeof context
191
-
192
- type Expected = Route.RouteContext<{
193
- pathParams: {
194
- id: string
195
- }
196
- }>
197
-
198
- Function.satisfies<Expected>()(context)
199
-
200
- Function.satisfies<string>()(context.pathParams.id)
201
-
202
- return Effect.succeed("hello")
203
- },
204
- )
205
- },
206
- )
207
-
208
- t.it("context is typed with urlParams when schemaUrlParams is provided", () => {
209
- const QuerySchema = Schema.Struct({
210
- page: Schema.NumberFromString,
211
- limit: Schema.NumberFromString,
212
- })
213
-
214
- Route
215
- .schemaUrlParams(QuerySchema)
216
- .text(
217
- (context) => {
218
- type Expected = Route.RouteContext<{
219
- urlParams: {
220
- page: number
221
- limit: number
222
- }
223
- }>
224
-
225
- Function.satisfies<Expected>()(context)
226
-
227
- Function.satisfies<number>()(context.urlParams.page)
228
-
229
- Function.satisfies<number>()(context.urlParams.limit)
230
-
231
- return Effect.succeed("hello")
232
- },
233
- )
234
- })
235
-
236
- t.it("context is typed with payload when schemaPayload is provided", () => {
237
- const PayloadSchema = Schema.Struct({
238
- name: Schema.String,
239
- age: Schema.Number,
240
- })
241
-
242
- Route
243
- .schemaPayload(PayloadSchema)
244
- .text(
245
- (context) => {
246
- type Expected = Route.RouteContext<{
247
- payload: {
248
- name: string
249
- age: number
250
- }
251
- }>
252
-
253
- Function.satisfies<Expected>()(context)
254
-
255
- Function.satisfies<string>()(context.payload.name)
256
-
257
- Function.satisfies<number>()(context.payload.age)
258
-
259
- return Effect.succeed("hello")
260
- },
261
- )
262
- })
263
-
264
- t.it("context is typed with headers when schemaHeaders is provided", () => {
265
- const HeadersSchema = Schema.Struct({
266
- authorization: Schema.String,
267
- })
268
-
269
- Route
270
- .schemaHeaders(HeadersSchema)
271
- .text(
272
- (context) => {
273
- type Expected = Route.RouteContext<{
274
- headers: {
275
- authorization: string
276
- }
277
- }>
278
-
279
- Function.satisfies<Expected>()(context)
280
-
281
- Function.satisfies<string>()(context.headers.authorization)
282
-
283
- return Effect.succeed("hello")
284
- },
285
- )
286
- })
287
-
288
- t.it("context is typed with multiple schemas", () => {
289
- const IdSchema = Schema.Struct({
290
- id: Schema.String,
291
- })
292
-
293
- const QuerySchema = Schema.Struct({
294
- page: Schema.NumberFromString,
295
- })
296
-
297
- const PayloadSchema = Schema.Struct({
298
- name: Schema.String,
299
- })
300
-
301
- Route
302
- .schemaPathParams(IdSchema)
303
- .schemaUrlParams(QuerySchema)
304
- .schemaPayload(PayloadSchema)
305
- .text(
306
- (context) => {
307
- type Expected = Route.RouteContext<{
308
- pathParams: {
309
- id: string
310
- }
311
- urlParams: {
312
- page: number
313
- }
314
- payload: {
315
- name: string
316
- }
317
- }>
318
-
319
- Function.satisfies<Expected>()(context)
320
-
321
- Function.satisfies<string>()(context.pathParams.id)
322
-
323
- Function.satisfies<number>()(context.urlParams.page)
324
-
325
- Function.satisfies<string>()(context.payload.name)
326
-
327
- return Effect.succeed("hello")
328
- },
329
- )
330
- })
331
-
332
- t.it("schemaSuccess and schemaError are stored in RouteSet", () => {
333
- const SuccessSchema = Schema.Struct({
334
- ok: Schema.Boolean,
335
- })
336
-
337
- const ErrorSchema = Schema.Struct({
338
- error: Schema.String,
339
- })
340
-
341
- const routes = Route
342
- .schemaSuccess(SuccessSchema)
343
- .schemaError(ErrorSchema)
344
- .text(
345
- Effect.succeed("hello"),
346
- )
347
-
348
- type ExpectedSchemas = {
349
- readonly Success: typeof SuccessSchema
350
- readonly Error: typeof ErrorSchema
351
- }
352
-
353
- type Expected = Route.RouteSet<
354
- [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
355
- ExpectedSchemas
356
- >
357
-
358
- Function.satisfies<Expected>()(routes)
359
- })
360
-
361
- t.it("all schema methods work together", () => {
362
- const PathSchema = Schema.Struct({
363
- id: Schema.String,
364
- })
365
-
366
- const QuerySchema = Schema.Struct({
367
- page: Schema.NumberFromString,
368
- })
369
-
370
- const PayloadSchema = Schema.Struct({
371
- name: Schema.String,
372
- })
373
-
374
- const SuccessSchema = Schema.Struct({
375
- ok: Schema.Boolean,
376
- })
377
-
378
- const ErrorSchema = Schema.Struct({
379
- error: Schema.String,
380
- })
381
-
382
- const HeadersSchema = Schema.Struct({
383
- authorization: Schema.String,
384
- })
385
-
386
- const routes = Route
387
- .schemaPathParams(PathSchema)
388
- .schemaUrlParams(QuerySchema)
389
- .schemaPayload(PayloadSchema)
390
- .schemaSuccess(SuccessSchema)
391
- .schemaError(ErrorSchema)
392
- .schemaHeaders(HeadersSchema)
393
- .text(
394
- Effect.succeed("hello"),
395
- )
396
-
397
- type ExpectedSchemas = {
398
- readonly PathParams: typeof PathSchema
399
- readonly UrlParams: typeof QuerySchema
400
- readonly Payload: typeof PayloadSchema
401
- readonly Success: typeof SuccessSchema
402
- readonly Error: typeof ErrorSchema
403
- readonly Headers: typeof HeadersSchema
404
- }
405
-
406
- type Expected = Route.RouteSet<
407
- [Route.Route<"GET", "text/plain", any, ExpectedSchemas>],
408
- ExpectedSchemas
409
- >
410
-
411
- Function.satisfies<Expected>()(routes)
412
- })
413
-
414
- t.it("schemas merge when RouteSet and Route both define same schema", () => {
415
- const BaseSchema = Schema.Struct({
416
- id: Schema.String,
417
- })
418
-
419
- const ExtendedSchema = Schema.Struct({
420
- name: Schema.String,
421
- })
422
-
423
- const routes = Route
424
- .schemaPathParams(BaseSchema)
425
- .get(
426
- Route
427
- .schemaPathParams(ExtendedSchema)
428
- .text(Effect.succeed("hello")),
429
- )
430
-
431
- type Expected = Route.RouteSet<
432
- [
433
- Route.Route<
434
- "GET",
435
- "text/plain",
436
- any,
437
- {
438
- readonly PathParams: Schema.Struct<
439
- {
440
- id: typeof Schema.String
441
- name: typeof Schema.String
442
- }
443
- >
444
- }
445
- >,
446
- ],
447
- {
448
- readonly PathParams: typeof BaseSchema
449
- }
450
- >
451
-
452
- Function.satisfies<Expected>()(routes)
453
- })
454
-
455
- t.it("context has only request and url when no schemas provided", () => {
456
- Route
457
- .text(
458
- (context) => {
459
- type Expected = Route.RouteContext<{}>
460
-
461
- Function.satisfies<Expected>()(context)
462
-
463
- Function.satisfies<typeof context.request>()(context.request)
464
-
465
- Function.satisfies<URL>()(context.url)
466
-
467
- // @ts-expect-error - pathParams should not exist
468
- context.pathParams
469
-
470
- return Effect.succeed("hello")
471
- },
472
- )
473
- })
474
-
475
- t.it("context.next() returns correct type for text handler", () => {
476
- Route.text(function*(context) {
477
- const next = context.next()
478
- type NextType = Effect.Effect.Success<typeof next>
479
- type _check = [NextType] extends [string] ? true : false
480
- const _assert: _check = true
481
- return "hello"
482
- })
483
- })
484
-
485
- t.it("context.next() returns correct type for html handler", () => {
486
- Route.html(function*(context) {
487
- const next = context.next()
488
- type NextType = Effect.Effect.Success<typeof next>
489
- type _check = [NextType] extends [string | Route.GenericJsxObject] ? true
490
- : false
491
- const _assert: _check = true
492
- return "<div>hello</div>"
493
- })
494
- })
495
-
496
- t.it("context.next() returns correct type for json handler", () => {
497
- Route.json(function*(context) {
498
- const next = context.next()
499
- type NextType = Effect.Effect.Success<typeof next>
500
- type _check = [NextType] extends [Route.JsonValue] ? true : false
501
- const _assert: _check = true
502
- return { message: "hello" }
503
- })
504
- })
505
-
506
- t.it("schemas work with all media types", () => {
507
- const PathSchema = Schema.Struct({
508
- id: Schema.String,
509
- })
510
-
511
- Route
512
- .schemaPathParams(PathSchema)
513
- .html((context) => {
514
- Function.satisfies<string>()(context.pathParams.id)
515
-
516
- return Effect.succeed("<h1>Hello</h1>")
517
- })
518
-
519
- Route
520
- .schemaPathParams(PathSchema)
521
- .json((context) => {
522
- Function.satisfies<string>()(context.pathParams.id)
523
-
524
- return Effect.succeed({ message: "hello" })
525
- })
526
- })
527
-
528
- t.it("schemas work with generator functions", () => {
529
- const IdSchema = Schema.Struct({
530
- id: Schema.String,
531
- })
532
-
533
- Route
534
- .schemaPathParams(IdSchema)
535
- .text(function*(context) {
536
- Function.satisfies<string>()(context.pathParams.id)
537
-
538
- return "hello"
539
- })
540
- })
541
-
542
- t.it("schema property is correctly set on RouteSet", () => {
543
- const PathSchema = Schema.Struct({
544
- id: Schema.String,
545
- })
546
-
547
- const routes = Route
548
- .schemaPathParams(PathSchema)
549
- .text(Effect.succeed("hello"))
550
-
551
- type Expected = {
552
- readonly PathParams: typeof PathSchema
553
- }
554
-
555
- Function.satisfies<Expected>()(routes.schema)
556
- })
557
-
558
- t.it("schemas don't leak between independent route chains", () => {
559
- const Schema1 = Schema.Struct({
560
- id: Schema.String,
561
- })
562
-
563
- const Schema2 = Schema.Struct({
564
- userId: Schema.String,
565
- })
566
-
567
- const route1 = Route
568
- .schemaPathParams(Schema1)
569
- .text(Effect.succeed("route1"))
570
-
571
- const route2 = Route
572
- .schemaPathParams(Schema2)
573
- .text(Effect.succeed("route2"))
574
-
575
- type Expected1 = Route.RouteSet<
576
- [
577
- Route.Route<
578
- "GET",
579
- "text/plain",
580
- any,
581
- { readonly PathParams: typeof Schema1 }
582
- >,
583
- ],
584
- { readonly PathParams: typeof Schema1 }
585
- >
586
-
587
- type Expected2 = Route.RouteSet<
588
- [
589
- Route.Route<
590
- "GET",
591
- "text/plain",
592
- any,
593
- { readonly PathParams: typeof Schema2 }
594
- >,
595
- ],
596
- { readonly PathParams: typeof Schema2 }
597
- >
598
-
599
- Function.satisfies<Expected1>()(route1)
600
- Function.satisfies<Expected2>()(route2)
601
- })
602
-
603
- t.it("schema order doesn't matter", () => {
604
- const PathSchema = Schema.Struct({
605
- id: Schema.String,
606
- })
607
-
608
- const PayloadSchema = Schema.Struct({
609
- name: Schema.String,
610
- })
611
-
612
- const routes1 = Route
613
- .schemaPathParams(PathSchema)
614
- .schemaPayload(PayloadSchema)
615
- .text(Effect.succeed("hello"))
616
-
617
- const routes2 = Route
618
- .schemaPayload(PayloadSchema)
619
- .schemaPathParams(PathSchema)
620
- .text(Effect.succeed("hello"))
621
-
622
- type Expected = {
623
- readonly PathParams: typeof PathSchema
624
- readonly Payload: typeof PayloadSchema
625
- }
626
-
627
- Function.satisfies<Expected>()(routes1.schema)
628
- Function.satisfies<Expected>()(routes2.schema)
629
- })
630
-
631
- t.it("multiple routes in RouteSet each get the schema", () => {
632
- const PathSchema = Schema.Struct({
633
- id: Schema.String,
634
- })
635
-
636
- const routes = Route
637
- .schemaPathParams(PathSchema)
638
- .text(Effect.succeed("text"))
639
- .html(Effect.succeed("<p>html</p>"))
640
- .json(Effect.succeed({ data: "json" }))
641
-
642
- type ExpectedSchemas = {
643
- readonly PathParams: typeof PathSchema
644
- }
645
-
646
- type Expected = Route.RouteSet<
647
- [
648
- Route.Route<"GET", "text/plain", any, ExpectedSchemas>,
649
- Route.Route<"GET", "text/html", any, ExpectedSchemas>,
650
- Route.Route<"GET", "application/json", any, ExpectedSchemas>,
651
- ],
652
- ExpectedSchemas
653
- >
654
-
655
- Function.satisfies<Expected>()(routes)
656
- })
657
-
658
- t.it("schemas merge correctly with struct fields syntax", () => {
659
- const routes = Route
660
- .schemaPathParams({ id: Schema.String })
661
- .get(
662
- Route
663
- .schemaPathParams({ userId: Schema.String })
664
- .text(Effect.succeed("hello")),
665
- )
666
-
667
- routes
668
- .set[0]
669
- .text(
670
- (context) => {
671
- Function.satisfies<string>()(context.pathParams.id)
672
- Function.satisfies<string>()(context.pathParams.userId)
673
-
674
- return Effect.succeed("hello")
675
- },
676
- )
677
- })
678
-
679
- t.it("method modifiers preserve and merge schemas", () => {
680
- const PathSchema = Schema.Struct({
681
- id: Schema.String,
682
- })
683
-
684
- const PayloadSchema = Schema.Struct({
685
- name: Schema.String,
686
- })
687
-
688
- const routes = Route
689
- .schemaPathParams(PathSchema)
690
- .post(
691
- Route
692
- .schemaPayload(PayloadSchema)
693
- .text(Effect.succeed("created")),
694
- )
695
-
696
- type Expected = Route.RouteSet<
697
- [
698
- Route.Route<
699
- "POST",
700
- "text/plain",
701
- any,
702
- {
703
- readonly PathParams: typeof PathSchema
704
- readonly Payload: typeof PayloadSchema
705
- }
706
- >,
707
- ],
708
- {
709
- readonly PathParams: typeof PathSchema
710
- }
711
- >
712
-
713
- Function.satisfies<Expected>()(routes)
714
- })
715
-
716
- t.it("method modifiers require routes with handlers", () => {
717
- const PathSchema = Schema.Struct({
718
- id: Schema.String,
719
- })
720
-
721
- Route
722
- .schemaPathParams(PathSchema)
723
- .get(
724
- Route
725
- .schemaPathParams({ userId: Schema.String })
726
- .text(Effect.succeed("hello")),
727
- )
728
-
729
- Route
730
- .schemaPathParams(PathSchema)
731
- .get(
732
- // @ts-expect-error - method modifiers should reject empty RouteSet
733
- Route.schemaPathParams({ userId: Schema.String }),
734
- )
735
- })
736
-
737
- t.it("method modifiers preserve proper types when nesting schemas", () => {
738
- const PathSchema = Schema.Struct({
739
- id: Schema.String,
740
- })
741
-
742
- const route = Route
743
- .schemaPathParams(PathSchema)
744
- .get(
745
- Route
746
- .schemaPathParams({ userId: Schema.String })
747
- .text(Effect.succeed("hello")),
748
- )
749
-
750
- type BaseSchemas = {
751
- readonly PathParams: typeof PathSchema
752
- }
753
-
754
- type MergedPathParams = Schema.Struct<{
755
- id: typeof Schema.String
756
- userId: typeof Schema.String
757
- }>
758
-
759
- type Expected = Route.RouteSet<
760
- [
761
- Route.Route<"GET", "text/plain", any, {
762
- readonly PathParams: MergedPathParams
763
- }>,
764
- ],
765
- BaseSchemas
766
- >
767
-
768
- Function.satisfies<Expected>()(route)
769
- })
770
-
771
- t.it("schemaUrlParams accepts optional fields", () => {
772
- const routes = Route
773
- .schemaUrlParams({
774
- hello: Function.pipe(
775
- Schema.String,
776
- Schema.optional,
777
- ),
778
- })
779
- .html(
780
- (ctx) => {
781
- Function.satisfies<string | undefined>()(ctx.urlParams.hello)
782
-
783
- const page = ctx.urlParams.hello ?? "default"
784
-
785
- return Effect.succeed(`<div><h1>About ${page}</h1></div>`)
786
- },
787
- )
788
-
789
- type ExpectedSchemas = {
790
- readonly UrlParams: Schema.Struct<{
791
- hello: Schema.optional<typeof Schema.String>
792
- }>
793
- }
794
-
795
- type Expected = Route.RouteSet<
796
- [Route.Route<"GET", "text/html", any, ExpectedSchemas>],
797
- ExpectedSchemas
798
- >
799
-
800
- Function.satisfies<Expected>()(routes)
801
- })
802
-
803
- t.it("schemaPathParams only accepts string-encoded schemas", () => {
804
- Route
805
- .schemaPathParams({
806
- id: Schema.String,
807
- })
808
- .text(Effect.succeed("ok"))
809
-
810
- Route
811
- .schemaPathParams({
812
- id: Schema.NumberFromString,
813
- })
814
- .text(Effect.succeed("ok"))
815
-
816
- Route
817
- .schemaPathParams({
818
- // @ts-expect-error - Schema.Number is not string-encoded
819
- id: Schema.Number,
820
- })
821
- .text(Effect.succeed("ok"))
822
-
823
- Route
824
- .schemaPathParams({
825
- // @ts-expect-error - Schema.Struct is not string-encoded
826
- nested: Schema.Struct({
827
- field: Schema.String,
828
- }),
829
- })
830
- .text(Effect.succeed("ok"))
831
- })
832
-
833
- t.it("schemaUrlParams accepts string and string array encoded schemas", () => {
834
- Route
835
- .schemaUrlParams({
836
- page: Schema.String,
837
- })
838
- .text(Effect.succeed("ok"))
839
-
840
- Route
841
- .schemaUrlParams({
842
- page: Schema.NumberFromString,
843
- })
844
- .text(Effect.succeed("ok"))
845
-
846
- Route
847
- .schemaUrlParams({
848
- tags: Schema.Array(Schema.String),
849
- })
850
- .text(Effect.succeed("ok"))
851
-
852
- Route
853
- .schemaUrlParams({
854
- // @ts-expect-error - Schema.Number is not string-encoded
855
- page: Schema.Number,
856
- })
857
- .text(Effect.succeed("ok"))
858
-
859
- Route
860
- .schemaUrlParams({
861
- // @ts-expect-error - Schema.Struct is not string-encoded
862
- nested: Schema.Struct({
863
- field: Schema.String,
864
- }),
865
- })
866
- .text(Effect.succeed("ok"))
867
- })
868
-
869
- t.it("schemaHeaders accepts string and string array encoded schemas", () => {
870
- Route
871
- .schemaHeaders({
872
- authorization: Schema.String,
873
- })
874
- .text(Effect.succeed("ok"))
875
-
876
- Route
877
- .schemaHeaders({
878
- "x-custom-header": Schema.NumberFromString,
879
- })
880
- .text(Effect.succeed("ok"))
881
-
882
- Route
883
- .schemaHeaders({
884
- "accept-encoding": Schema.Array(Schema.String),
885
- })
886
- .text(Effect.succeed("ok"))
887
-
888
- Route
889
- .schemaHeaders({
890
- // @ts-expect-error - Schema.Number is not string-encoded
891
- "x-count": Schema.Number,
892
- })
893
- .text(Effect.succeed("ok"))
894
-
895
- Route
896
- .schemaHeaders({
897
- // @ts-expect-error - Schema.Struct is not string-encoded
898
- "x-metadata": Schema.Struct({
899
- field: Schema.String,
900
- }),
901
- })
902
- .text(Effect.succeed("ok"))
903
- })
904
-
905
- t.it("Route.http creates RouteMiddleware", () => {
906
- const middleware = (app: any) => app
907
-
908
- const spec = Route.http(middleware)
909
-
910
- t.expect(spec._tag).toBe("RouteMiddleware")
911
- t.expect(spec.middleware).toBe(middleware)
912
- })
913
-
914
- t.it("Route.layer creates RouteLayer with middleware", () => {
915
- const middleware = (app: any) => app
916
-
917
- const layer = Route.layer(
918
- Route.http(middleware),
919
- Route.html(Effect.succeed("<div>test</div>")),
920
- )
921
-
922
- t.expect(Route.isRouteLayer(layer)).toBe(true)
923
- t.expect(layer.httpMiddleware).toBe(middleware)
924
- t.expect(layer.set.length).toBe(1)
925
- })
926
-
927
- t.it("Route.layer merges multiple route sets", () => {
928
- const routes1 = Route.html(Effect.succeed("<div>1</div>"))
929
- const routes2 = Route.text("text")
930
-
931
- const layer = Route.layer(routes1, routes2)
932
-
933
- t.expect(layer.set.length).toBe(2)
934
- t.expect(Route.isRouteLayer(layer)).toBe(true)
935
- })
936
-
937
- t.it("Route.layer merges routes from all route sets", () => {
938
- const routes1 = Route
939
- .schemaPathParams({ id: Schema.String })
940
- .html(Effect.succeed("<div>test</div>"))
941
-
942
- const routes2 = Route
943
- .schemaUrlParams({ page: Schema.NumberFromString })
944
- .text(Effect.succeed("text"))
945
-
946
- const layer = Route.layer(routes1, routes2)
947
-
948
- t.expect(layer.set.length).toBe(2)
949
- t.expect(layer.set[0]!.media).toBe("text/html")
950
- t.expect(layer.set[1]!.media).toBe("text/plain")
951
- })
952
-
953
- t.it("Route.layer works with no middleware", () => {
954
- const layer = Route.layer(
955
- Route.html(Effect.succeed("<div>test</div>")),
956
- )
957
-
958
- t.expect(Route.isRouteLayer(layer)).toBe(true)
959
- t.expect(layer.httpMiddleware).toBeUndefined()
960
- t.expect(layer.set.length).toBe(1)
961
- })
962
-
963
- t.it("Route.layer works with no routes", () => {
964
- const middleware = (app: any) => app
965
-
966
- const layer = Route.layer(
967
- Route.http(middleware),
968
- )
969
-
970
- t.expect(Route.isRouteLayer(layer)).toBe(true)
971
- t.expect(layer.httpMiddleware).toBe(middleware)
972
- t.expect(layer.set.length).toBe(0)
973
- })
974
-
975
- t.it("isRouteLayer type guard works correctly", () => {
976
- const middleware = (app: any) => app
977
- const layer = Route.layer(Route.http(middleware))
978
- const regularRoutes = Route.html(Effect.succeed("<div>test</div>"))
979
-
980
- t.expect(Route.isRouteLayer(layer)).toBe(true)
981
- t.expect(Route.isRouteLayer(regularRoutes)).toBe(false)
982
- t.expect(Route.isRouteLayer(null)).toBe(false)
983
- t.expect(Route.isRouteLayer(undefined)).toBe(false)
984
- t.expect(Route.isRouteLayer({})).toBe(false)
985
- })
986
-
987
- t.it("Route.layer composes multiple middleware in order", async () => {
988
- const executionOrder: string[] = []
989
-
990
- const middleware1 = HttpMiddleware.make((app) =>
991
- Effect.gen(function*() {
992
- executionOrder.push("middleware1-before")
993
- const result = yield* app
994
- executionOrder.push("middleware1-after")
995
- return result
996
- })
997
- ) as Route.HttpMiddlewareFunction
998
-
999
- const middleware2 = HttpMiddleware.make((app) =>
1000
- Effect.gen(function*() {
1001
- executionOrder.push("middleware2-before")
1002
- const result = yield* app
1003
- executionOrder.push("middleware2-after")
1004
- return result
1005
- })
1006
- ) as Route.HttpMiddlewareFunction
1007
-
1008
- const middleware3 = HttpMiddleware.make((app) =>
1009
- Effect.gen(function*() {
1010
- executionOrder.push("middleware3-before")
1011
- const result = yield* app
1012
- executionOrder.push("middleware3-after")
1013
- return result
1014
- })
1015
- ) as Route.HttpMiddlewareFunction
1016
-
1017
- const layer = Route.layer(
1018
- Route.http(middleware1),
1019
- Route.http(middleware2),
1020
- Route.http(middleware3),
1021
- )
1022
-
1023
- t.expect(layer.httpMiddleware).toBeDefined()
1024
-
1025
- const mockApp = Effect.sync(() => {
1026
- executionOrder.push("app")
1027
- return HttpServerResponse.text("result")
1028
- })
1029
-
1030
- const composed = layer.httpMiddleware!(mockApp) as Effect.Effect<
1031
- HttpServerResponse.HttpServerResponse,
1032
- never,
1033
- never
1034
- >
1035
- await Effect.runPromise(composed.pipe(Effect.orDie))
1036
-
1037
- t.expect(executionOrder).toEqual([
1038
- "middleware1-before",
1039
- "middleware2-before",
1040
- "middleware3-before",
1041
- "app",
1042
- "middleware3-after",
1043
- "middleware2-after",
1044
- "middleware1-after",
1045
- ])
1046
- })
1047
-
1048
- t.it("Route.layer with single middleware works correctly", async () => {
1049
- let middlewareCalled = false
1050
-
1051
- const middleware = HttpMiddleware.make((app) =>
1052
- Effect.gen(function*() {
1053
- middlewareCalled = true
1054
- return yield* app
1055
- })
1056
- ) as Route.HttpMiddlewareFunction
1057
-
1058
- const layer = Route.layer(Route.http(middleware))
1059
-
1060
- t.expect(layer.httpMiddleware).toBeDefined()
1061
-
1062
- const mockApp = Effect.succeed(HttpServerResponse.text("result"))
1063
- const composed = layer.httpMiddleware!(mockApp) as Effect.Effect<
1064
- HttpServerResponse.HttpServerResponse,
1065
- never,
1066
- never
1067
- >
1068
- await Effect.runPromise(composed.pipe(Effect.orDie))
1069
-
1070
- t.expect(middlewareCalled).toBe(true)
1071
- })
1072
-
1073
- t.it("Route.layer middleware can modify responses", async () => {
1074
- const addHeader1 = HttpMiddleware.make((app) =>
1075
- Effect.gen(function*() {
1076
- const result = yield* app
1077
- return HttpServerResponse.setHeader(result, "X-Custom-1", "value1")
1078
- })
1079
- ) as Route.HttpMiddlewareFunction
1080
-
1081
- const addHeader2 = HttpMiddleware.make((app) =>
1082
- Effect.gen(function*() {
1083
- const result = yield* app
1084
- return HttpServerResponse.setHeader(result, "X-Custom-2", "value2")
1085
- })
1086
- ) as Route.HttpMiddlewareFunction
1087
-
1088
- const layer = Route.layer(
1089
- Route.http(addHeader1),
1090
- Route.http(addHeader2),
1091
- )
1092
-
1093
- const mockApp = Effect.succeed(HttpServerResponse.text("data"))
1094
- const composed = layer.httpMiddleware!(mockApp) as Effect.Effect<
1095
- HttpServerResponse.HttpServerResponse,
1096
- never,
1097
- never
1098
- >
1099
- const result = await Effect.runPromise(composed.pipe(Effect.orDie))
1100
-
1101
- t.expect(result.headers["x-custom-1"]).toBe("value1")
1102
- t.expect(result.headers["x-custom-2"]).toBe("value2")
1103
- })
1104
-
1105
- t.it("Route.matches returns true for exact method and media match", () => {
1106
- const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
1107
- const route2 = Route.get(Route.html(Effect.succeed("<div>other</div>")))
1108
-
1109
- t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(true)
1110
- })
1111
-
1112
- t.it("Route.matches returns false for different methods", () => {
1113
- const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
1114
- const route2 = Route.post(Route.html(Effect.succeed("<div>other</div>")))
1115
-
1116
- t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(false)
1117
- })
1118
-
1119
- t.it("Route.matches returns false for different media types", () => {
1120
- const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
1121
- const route2 = Route.get(Route.json({ data: "test" }))
1122
-
1123
- t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(false)
1124
- })
1125
-
1126
- t.it("Route.matches returns true when method is wildcard", () => {
1127
- const route1 = Route.html(Effect.succeed("<div>test</div>"))
1128
- const route2 = Route.get(Route.html(Effect.succeed("<div>other</div>")))
1129
-
1130
- t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(true)
1131
- t.expect(Route.matches(route2.set[0]!, route1.set[0]!)).toBe(true)
1132
- })
1133
-
1134
- t.it("Route.matches returns true when one route has wildcard method", () => {
1135
- const wildcardRoute = Route.html(Effect.succeed("<div>test</div>"))
1136
- const specificRoute = Route.get(
1137
- Route.html(Effect.succeed("<div>other</div>")),
1138
- )
1139
-
1140
- t.expect(Route.matches(wildcardRoute.set[0]!, specificRoute.set[0]!)).toBe(
1141
- true,
1142
- )
1143
- })
1144
-
1145
- t.describe("Route.merge", () => {
1146
- t.it("types merged routes with union of methods", () => {
1147
- const textRoute = Route.text("hello")
1148
-
1149
- const htmlRoute = Route.html(Effect.succeed("<div>world</div>"))
1150
-
1151
- const merged = Route.merge(textRoute, htmlRoute)
1152
-
1153
- type Expected = Route.RouteSet<
1154
- [
1155
- Route.Route<
1156
- "GET",
1157
- "text/plain" | "text/html",
1158
- Route.RouteHandler<HttpServerResponse.HttpServerResponse>
1159
- >,
1160
- ]
1161
- >
1162
-
1163
- Function.satisfies<Expected>()(merged)
1164
- })
1165
-
1166
- t.it("types merged routes with different methods", () => {
1167
- const getRoute = Route.get(Route.text("get"))
1168
- const postRoute = Route.post(Route.json({ ok: true }))
1169
-
1170
- const merged = Route.merge(getRoute, postRoute)
1171
-
1172
- type Expected = Route.RouteSet<
1173
- [
1174
- Route.Route<
1175
- "GET" | "POST",
1176
- "text/plain" | "application/json",
1177
- Route.RouteHandler<HttpServerResponse.HttpServerResponse>
1178
- >,
1179
- ]
1180
- >
1181
-
1182
- Function.satisfies<Expected>()(merged)
1183
- })
1184
-
1185
- t.it("types merged schemas using MergeSchemas", () => {
1186
- const routeA = Route
1187
- .schemaPathParams({ id: Schema.NumberFromString })
1188
- .text(Effect.succeed("a"))
1189
-
1190
- const routeB = Route
1191
- .schemaUrlParams({ page: Schema.NumberFromString })
1192
- .html(Effect.succeed("<div>b</div>"))
1193
-
1194
- const merged = Route.merge(routeA, routeB)
1195
-
1196
- type MergedSchemas = typeof merged.schema
1197
-
1198
- type ExpectedPathParams = {
1199
- readonly id: typeof Schema.NumberFromString
1200
- }
1201
- type ExpectedUrlParams = {
1202
- readonly page: typeof Schema.NumberFromString
1203
- }
1204
-
1205
- type CheckPathParams = MergedSchemas["PathParams"] extends
1206
- Schema.Struct<ExpectedPathParams> ? true : false
1207
- type CheckUrlParams = MergedSchemas["UrlParams"] extends
1208
- Schema.Struct<ExpectedUrlParams> ? true : false
1209
-
1210
- const _pathParamsCheck: CheckPathParams = true
1211
- const _urlParamsCheck: CheckUrlParams = true
1212
- })
1213
-
1214
- t.it("merged route does content negotiation for text/plain", async () => {
1215
- const textRoute = Route.text("plain text")
1216
- const htmlRoute = Route.html("<div>html</div>")
1217
-
1218
- const merged = Route.merge(textRoute, htmlRoute)
1219
- const route = merged.set[0]!
1220
-
1221
- const request = HttpServerRequest.fromWeb(
1222
- new Request("http://localhost/test", {
1223
- headers: { Accept: "text/plain" },
1224
- }),
1225
- )
1226
-
1227
- const context: Route.RouteContext = {
1228
- request,
1229
- get url() {
1230
- return new URL(request.url)
1231
- },
1232
- slots: {},
1233
- next: () => Effect.void,
1234
- }
1235
-
1236
- const result = await Effect.runPromise(route.handler(context))
1237
-
1238
- const webResponse = HttpServerResponse.toWeb(result)
1239
- const text = await webResponse.text()
1240
-
1241
- t.expect(text).toBe("plain text")
1242
- t.expect(result.headers["content-type"]).toBe("text/plain")
1243
- })
1244
-
1245
- t.it("merged route does content negotiation for text/html", async () => {
1246
- const textRoute = Route.text("plain text")
1247
- const htmlRoute = Route.html("<div>html</div>")
1248
-
1249
- const merged = Route.merge(textRoute, htmlRoute)
1250
- const route = merged.set[0]!
1251
-
1252
- const request = HttpServerRequest.fromWeb(
1253
- new Request("http://localhost/test", {
1254
- headers: { Accept: "text/html" },
1255
- }),
1256
- )
1257
-
1258
- const context: Route.RouteContext = {
1259
- request,
1260
- get url() {
1261
- return new URL(request.url)
1262
- },
1263
- slots: {},
1264
- next: () => Effect.void,
1265
- }
1266
-
1267
- const result = await Effect.runPromise(route.handler(context))
1268
-
1269
- const webResponse = HttpServerResponse.toWeb(result)
1270
- const text = await webResponse.text()
1271
-
1272
- t.expect(text).toBe("<div>html</div>")
1273
- t.expect(result.headers["content-type"]).toContain("text/html")
1274
- })
1275
-
1276
- t.it(
1277
- "merged route does content negotiation for application/json",
1278
- async () => {
1279
- const textRoute = Route.text("plain text")
1280
- const jsonRoute = Route.json({ message: "json" })
1281
-
1282
- const merged = Route.merge(textRoute, jsonRoute)
1283
- const route = merged.set[0]!
1284
-
1285
- const request = HttpServerRequest.fromWeb(
1286
- new Request("http://localhost/test", {
1287
- headers: { Accept: "application/json" },
1288
- }),
1289
- )
1290
-
1291
- const context: Route.RouteContext = {
1292
- request,
1293
- get url() {
1294
- return new URL(request.url)
1295
- },
1296
- slots: {},
1297
- next: () => Effect.void,
1298
- }
1299
-
1300
- const result = await Effect.runPromise(route.handler(context))
1301
-
1302
- const webResponse = HttpServerResponse.toWeb(result)
1303
- const text = await webResponse.text()
1304
-
1305
- t.expect(text).toBe("{\"message\":\"json\"}")
1306
- t.expect(result.headers["content-type"]).toContain("application/json")
1307
- },
1308
- )
1309
-
1310
- t.it("merged route defaults to html for */* accept", async () => {
1311
- const textRoute = Route.text("plain text")
1312
- const htmlRoute = Route.html("<div>html</div>")
1313
-
1314
- const merged = Route.merge(textRoute, htmlRoute)
1315
- const route = merged.set[0]!
1316
-
1317
- const request = HttpServerRequest.fromWeb(
1318
- new Request("http://localhost/test", {
1319
- headers: { Accept: "*/*" },
1320
- }),
1321
- )
1322
-
1323
- const context: Route.RouteContext = {
1324
- request,
1325
- get url() {
1326
- return new URL(request.url)
1327
- },
1328
- slots: {},
1329
- next: () => Effect.void,
1330
- }
1331
-
1332
- const result = await Effect.runPromise(route.handler(context))
1333
-
1334
- const webResponse = HttpServerResponse.toWeb(result)
1335
- const text = await webResponse.text()
1336
-
1337
- t.expect(text).toBe("<div>html</div>")
1338
- })
1339
-
1340
- t.it(
1341
- "merged route defaults to first route when no Accept header",
1342
- async () => {
1343
- const textRoute = Route.text("plain text")
1344
- const htmlRoute = Route.html("<div>html</div>")
1345
-
1346
- const merged = Route.merge(textRoute, htmlRoute)
1347
- const route = merged.set[0]!
1348
-
1349
- const request = HttpServerRequest.fromWeb(
1350
- new Request("http://localhost/test"),
1351
- )
1352
-
1353
- const context: Route.RouteContext = {
1354
- request,
1355
- get url() {
1356
- return new URL(request.url)
1357
- },
1358
- slots: {},
1359
- next: () => Effect.void,
1360
- }
1361
-
1362
- const result = await Effect.runPromise(route.handler(context))
1363
-
1364
- const webResponse = HttpServerResponse.toWeb(result)
1365
- const text = await webResponse.text()
1366
-
1367
- t.expect(text).toBe("<div>html</div>")
1368
- },
1369
- )
1370
- })