effect-start 0.9.0 → 0.11.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.
- package/package.json +15 -14
- package/src/BundleHttp.test.ts +1 -1
- package/src/Commander.test.ts +15 -15
- package/src/Commander.ts +58 -88
- package/src/EncryptedCookies.test.ts +4 -4
- package/src/FileHttpRouter.test.ts +85 -16
- package/src/FileHttpRouter.ts +119 -32
- package/src/FileRouter.ts +62 -166
- package/src/FileRouterCodegen.test.ts +252 -66
- package/src/FileRouterCodegen.ts +13 -56
- package/src/FileRouterPattern.test.ts +116 -0
- package/src/FileRouterPattern.ts +59 -0
- package/src/FileRouter_path.test.ts +63 -102
- package/src/FileSystemExtra.test.ts +226 -0
- package/src/FileSystemExtra.ts +24 -60
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/HttpUtils.test.ts +68 -0
- package/src/HttpUtils.ts +15 -0
- package/src/HyperHtml.ts +24 -5
- package/src/JsModule.test.ts +1 -1
- package/src/NodeFileSystem.ts +764 -0
- package/src/Random.ts +59 -0
- package/src/Route.test.ts +515 -18
- package/src/Route.ts +321 -166
- package/src/RouteRender.ts +40 -0
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +288 -31
- package/src/RouterPattern.test.ts +655 -0
- package/src/RouterPattern.ts +416 -0
- package/src/Start.ts +14 -52
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +259 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +514 -0
- package/src/bun/BunRoute.ts +427 -0
- package/src/bun/BunRoute_bundles.test.ts +218 -0
- package/src/bun/BunRuntime.ts +33 -0
- package/src/bun/BunTailwindPlugin.test.ts +1 -1
- package/src/bun/_empty.html +1 -0
- package/src/bun/index.ts +2 -1
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
- package/src/testing.ts +12 -3
- package/src/Datastar.test.ts +0 -267
- package/src/Datastar.ts +0 -68
- package/src/bun/BunFullstackServer.ts +0 -45
- package/src/bun/BunFullstackServer_httpServer.ts +0 -541
- package/src/jsx-datastar.d.ts +0 -63
package/src/Random.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function uuid(): string {
|
|
2
|
+
return base36(uuid4bytes())
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function uuidSorted(): string {
|
|
6
|
+
return base36(uuid7bytes())
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function token(length: number): string {
|
|
10
|
+
return base36(bytes(length))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function base36(bytes: Uint8Array): string {
|
|
14
|
+
if (bytes.length === 0) return ""
|
|
15
|
+
let zeros = 0
|
|
16
|
+
while (zeros < bytes.length && bytes[zeros] === 0) zeros++
|
|
17
|
+
|
|
18
|
+
let n = 0n
|
|
19
|
+
for (let i = zeros; i < bytes.length; i++) n = (n << 8n) + BigInt(bytes[i])
|
|
20
|
+
|
|
21
|
+
return "0".repeat(zeros) + (n === 0n ? "" : n.toString(36))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function bytes(length: number): Uint8Array {
|
|
25
|
+
const buf = new Uint8Array(length)
|
|
26
|
+
crypto.getRandomValues(buf)
|
|
27
|
+
return buf
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function uuid4bytes(): Uint8Array {
|
|
31
|
+
const buf = bytes(16)
|
|
32
|
+
buf[6] = (buf[6] & 0x0f) | 0x40 // version 4
|
|
33
|
+
buf[8] = (buf[8] & 0x3f) | 0x80 // variant
|
|
34
|
+
|
|
35
|
+
return buf
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function uuid7bytes(): Uint8Array {
|
|
39
|
+
const buf = new Uint8Array(16)
|
|
40
|
+
const timestamp = BigInt(Date.now())
|
|
41
|
+
|
|
42
|
+
// 48-bit timestamp (6 bytes)
|
|
43
|
+
buf[0] = Number((timestamp >> 40n) & 0xffn)
|
|
44
|
+
buf[1] = Number((timestamp >> 32n) & 0xffn)
|
|
45
|
+
buf[2] = Number((timestamp >> 24n) & 0xffn)
|
|
46
|
+
buf[3] = Number((timestamp >> 16n) & 0xffn)
|
|
47
|
+
buf[4] = Number((timestamp >> 8n) & 0xffn)
|
|
48
|
+
buf[5] = Number(timestamp & 0xffn)
|
|
49
|
+
|
|
50
|
+
// 12-bit random A (1.5 bytes)
|
|
51
|
+
crypto.getRandomValues(buf.subarray(6, 8))
|
|
52
|
+
buf[6] = (buf[6] & 0x0f) | 0x70 // version 7
|
|
53
|
+
|
|
54
|
+
// 2-bit variant + 62-bit random B (8 bytes)
|
|
55
|
+
crypto.getRandomValues(buf.subarray(8, 16))
|
|
56
|
+
buf[8] = (buf[8] & 0x3f) | 0x80 // variant
|
|
57
|
+
|
|
58
|
+
return buf
|
|
59
|
+
}
|
package/src/Route.test.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
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"
|
|
1
5
|
import * as t from "bun:test"
|
|
6
|
+
|
|
2
7
|
import * as Effect from "effect/Effect"
|
|
3
8
|
import * as Function from "effect/Function"
|
|
4
9
|
import * as Schema from "effect/Schema"
|
|
@@ -467,6 +472,37 @@ t.it("context has only request and url when no schemas provided", () => {
|
|
|
467
472
|
)
|
|
468
473
|
})
|
|
469
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
|
+
|
|
470
506
|
t.it("schemas work with all media types", () => {
|
|
471
507
|
const PathSchema = Schema.Struct({
|
|
472
508
|
id: Schema.String,
|
|
@@ -474,23 +510,19 @@ t.it("schemas work with all media types", () => {
|
|
|
474
510
|
|
|
475
511
|
Route
|
|
476
512
|
.schemaPathParams(PathSchema)
|
|
477
|
-
.html(
|
|
478
|
-
(context)
|
|
479
|
-
Function.satisfies<string>()(context.pathParams.id)
|
|
513
|
+
.html((context) => {
|
|
514
|
+
Function.satisfies<string>()(context.pathParams.id)
|
|
480
515
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
)
|
|
516
|
+
return Effect.succeed("<h1>Hello</h1>")
|
|
517
|
+
})
|
|
484
518
|
|
|
485
519
|
Route
|
|
486
520
|
.schemaPathParams(PathSchema)
|
|
487
|
-
.json(
|
|
488
|
-
(context)
|
|
489
|
-
Function.satisfies<string>()(context.pathParams.id)
|
|
521
|
+
.json((context) => {
|
|
522
|
+
Function.satisfies<string>()(context.pathParams.id)
|
|
490
523
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
)
|
|
524
|
+
return Effect.succeed({ message: "hello" })
|
|
525
|
+
})
|
|
494
526
|
})
|
|
495
527
|
|
|
496
528
|
t.it("schemas work with generator functions", () => {
|
|
@@ -500,13 +532,11 @@ t.it("schemas work with generator functions", () => {
|
|
|
500
532
|
|
|
501
533
|
Route
|
|
502
534
|
.schemaPathParams(IdSchema)
|
|
503
|
-
.text(
|
|
504
|
-
|
|
505
|
-
Function.satisfies<string>()(context.pathParams.id)
|
|
535
|
+
.text(function*(context) {
|
|
536
|
+
Function.satisfies<string>()(context.pathParams.id)
|
|
506
537
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
)
|
|
538
|
+
return "hello"
|
|
539
|
+
})
|
|
510
540
|
})
|
|
511
541
|
|
|
512
542
|
t.it("schema property is correctly set on RouteSet", () => {
|
|
@@ -871,3 +901,470 @@ t.it("schemaHeaders accepts string and string array encoded schemas", () => {
|
|
|
871
901
|
})
|
|
872
902
|
.text(Effect.succeed("ok"))
|
|
873
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
|
+
})
|