effect-start 0.15.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.
- package/package.json +1 -1
- package/src/ContentNegotiation.test.ts +103 -0
- package/src/ContentNegotiation.ts +10 -3
- package/src/Entity.test.ts +592 -0
- package/src/Entity.ts +362 -0
- package/src/Http.test.ts +315 -20
- package/src/Http.ts +153 -11
- package/src/PathPattern.ts +3 -1
- package/src/Route.ts +22 -10
- package/src/RouteBody.test.ts +81 -29
- package/src/RouteBody.ts +122 -35
- package/src/RouteHook.ts +15 -14
- package/src/RouteHttp.test.ts +2546 -83
- package/src/RouteHttp.ts +321 -113
- package/src/RouteHttpTracer.ts +92 -0
- package/src/RouteMount.test.ts +23 -10
- package/src/RouteMount.ts +161 -4
- package/src/RouteSchema.test.ts +346 -0
- package/src/RouteSchema.ts +386 -7
- package/src/RouteTree.test.ts +233 -85
- package/src/RouteTree.ts +98 -44
- package/src/StreamExtra.ts +21 -1
- package/src/Values.test.ts +263 -0
- package/src/Values.ts +60 -0
- package/src/bun/BunHttpServer.ts +23 -7
- package/src/bun/BunRoute.test.ts +162 -0
- package/src/bun/BunRoute.ts +146 -105
- package/src/index.ts +1 -0
- package/src/testing/TestLogger.test.ts +0 -3
- package/src/testing/TestLogger.ts +15 -9
package/src/Http.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as Values from "./Values.ts"
|
|
2
|
+
|
|
1
3
|
export type Method =
|
|
2
4
|
| "GET"
|
|
3
5
|
| "POST"
|
|
@@ -7,19 +9,159 @@ export type Method =
|
|
|
7
9
|
| "HEAD"
|
|
8
10
|
| "OPTIONS"
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
type Respondable =
|
|
13
|
+
| Response
|
|
14
|
+
| Promise<Response>
|
|
15
|
+
|
|
16
|
+
export type WebHandler = (request: Request) => Respondable
|
|
11
17
|
|
|
12
|
-
export
|
|
18
|
+
export type WebMiddleware = (
|
|
13
19
|
request: Request,
|
|
14
|
-
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
next: WebHandler,
|
|
21
|
+
) => Respondable
|
|
22
|
+
|
|
23
|
+
export function fetch(
|
|
24
|
+
handler: WebHandler,
|
|
25
|
+
init:
|
|
26
|
+
& Omit<RequestInit, "body">
|
|
27
|
+
& (
|
|
28
|
+
| { url: string }
|
|
29
|
+
| { path: `/${string}` }
|
|
30
|
+
)
|
|
31
|
+
& { body?: RequestInit["body"] | Record<string, unknown> },
|
|
32
|
+
): Promise<Response> {
|
|
33
|
+
const url = "path" in init
|
|
34
|
+
? `http://localhost${init.path}`
|
|
35
|
+
: init.url
|
|
36
|
+
|
|
37
|
+
const isPlain = Values.isPlainObject(init.body)
|
|
38
|
+
|
|
39
|
+
const headers = new Headers(init.headers)
|
|
40
|
+
if (isPlain && !headers.has("Content-Type")) {
|
|
41
|
+
headers.set("Content-Type", "application/json")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const body = isPlain ? JSON.stringify(init.body) : init.body
|
|
45
|
+
|
|
46
|
+
const request = new Request(url, {
|
|
47
|
+
...init,
|
|
48
|
+
headers,
|
|
49
|
+
body: body as BodyInit,
|
|
20
50
|
})
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
return Promise.resolve(handler(request))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createAbortableRequest(
|
|
55
|
+
init:
|
|
56
|
+
& Omit<RequestInit, "signal">
|
|
57
|
+
& (
|
|
58
|
+
| { url: string }
|
|
59
|
+
| { path: `/${string}` }
|
|
60
|
+
),
|
|
61
|
+
): { request: Request; abort: () => void } {
|
|
62
|
+
const url = "path" in init
|
|
63
|
+
? `http://localhost${init.path}`
|
|
64
|
+
: init.url
|
|
65
|
+
const controller = new AbortController()
|
|
66
|
+
const request = new Request(url, { ...init, signal: controller.signal })
|
|
67
|
+
return { request, abort: () => controller.abort() }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function mapHeaders(
|
|
71
|
+
headers: Headers,
|
|
72
|
+
): Record<string, string | undefined> {
|
|
73
|
+
const result: Record<string, string | undefined> = {}
|
|
74
|
+
headers.forEach((value, key) => {
|
|
75
|
+
result[key.toLowerCase()] = value
|
|
76
|
+
})
|
|
77
|
+
return result
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function parseCookies(
|
|
81
|
+
cookieHeader: string | null,
|
|
82
|
+
): Record<string, string | undefined> {
|
|
83
|
+
if (!cookieHeader) return {}
|
|
84
|
+
const result: Record<string, string | undefined> = {}
|
|
85
|
+
for (const part of cookieHeader.split(";")) {
|
|
86
|
+
const idx = part.indexOf("=")
|
|
87
|
+
if (idx === -1) {
|
|
88
|
+
// Cookie without value (e.g., "name" or just whitespace)
|
|
89
|
+
const key = part.trim()
|
|
90
|
+
if (key) {
|
|
91
|
+
result[key] = undefined
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
const key = part.slice(0, idx).trim()
|
|
95
|
+
const value = part.slice(idx + 1).trim()
|
|
96
|
+
if (key) {
|
|
97
|
+
result[key] = value
|
|
98
|
+
}
|
|
99
|
+
}
|
|
23
100
|
}
|
|
24
|
-
return
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function mapUrlSearchParams(
|
|
105
|
+
params: URLSearchParams,
|
|
106
|
+
): Record<string, string | ReadonlyArray<string> | undefined> {
|
|
107
|
+
const result: Record<string, string | ReadonlyArray<string> | undefined> = {}
|
|
108
|
+
for (const key of new Set(params.keys())) {
|
|
109
|
+
const values = params.getAll(key)
|
|
110
|
+
result[key] = values.length === 1 ? values[0] : values
|
|
111
|
+
}
|
|
112
|
+
return result
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface FilePart {
|
|
116
|
+
readonly _tag: "File"
|
|
117
|
+
readonly key: string
|
|
118
|
+
readonly name: string
|
|
119
|
+
readonly contentType: string
|
|
120
|
+
readonly content: Uint8Array
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface FieldPart {
|
|
124
|
+
readonly _tag: "Field"
|
|
125
|
+
readonly key: string
|
|
126
|
+
readonly value: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type MultipartPart = FilePart | FieldPart
|
|
130
|
+
|
|
131
|
+
export async function parseFormData(
|
|
132
|
+
request: Request,
|
|
133
|
+
): Promise<
|
|
134
|
+
Record<string, ReadonlyArray<FilePart> | ReadonlyArray<string> | string>
|
|
135
|
+
> {
|
|
136
|
+
const formData = await request.formData()
|
|
137
|
+
const result: Record<
|
|
138
|
+
string,
|
|
139
|
+
ReadonlyArray<FilePart> | ReadonlyArray<string> | string
|
|
140
|
+
> = {}
|
|
141
|
+
|
|
142
|
+
for (const key of new Set(formData.keys())) {
|
|
143
|
+
const values = formData.getAll(key)
|
|
144
|
+
const first = values[0]
|
|
145
|
+
|
|
146
|
+
if (typeof first === "string") {
|
|
147
|
+
result[key] = values.length === 1 ? first : (values as string[])
|
|
148
|
+
} else {
|
|
149
|
+
const files: FilePart[] = []
|
|
150
|
+
for (const value of values) {
|
|
151
|
+
if (typeof value !== "string") {
|
|
152
|
+
const content = new Uint8Array(await value.arrayBuffer())
|
|
153
|
+
files.push({
|
|
154
|
+
_tag: "File",
|
|
155
|
+
key,
|
|
156
|
+
name: value.name,
|
|
157
|
+
contentType: value.type || "application/octet-stream",
|
|
158
|
+
content,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
result[key] = files
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result
|
|
25
167
|
}
|
package/src/PathPattern.ts
CHANGED
|
@@ -477,7 +477,9 @@ export function toBun(path: string): PathPattern[] {
|
|
|
477
477
|
: `:${optionalName}`
|
|
478
478
|
|
|
479
479
|
const withOptionalSegments = [...before, requiredOptional, ...after]
|
|
480
|
-
const withOptionalPath: PathPattern = `/${
|
|
480
|
+
const withOptionalPath: PathPattern = `/${
|
|
481
|
+
withOptionalSegments.map(formatSegment).join("/")
|
|
482
|
+
}`
|
|
481
483
|
|
|
482
484
|
return [...toBun(basePath), ...toBun(withOptionalPath)]
|
|
483
485
|
}
|
package/src/Route.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type * as Effect from "effect/Effect"
|
|
|
3
3
|
import * as Layer from "effect/Layer"
|
|
4
4
|
import * as Pipeable from "effect/Pipeable"
|
|
5
5
|
import * as Predicate from "effect/Predicate"
|
|
6
|
+
import type * as Entity from "./Entity.ts"
|
|
6
7
|
import * as RouteBody from "./RouteBody.ts"
|
|
7
8
|
import * as RouteTree from "./RouteTree.ts"
|
|
8
9
|
import * as Values from "./Values.ts"
|
|
@@ -99,14 +100,8 @@ export namespace Route {
|
|
|
99
100
|
|
|
100
101
|
export type Handler<B, A, E, R> = (
|
|
101
102
|
context: B,
|
|
102
|
-
next: (context
|
|
103
|
-
) => Effect.Effect<A
|
|
104
|
-
|
|
105
|
-
// handler that cannot modify the context
|
|
106
|
-
export type HandlerImmutable<B, A, E, R> = (
|
|
107
|
-
context: B,
|
|
108
|
-
next: () => Effect.Effect<A>,
|
|
109
|
-
) => Effect.Effect<A, E, R>
|
|
103
|
+
next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<A>,
|
|
104
|
+
) => Effect.Effect<Entity.Entity<A>, E, R>
|
|
110
105
|
|
|
111
106
|
/**
|
|
112
107
|
* Extracts only the bindings (B) from routes, excluding descriptors.
|
|
@@ -246,8 +241,21 @@ export function descriptor<
|
|
|
246
241
|
T extends RouteSet.Data<any, any, any>,
|
|
247
242
|
>(
|
|
248
243
|
self: T,
|
|
249
|
-
): T[typeof RouteDescriptor]
|
|
250
|
-
|
|
244
|
+
): T[typeof RouteDescriptor]
|
|
245
|
+
export function descriptor<
|
|
246
|
+
T extends RouteSet.Data<any, any, any>,
|
|
247
|
+
>(
|
|
248
|
+
self: Iterable<T>,
|
|
249
|
+
): T[typeof RouteDescriptor][]
|
|
250
|
+
export function descriptor(
|
|
251
|
+
self:
|
|
252
|
+
| RouteSet.Data<any, any, any>
|
|
253
|
+
| Iterable<RouteSet.Data<any, any, any>>,
|
|
254
|
+
): RouteDescriptor.Any | RouteDescriptor.Any[] {
|
|
255
|
+
if (RouteDescriptor in self) {
|
|
256
|
+
return self[RouteDescriptor]
|
|
257
|
+
}
|
|
258
|
+
return [...self].map((r) => r[RouteDescriptor])
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
export type ExtractBindings<
|
|
@@ -311,6 +319,10 @@ export const bytes = RouteBody.build<Uint8Array, "bytes">({
|
|
|
311
319
|
format: "bytes",
|
|
312
320
|
})
|
|
313
321
|
|
|
322
|
+
export {
|
|
323
|
+
render,
|
|
324
|
+
} from "./RouteBody.ts"
|
|
325
|
+
|
|
314
326
|
export class Routes extends Context.Tag("effect-start/Routes")<
|
|
315
327
|
Routes,
|
|
316
328
|
RouteTree.RouteTree
|
package/src/RouteBody.test.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import * as test from "bun:test"
|
|
2
2
|
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Stream from "effect/Stream"
|
|
4
|
+
import * as Entity from "./Entity.ts"
|
|
5
|
+
import * as Route from "./Route.ts"
|
|
3
6
|
import * as RouteBody from "./RouteBody.ts"
|
|
4
7
|
import * as RouteMount from "./RouteMount.ts"
|
|
5
8
|
|
|
6
|
-
const text = RouteBody.build<string, "text">({
|
|
7
|
-
format: "text",
|
|
8
|
-
})
|
|
9
|
-
|
|
10
9
|
test.it("infers parent descriptions", () => {
|
|
11
10
|
RouteMount.get(
|
|
12
|
-
text((ctx) =>
|
|
11
|
+
Route.text((ctx) =>
|
|
13
12
|
Effect.gen(function*() {
|
|
14
13
|
test
|
|
15
14
|
.expectTypeOf(ctx)
|
|
@@ -24,13 +23,12 @@ test.it("infers parent descriptions", () => {
|
|
|
24
23
|
)
|
|
25
24
|
})
|
|
26
25
|
|
|
27
|
-
test.it("
|
|
28
|
-
text((ctx, next) =>
|
|
26
|
+
test.it("next is function returning Entity", () => {
|
|
27
|
+
Route.text((ctx, next) =>
|
|
29
28
|
Effect.gen(function*() {
|
|
30
29
|
test
|
|
31
30
|
.expectTypeOf(next)
|
|
32
|
-
.
|
|
33
|
-
.toEqualTypeOf<[]>()
|
|
31
|
+
.toExtend<() => Entity.Entity<string>>()
|
|
34
32
|
|
|
35
33
|
return "Hello, world!"
|
|
36
34
|
})
|
|
@@ -39,24 +37,66 @@ test.it("cannot modify context", () => {
|
|
|
39
37
|
|
|
40
38
|
test.it("enforces result value", () => {
|
|
41
39
|
// @ts-expect-error must return string
|
|
42
|
-
text((ctx, next) =>
|
|
40
|
+
Route.text((ctx, next) =>
|
|
43
41
|
Effect.gen(function*() {
|
|
44
42
|
return 1337
|
|
45
43
|
})
|
|
46
44
|
)
|
|
47
45
|
})
|
|
48
46
|
|
|
47
|
+
test.it("accepts text stream", () => {
|
|
48
|
+
RouteMount.get(
|
|
49
|
+
Route.text((ctx) =>
|
|
50
|
+
Effect.gen(function*() {
|
|
51
|
+
test
|
|
52
|
+
.expectTypeOf(ctx)
|
|
53
|
+
.toExtend<{
|
|
54
|
+
method: "GET"
|
|
55
|
+
format: "text"
|
|
56
|
+
}>()
|
|
57
|
+
|
|
58
|
+
return Stream.make("Hello", " ", "world!")
|
|
59
|
+
})
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test.it("accepts Effect<Stream<string>> for html format", () => {
|
|
65
|
+
RouteMount.get(
|
|
66
|
+
Route.html(function*() {
|
|
67
|
+
return Stream.make("<div>", "content", "</div>")
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test.it("accepts Effect<Stream<Uint8Array>> for bytes format", () => {
|
|
73
|
+
const encoder = new TextEncoder()
|
|
74
|
+
|
|
75
|
+
RouteMount.get(
|
|
76
|
+
Route.bytes(function*() {
|
|
77
|
+
return Stream.make(encoder.encode("chunk"))
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test.it("rejects Stream for json format", () => {
|
|
83
|
+
// @ts-expect-error Stream not allowed for json format
|
|
84
|
+
Route.json(function*() {
|
|
85
|
+
return Stream.make({ msg: "hello" })
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
49
89
|
test.it("accepts value directly", () => {
|
|
50
90
|
const value = "Hello, world!"
|
|
51
91
|
|
|
52
92
|
test
|
|
53
|
-
.expectTypeOf(text)
|
|
93
|
+
.expectTypeOf(Route.text)
|
|
54
94
|
.toBeCallableWith(value)
|
|
55
95
|
})
|
|
56
96
|
|
|
57
97
|
test.describe(`${RouteBody.handle.name}()`, () => {
|
|
58
98
|
const ctx = {}
|
|
59
|
-
const next = () => Effect.succeed("next"
|
|
99
|
+
const next = () => Entity.effect(Effect.succeed(Entity.make("next")))
|
|
60
100
|
|
|
61
101
|
test.it("accepts all HandlerInput variants", () => {
|
|
62
102
|
test
|
|
@@ -73,11 +113,12 @@ test.describe(`${RouteBody.handle.name}()`, () => {
|
|
|
73
113
|
.expectTypeOf(handler)
|
|
74
114
|
.returns
|
|
75
115
|
.toEqualTypeOf<
|
|
76
|
-
Effect.Effect<string
|
|
116
|
+
Effect.Effect<Entity.Entity<string>, never, never>
|
|
77
117
|
>()
|
|
78
118
|
|
|
79
119
|
const result = await Effect.runPromise(handler(ctx, next))
|
|
80
|
-
test.expect(result).toBe("hello")
|
|
120
|
+
test.expect(result.body).toBe("hello")
|
|
121
|
+
test.expect(result.status).toBe(200)
|
|
81
122
|
})
|
|
82
123
|
|
|
83
124
|
test.it("handles Effect directly", async () => {
|
|
@@ -87,13 +128,13 @@ test.describe(`${RouteBody.handle.name}()`, () => {
|
|
|
87
128
|
.expectTypeOf(handler)
|
|
88
129
|
.returns
|
|
89
130
|
.toEqualTypeOf<
|
|
90
|
-
Effect.Effect<string
|
|
131
|
+
Effect.Effect<Entity.Entity<string>, never, never>
|
|
91
132
|
>()
|
|
92
133
|
|
|
93
134
|
const result = await Effect.runPromise(handler(ctx, next))
|
|
94
135
|
|
|
95
136
|
test
|
|
96
|
-
.expect(result)
|
|
137
|
+
.expect(result.body)
|
|
97
138
|
.toBe("from effect")
|
|
98
139
|
})
|
|
99
140
|
|
|
@@ -104,7 +145,7 @@ test.describe(`${RouteBody.handle.name}()`, () => {
|
|
|
104
145
|
.expectTypeOf(handler)
|
|
105
146
|
.returns
|
|
106
147
|
.toEqualTypeOf<
|
|
107
|
-
Effect.Effect<never
|
|
148
|
+
Effect.Effect<Entity.Entity<never>, Error, never>
|
|
108
149
|
>()
|
|
109
150
|
})
|
|
110
151
|
|
|
@@ -117,21 +158,27 @@ test.describe(`${RouteBody.handle.name}()`, () => {
|
|
|
117
158
|
.expectTypeOf(handler)
|
|
118
159
|
.parameters
|
|
119
160
|
.toEqualTypeOf<
|
|
120
|
-
[
|
|
161
|
+
[
|
|
162
|
+
{ id: number },
|
|
163
|
+
(
|
|
164
|
+
context?: Partial<{ id: number }> & Record<string, unknown>,
|
|
165
|
+
) => Entity.Entity<number>,
|
|
166
|
+
]
|
|
121
167
|
>()
|
|
122
168
|
test
|
|
123
169
|
.expectTypeOf(handler)
|
|
124
170
|
.returns
|
|
125
171
|
.toEqualTypeOf<
|
|
126
|
-
Effect.Effect<number
|
|
172
|
+
Effect.Effect<Entity.Entity<number>, never, never>
|
|
127
173
|
>()
|
|
128
174
|
|
|
175
|
+
const numNext = () => Entity.effect(Effect.succeed(Entity.make(23)))
|
|
129
176
|
const result = await Effect.runPromise(
|
|
130
|
-
handler({ id: 42 },
|
|
177
|
+
handler({ id: 42 }, numNext),
|
|
131
178
|
)
|
|
132
179
|
|
|
133
180
|
test
|
|
134
|
-
.expect(result)
|
|
181
|
+
.expect(result.body)
|
|
135
182
|
.toBe(42)
|
|
136
183
|
})
|
|
137
184
|
|
|
@@ -145,30 +192,35 @@ test.describe(`${RouteBody.handle.name}()`, () => {
|
|
|
145
192
|
.expectTypeOf(handler)
|
|
146
193
|
.parameters
|
|
147
194
|
.toEqualTypeOf<
|
|
148
|
-
[
|
|
195
|
+
[
|
|
196
|
+
{ id: number },
|
|
197
|
+
(
|
|
198
|
+
context?: Partial<{ id: number }> & Record<string, unknown>,
|
|
199
|
+
) => Entity.Entity<number>,
|
|
200
|
+
]
|
|
149
201
|
>()
|
|
150
202
|
|
|
151
203
|
test
|
|
152
204
|
.expectTypeOf(handler)
|
|
153
205
|
.returns
|
|
154
206
|
.toEqualTypeOf<
|
|
155
|
-
Effect.Effect<number
|
|
207
|
+
Effect.Effect<Entity.Entity<number>, never, never>
|
|
156
208
|
>()
|
|
157
209
|
|
|
210
|
+
const numNext = () => Entity.effect(Effect.succeed(Entity.make(23)))
|
|
158
211
|
const result = await Effect.runPromise(
|
|
159
|
-
|
|
160
|
-
handler({ id: 21 }, () => Effect.succeed(23)),
|
|
212
|
+
handler({ id: 21 }, numNext),
|
|
161
213
|
)
|
|
162
214
|
|
|
163
215
|
test
|
|
164
|
-
.expect(result)
|
|
216
|
+
.expect(result.body)
|
|
165
217
|
.toBe(42)
|
|
166
218
|
})
|
|
167
219
|
|
|
168
220
|
test.it("generator can call next", async () => {
|
|
169
221
|
const handler = RouteBody.handle(
|
|
170
|
-
function*(_ctx: {}, next: () =>
|
|
171
|
-
const fromNext = yield* next()
|
|
222
|
+
function*(_ctx: {}, next: () => Entity.Entity<string>) {
|
|
223
|
+
const fromNext = yield* next().text
|
|
172
224
|
return `got: ${fromNext}`
|
|
173
225
|
},
|
|
174
226
|
)
|
|
@@ -176,7 +228,7 @@ test.describe(`${RouteBody.handle.name}()`, () => {
|
|
|
176
228
|
const result = await Effect.runPromise(handler(ctx, next))
|
|
177
229
|
|
|
178
230
|
test
|
|
179
|
-
.expect(result)
|
|
231
|
+
.expect(result.body)
|
|
180
232
|
.toBe("got: next")
|
|
181
233
|
})
|
|
182
234
|
})
|
package/src/RouteBody.ts
CHANGED
|
@@ -1,52 +1,83 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect"
|
|
2
|
+
import type * as Stream from "effect/Stream"
|
|
2
3
|
import type * as Utils from "effect/Utils"
|
|
4
|
+
import * as Entity from "./Entity.ts"
|
|
3
5
|
import * as Route from "./Route.ts"
|
|
6
|
+
import type * as Values from "./Values.ts"
|
|
4
7
|
|
|
5
8
|
export type Format =
|
|
6
9
|
| "text"
|
|
7
10
|
| "html"
|
|
8
11
|
| "json"
|
|
9
12
|
| "bytes"
|
|
13
|
+
| "*"
|
|
14
|
+
|
|
15
|
+
const formatToContentType: Record<Format, string | undefined> = {
|
|
16
|
+
text: "text/plain; charset=utf-8",
|
|
17
|
+
html: "text/html; charset=utf-8",
|
|
18
|
+
json: "application/json",
|
|
19
|
+
bytes: "application/octet-stream",
|
|
20
|
+
"*": undefined,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type UnwrapStream<T> = T extends Stream.Stream<infer V, any, any> ? V : T
|
|
10
24
|
|
|
11
25
|
export type HandlerInput<B, A, E, R> =
|
|
12
26
|
| A
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
| Entity.Entity<A>
|
|
28
|
+
| Effect.Effect<A | Entity.Entity<A>, E, R>
|
|
29
|
+
| ((
|
|
30
|
+
context: Values.Simplify<B>,
|
|
31
|
+
next: (
|
|
32
|
+
context?: Partial<B> & Record<string, unknown>,
|
|
33
|
+
) => Entity.Entity<UnwrapStream<A>>,
|
|
34
|
+
) =>
|
|
35
|
+
| Effect.Effect<A | Entity.Entity<A>, E, R>
|
|
36
|
+
| Generator<
|
|
37
|
+
Utils.YieldWrap<Effect.Effect<unknown, E, R>>,
|
|
38
|
+
A | Entity.Entity<A>,
|
|
39
|
+
unknown
|
|
40
|
+
>)
|
|
17
41
|
|
|
18
|
-
export function handle<B, A, E, R>(
|
|
19
|
-
handler: (context: B, next: () => Effect.Effect<A>) =>
|
|
20
|
-
| Effect.Effect<A, E, R>
|
|
21
|
-
| Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>,
|
|
22
|
-
): Route.Route.HandlerImmutable<B, A, E, R>
|
|
23
|
-
export function handle<A, E, R>(
|
|
24
|
-
handler: Effect.Effect<A, E, R>,
|
|
25
|
-
): Route.Route.HandlerImmutable<{}, A, E, R>
|
|
26
|
-
export function handle<A>(
|
|
27
|
-
handler: A,
|
|
28
|
-
): Route.Route.HandlerImmutable<{}, A, never, never>
|
|
29
42
|
export function handle<B, A, E, R>(
|
|
30
43
|
handler: HandlerInput<B, A, E, R>,
|
|
31
|
-
): Route.Route.
|
|
44
|
+
): Route.Route.Handler<B, A, E, R> {
|
|
32
45
|
if (typeof handler === "function") {
|
|
33
46
|
return (
|
|
34
47
|
context: B,
|
|
35
|
-
next: (
|
|
36
|
-
|
|
48
|
+
next: (
|
|
49
|
+
context?: Partial<B> & Record<string, unknown>,
|
|
50
|
+
) => Entity.Entity<A>,
|
|
51
|
+
): Effect.Effect<Entity.Entity<A>, E, R> => {
|
|
37
52
|
const result = (handler as Function)(context, next)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const effect = Effect.isEffect(result)
|
|
54
|
+
? result as Effect.Effect<A | Entity.Entity<A>, E, R>
|
|
55
|
+
: Effect.gen(function*() {
|
|
56
|
+
return yield* result
|
|
57
|
+
}) as Effect.Effect<A | Entity.Entity<A>, E, R>
|
|
58
|
+
return Effect.map(effect, normalizeToEntity)
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
61
|
if (Effect.isEffect(handler)) {
|
|
47
|
-
return (_context, _next) =>
|
|
62
|
+
return (_context, _next) =>
|
|
63
|
+
Effect.map(handler, normalizeToEntity) as Effect.Effect<
|
|
64
|
+
Entity.Entity<A>,
|
|
65
|
+
E,
|
|
66
|
+
R
|
|
67
|
+
>
|
|
68
|
+
}
|
|
69
|
+
if (Entity.isEntity(handler)) {
|
|
70
|
+
return (_context, _next) => Effect.succeed(handler as Entity.Entity<A>)
|
|
48
71
|
}
|
|
49
|
-
return (_context, _next) =>
|
|
72
|
+
return (_context, _next) =>
|
|
73
|
+
Effect.succeed(normalizeToEntity(handler as A) as Entity.Entity<A>)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeToEntity<A>(value: A | Entity.Entity<A>): Entity.Entity<A> {
|
|
77
|
+
if (Entity.isEntity(value)) {
|
|
78
|
+
return value as Entity.Entity<A>
|
|
79
|
+
}
|
|
80
|
+
return Entity.make(value as A, { status: 200 })
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
export function build<
|
|
@@ -59,7 +90,8 @@ export function build<
|
|
|
59
90
|
D extends Route.RouteDescriptor.Any,
|
|
60
91
|
B extends {},
|
|
61
92
|
I extends Route.Route.Tuple,
|
|
62
|
-
A extends Value
|
|
93
|
+
A extends F extends "json" ? Value
|
|
94
|
+
: Value | Stream.Stream<Value, any, any>,
|
|
63
95
|
E = never,
|
|
64
96
|
R = never,
|
|
65
97
|
>(
|
|
@@ -75,8 +107,25 @@ export function build<
|
|
|
75
107
|
return function(
|
|
76
108
|
self: Route.RouteSet.RouteSet<D, B, I>,
|
|
77
109
|
) {
|
|
110
|
+
const contentType = formatToContentType[descriptors.format]
|
|
111
|
+
const baseHandler = handle(handler)
|
|
112
|
+
const wrappedHandler: Route.Route.Handler<
|
|
113
|
+
D & B & Route.ExtractBindings<I> & { format: F },
|
|
114
|
+
A,
|
|
115
|
+
E,
|
|
116
|
+
R
|
|
117
|
+
> = (ctx, next) =>
|
|
118
|
+
Effect.map(baseHandler(ctx as any, next as any), (entity) =>
|
|
119
|
+
entity.headers["content-type"]
|
|
120
|
+
? entity
|
|
121
|
+
: Entity.make(entity.body, {
|
|
122
|
+
status: entity.status,
|
|
123
|
+
url: entity.url,
|
|
124
|
+
headers: { ...entity.headers, "content-type": contentType },
|
|
125
|
+
}))
|
|
126
|
+
|
|
78
127
|
const route = Route.make<{ format: F }, {}, A, E, R>(
|
|
79
|
-
|
|
128
|
+
wrappedHandler as any,
|
|
80
129
|
descriptors,
|
|
81
130
|
)
|
|
82
131
|
|
|
@@ -97,10 +146,48 @@ export function build<
|
|
|
97
146
|
}
|
|
98
147
|
}
|
|
99
148
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
149
|
+
export type RenderValue =
|
|
150
|
+
| string
|
|
151
|
+
| Uint8Array
|
|
152
|
+
| Stream.Stream<string | Uint8Array, any, any>
|
|
153
|
+
|
|
154
|
+
export function render<
|
|
155
|
+
D extends Route.RouteDescriptor.Any,
|
|
156
|
+
B extends {},
|
|
157
|
+
I extends Route.Route.Tuple,
|
|
158
|
+
A extends RenderValue,
|
|
159
|
+
E = never,
|
|
160
|
+
R = never,
|
|
161
|
+
>(
|
|
162
|
+
handler: HandlerInput<
|
|
163
|
+
NoInfer<
|
|
164
|
+
D & B & Route.ExtractBindings<I> & { format: "*" }
|
|
165
|
+
>,
|
|
166
|
+
A,
|
|
167
|
+
E,
|
|
168
|
+
R
|
|
169
|
+
>,
|
|
170
|
+
) {
|
|
171
|
+
return function(
|
|
172
|
+
self: Route.RouteSet.RouteSet<D, B, I>,
|
|
173
|
+
) {
|
|
174
|
+
const route = Route.make<{ format: "*" }, {}, A, E, R>(
|
|
175
|
+
handle(handler) as any,
|
|
176
|
+
{ format: "*" },
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const items: [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>] = [
|
|
180
|
+
...Route.items(self),
|
|
181
|
+
route,
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
return Route.set<
|
|
185
|
+
D,
|
|
186
|
+
B,
|
|
187
|
+
[...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>]
|
|
188
|
+
>(
|
|
189
|
+
items,
|
|
190
|
+
Route.descriptor(self),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|