effect-start 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -10
- package/src/FileHttpRouter.test.ts +4 -4
- package/src/FileHttpRouter.ts +6 -8
- package/src/FileRouter.ts +4 -6
- package/src/FileRouterCodegen.test.ts +2 -2
- package/src/HttpAppExtra.test.ts +84 -0
- package/src/HttpAppExtra.ts +399 -47
- package/src/Route.test.ts +59 -33
- package/src/Route.ts +59 -49
- package/src/RouteRender.ts +6 -4
- package/src/Router.test.ts +416 -0
- package/src/Router.ts +279 -0
- package/src/RouterPattern.test.ts +29 -3
- package/src/RouterPattern.ts +30 -5
- package/src/TestHttpClient.test.ts +29 -0
- package/src/TestHttpClient.ts +122 -73
- package/src/assets.d.ts +39 -0
- package/src/bun/BunHttpServer.test.ts +74 -0
- package/src/bun/BunHttpServer.ts +22 -9
- package/src/bun/BunRoute.test.ts +307 -134
- package/src/bun/BunRoute.ts +240 -139
- package/src/bun/BunRoute_bundles.test.ts +181 -181
- package/src/index.ts +14 -14
- package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
- package/src/middlewares/BasicAuthMiddleware.ts +36 -0
package/src/HttpAppExtra.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HttpRouter,
|
|
3
|
+
HttpServerRequest,
|
|
3
4
|
HttpServerResponse,
|
|
4
5
|
} from "@effect/platform"
|
|
5
6
|
import * as HttpApp from "@effect/platform/HttpApp"
|
|
6
7
|
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
8
|
+
|
|
7
9
|
import {
|
|
8
10
|
RequestError,
|
|
9
11
|
RouteNotFound,
|
|
@@ -27,10 +29,242 @@ type GraciousError =
|
|
|
27
29
|
| RequestError
|
|
28
30
|
| ParseResult.ParseError
|
|
29
31
|
|
|
32
|
+
type StackFrame = {
|
|
33
|
+
function: string
|
|
34
|
+
file: string
|
|
35
|
+
type: "application" | "framework" | "node_modules"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ERROR_PAGE_CSS = `
|
|
39
|
+
:root {
|
|
40
|
+
--error-red: #c00;
|
|
41
|
+
--error-red-dark: #a00;
|
|
42
|
+
--bg-error: #fee;
|
|
43
|
+
--bg-light: #f5f5f5;
|
|
44
|
+
--bg-white: #fff;
|
|
45
|
+
--border-color: #ddd;
|
|
46
|
+
--text-dark: #333;
|
|
47
|
+
--text-gray: #666;
|
|
48
|
+
--text-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
* { box-sizing: border-box; }
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
color: var(--text-dark);
|
|
58
|
+
line-height: 1.6;
|
|
59
|
+
min-height: 100dvh;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.error-page { width: 100%; margin: 0; }
|
|
63
|
+
|
|
64
|
+
.error-header {
|
|
65
|
+
background: var(--error-red);
|
|
66
|
+
color: white;
|
|
67
|
+
padding: 2rem 2.5rem;
|
|
68
|
+
margin: 0;
|
|
69
|
+
font-family: var(--text-mono);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.error-header h1 {
|
|
73
|
+
margin: 0 0 0.5rem 0;
|
|
74
|
+
font-size: 2rem;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
font-family: var(--text-mono);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.error-message {
|
|
80
|
+
margin: 0;
|
|
81
|
+
font-size: 1.1rem;
|
|
82
|
+
opacity: 0.95;
|
|
83
|
+
font-family: var(--text-mono);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.error-content {
|
|
87
|
+
background: var(--bg-white);
|
|
88
|
+
padding: 2rem 2.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stack-trace {
|
|
92
|
+
margin: 1.5rem 0;
|
|
93
|
+
border: 1px solid var(--border-color);
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.stack-trace-header {
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
padding: 0.75rem 1rem;
|
|
101
|
+
background: var(--bg-light);
|
|
102
|
+
border-bottom: 1px solid var(--border-color);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.stack-list {
|
|
106
|
+
list-style: none;
|
|
107
|
+
padding: 0;
|
|
108
|
+
margin: 0;
|
|
109
|
+
max-height: 400px;
|
|
110
|
+
overflow-y: auto;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.stack-list li {
|
|
114
|
+
padding: 0.5rem 1rem;
|
|
115
|
+
font-family: var(--text-mono);
|
|
116
|
+
font-size: 0.875rem;
|
|
117
|
+
border-bottom: 1px solid var(--border-color);
|
|
118
|
+
background: var(--bg-white);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.stack-list li:last-child { border-bottom: none; }
|
|
122
|
+
|
|
123
|
+
.stack-list li:hover { background: #fafafa; }
|
|
124
|
+
|
|
125
|
+
.stack-list code {
|
|
126
|
+
background: transparent;
|
|
127
|
+
padding: 0;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
color: var(--error-red-dark);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.stack-list .path { color: var(--text-gray); margin-left: 0.5rem; }
|
|
133
|
+
|
|
134
|
+
.request-info {
|
|
135
|
+
margin: 1.5rem 0;
|
|
136
|
+
border: 1px solid var(--border-color);
|
|
137
|
+
border-radius: 4px;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.request-info-header {
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
padding: 0.75rem 1rem;
|
|
144
|
+
background: var(--bg-light);
|
|
145
|
+
border-bottom: 1px solid var(--border-color);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.request-info-content {
|
|
149
|
+
padding: 1rem;
|
|
150
|
+
font-family: var(--text-mono);
|
|
151
|
+
font-size: 0.875rem;
|
|
152
|
+
white-space: pre-wrap;
|
|
153
|
+
word-break: break-all;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (max-width: 768px) {
|
|
157
|
+
.error-header, .error-content { padding: 1.5rem 1rem; }
|
|
158
|
+
.error-header h1 { font-size: 1.5rem; }
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
|
|
162
|
+
type ErrorHtmlData = {
|
|
163
|
+
status: number
|
|
164
|
+
tag: string
|
|
165
|
+
message?: string
|
|
166
|
+
details?: object
|
|
167
|
+
requestContext?: RequestContext
|
|
168
|
+
errorName?: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function errorHtml(data: ErrorHtmlData): HttpServerResponse.HttpServerResponse {
|
|
172
|
+
let detailsHtml = ""
|
|
173
|
+
|
|
174
|
+
if (data.details) {
|
|
175
|
+
const detailsObj = data.details as Record<string, unknown>
|
|
176
|
+
|
|
177
|
+
if ("stack" in detailsObj && Array.isArray(detailsObj.stack)) {
|
|
178
|
+
const stackFrames = detailsObj.stack as StackFrame[]
|
|
179
|
+
detailsHtml = renderStackTrace(stackFrames)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requestHtml = data.requestContext
|
|
184
|
+
? renderRequestContext(data.requestContext)
|
|
185
|
+
: ""
|
|
186
|
+
|
|
187
|
+
const messageHtml = data.message
|
|
188
|
+
? `<p class="error-message">${escapeHtml(data.message)}</p>`
|
|
189
|
+
: ""
|
|
190
|
+
|
|
191
|
+
const headerTitle = data.errorName ?? "UnexpectedError"
|
|
192
|
+
|
|
193
|
+
const html = `<!DOCTYPE html>
|
|
194
|
+
<html lang="en">
|
|
195
|
+
<head>
|
|
196
|
+
<meta charset="UTF-8">
|
|
197
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
198
|
+
<title>${headerTitle} - Error ${data.status}</title>
|
|
199
|
+
<style>${ERROR_PAGE_CSS}</style>
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<div class="error-header">
|
|
203
|
+
<h1>${escapeHtml(headerTitle)}</h1>
|
|
204
|
+
${messageHtml}
|
|
205
|
+
</div>
|
|
206
|
+
<div class="error-content">
|
|
207
|
+
${detailsHtml}
|
|
208
|
+
${requestHtml}
|
|
209
|
+
</div>
|
|
210
|
+
</body>
|
|
211
|
+
</html>`
|
|
212
|
+
return HttpServerResponse.text(html, {
|
|
213
|
+
status: data.status,
|
|
214
|
+
contentType: "text/html",
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function errorText(
|
|
219
|
+
status: number,
|
|
220
|
+
tag: string,
|
|
221
|
+
details?: object,
|
|
222
|
+
): HttpServerResponse.HttpServerResponse {
|
|
223
|
+
const text = details ? `${tag}\n${JSON.stringify(details, null, 2)}` : tag
|
|
224
|
+
return HttpServerResponse.text(text, { status })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function respondWithError(
|
|
228
|
+
accept: string,
|
|
229
|
+
status: number,
|
|
230
|
+
tag: string,
|
|
231
|
+
message?: string,
|
|
232
|
+
details?: object,
|
|
233
|
+
requestContext?: RequestContext,
|
|
234
|
+
errorName?: string,
|
|
235
|
+
): HttpServerResponse.HttpServerResponse {
|
|
236
|
+
if (accept.includes("text/html")) {
|
|
237
|
+
return errorHtml({
|
|
238
|
+
status,
|
|
239
|
+
tag,
|
|
240
|
+
message,
|
|
241
|
+
details,
|
|
242
|
+
requestContext,
|
|
243
|
+
errorName,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
if (accept.includes("text/plain")) {
|
|
247
|
+
return errorText(status, tag, details)
|
|
248
|
+
}
|
|
249
|
+
return HttpServerResponse.unsafeJson(
|
|
250
|
+
{ error: { _tag: tag, ...details } },
|
|
251
|
+
{ status },
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
30
255
|
export const renderError = (
|
|
31
256
|
error: unknown,
|
|
257
|
+
accept: string = "",
|
|
32
258
|
) =>
|
|
33
259
|
Effect.gen(function*() {
|
|
260
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
261
|
+
|
|
262
|
+
const requestContext: RequestContext = {
|
|
263
|
+
url: request.url,
|
|
264
|
+
method: request.method,
|
|
265
|
+
headers: filterSensitiveHeaders(request.headers),
|
|
266
|
+
}
|
|
267
|
+
|
|
34
268
|
let unwrappedError: GraciousError | undefined
|
|
35
269
|
|
|
36
270
|
if (Cause.isCause(error)) {
|
|
@@ -45,68 +279,175 @@ export const renderError = (
|
|
|
45
279
|
|
|
46
280
|
switch (unwrappedError?._tag) {
|
|
47
281
|
case "RouteNotFound":
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
282
|
+
return respondWithError(
|
|
283
|
+
accept,
|
|
284
|
+
404,
|
|
285
|
+
"RouteNotFound",
|
|
286
|
+
"The page you were looking for doesn't exist",
|
|
287
|
+
undefined,
|
|
288
|
+
requestContext,
|
|
289
|
+
)
|
|
55
290
|
case "RequestError": {
|
|
56
291
|
const message = unwrappedError.reason === "Decode"
|
|
57
292
|
? "Request body is invalid"
|
|
58
|
-
:
|
|
293
|
+
: "Request could not be processed"
|
|
59
294
|
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
295
|
+
return respondWithError(
|
|
296
|
+
accept,
|
|
297
|
+
400,
|
|
298
|
+
"RequestError",
|
|
299
|
+
message,
|
|
300
|
+
{
|
|
63
301
|
reason: unwrappedError.reason,
|
|
64
|
-
message,
|
|
65
302
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
})
|
|
303
|
+
requestContext,
|
|
304
|
+
)
|
|
69
305
|
}
|
|
70
306
|
case "ParseError": {
|
|
71
307
|
const issues = yield* ParseResult.ArrayFormatter.formatIssue(
|
|
72
308
|
unwrappedError.issue,
|
|
73
309
|
)
|
|
74
|
-
const cleanIssues = issues.map(v => Record.remove(v, "_tag"))
|
|
310
|
+
const cleanIssues = issues.map((v) => Record.remove(v, "_tag"))
|
|
75
311
|
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
312
|
+
return respondWithError(
|
|
313
|
+
accept,
|
|
314
|
+
400,
|
|
315
|
+
"ParseError",
|
|
316
|
+
"Validation failed",
|
|
317
|
+
{
|
|
79
318
|
issues: cleanIssues,
|
|
80
319
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
})
|
|
320
|
+
requestContext,
|
|
321
|
+
)
|
|
84
322
|
}
|
|
85
323
|
}
|
|
86
324
|
|
|
87
|
-
|
|
88
|
-
error
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
325
|
+
if (Cause.isCause(error)) {
|
|
326
|
+
const defects = [...Cause.defects(error)]
|
|
327
|
+
const defect = defects[0]
|
|
328
|
+
if (defect instanceof Error) {
|
|
329
|
+
const stackFrames = extractPrettyStack(defect.stack ?? "")
|
|
330
|
+
return respondWithError(
|
|
331
|
+
accept,
|
|
332
|
+
500,
|
|
333
|
+
"UnexpectedError",
|
|
334
|
+
defect.message,
|
|
335
|
+
{
|
|
336
|
+
name: defect.name,
|
|
337
|
+
stack: stackFrames,
|
|
338
|
+
},
|
|
339
|
+
requestContext,
|
|
340
|
+
defect.name,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return respondWithError(
|
|
346
|
+
accept,
|
|
347
|
+
500,
|
|
348
|
+
"UnexpectedError",
|
|
349
|
+
"An unexpected error occurred",
|
|
350
|
+
undefined,
|
|
351
|
+
requestContext,
|
|
352
|
+
"UnexpectedError",
|
|
353
|
+
)
|
|
94
354
|
})
|
|
95
355
|
|
|
96
|
-
function
|
|
356
|
+
function parseStackFrame(line: string): StackFrame | null {
|
|
357
|
+
const match = line.trim().match(StackLinePattern)
|
|
358
|
+
if (!match) return null
|
|
359
|
+
|
|
360
|
+
const [_, fn, fullPath] = match
|
|
361
|
+
const relativePath = fullPath.replace(process.cwd(), ".")
|
|
362
|
+
|
|
363
|
+
let type: "application" | "framework" | "node_modules"
|
|
364
|
+
if (relativePath.includes("node_modules")) {
|
|
365
|
+
type = "node_modules"
|
|
366
|
+
} else if (
|
|
367
|
+
relativePath.startsWith("./src")
|
|
368
|
+
|| relativePath.startsWith("./examples")
|
|
369
|
+
) {
|
|
370
|
+
type = "application"
|
|
371
|
+
} else {
|
|
372
|
+
type = "framework"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
function: fn,
|
|
377
|
+
file: relativePath,
|
|
378
|
+
type,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function extractPrettyStack(stack: string): StackFrame[] {
|
|
97
383
|
return stack
|
|
98
384
|
.split("\n")
|
|
99
385
|
.slice(1)
|
|
100
|
-
.map(
|
|
101
|
-
|
|
386
|
+
.map(parseStackFrame)
|
|
387
|
+
.filter((frame): frame is StackFrame => frame !== null)
|
|
388
|
+
}
|
|
102
389
|
|
|
103
|
-
|
|
390
|
+
function renderStackFrames(frames: StackFrame[]): string {
|
|
391
|
+
if (frames.length === 0) {
|
|
392
|
+
return "<li>No stack frames</li>"
|
|
393
|
+
}
|
|
394
|
+
return frames
|
|
395
|
+
.map(
|
|
396
|
+
(f) =>
|
|
397
|
+
`<li><code>${f.function}</code> at <span class="path">${f.file}</span></li>`,
|
|
398
|
+
)
|
|
399
|
+
.join("")
|
|
400
|
+
}
|
|
104
401
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
402
|
+
function renderStackTrace(frames: StackFrame[]): string {
|
|
403
|
+
return `
|
|
404
|
+
<div class="stack-trace">
|
|
405
|
+
<div class="stack-trace-header">Stack Trace (${frames.length})</div>
|
|
406
|
+
<ul class="stack-list">${renderStackFrames(frames)}</ul>
|
|
407
|
+
</div>
|
|
408
|
+
`
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function escapeHtml(unsafe: string): string {
|
|
412
|
+
return unsafe
|
|
413
|
+
.replace(/&/g, "&")
|
|
414
|
+
.replace(/</g, "<")
|
|
415
|
+
.replace(/>/g, ">")
|
|
416
|
+
.replace(/"/g, """)
|
|
417
|
+
.replace(/'/g, "'")
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function filterSensitiveHeaders(
|
|
421
|
+
headers: Record<string, string>,
|
|
422
|
+
): Record<string, string> {
|
|
423
|
+
const sensitive = ["authorization", "cookie", "x-api-key"]
|
|
424
|
+
return Object.fromEntries(
|
|
425
|
+
Object.entries(headers).filter(
|
|
426
|
+
([key]) => !sensitive.includes(key.toLowerCase()),
|
|
427
|
+
),
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
type RequestContext = {
|
|
432
|
+
url: string
|
|
433
|
+
method: string
|
|
434
|
+
headers: Record<string, string>
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderRequestContext(context: RequestContext): string {
|
|
438
|
+
const headersText = Object
|
|
439
|
+
.entries(context.headers)
|
|
440
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
441
|
+
.join("\n")
|
|
442
|
+
|
|
443
|
+
const requestText = `${context.method} ${context.url}\n${headersText}`
|
|
444
|
+
|
|
445
|
+
return `
|
|
446
|
+
<div class="request-info">
|
|
447
|
+
<div class="request-info-header">Request</div>
|
|
448
|
+
<div class="request-info-content">${escapeHtml(requestText)}</div>
|
|
449
|
+
</div>
|
|
450
|
+
`
|
|
110
451
|
}
|
|
111
452
|
|
|
112
453
|
export function handleErrors<
|
|
@@ -114,14 +455,25 @@ export function handleErrors<
|
|
|
114
455
|
R,
|
|
115
456
|
>(
|
|
116
457
|
app: HttpApp.Default<E, R>,
|
|
117
|
-
): HttpApp.Default<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
458
|
+
): HttpApp.Default<
|
|
459
|
+
Exclude<E, RouteNotFound>,
|
|
460
|
+
R | HttpServerRequest.HttpServerRequest
|
|
461
|
+
> {
|
|
462
|
+
return Effect.gen(function*() {
|
|
463
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
464
|
+
const accept = request.headers.accept ?? ""
|
|
465
|
+
return yield* app.pipe(
|
|
466
|
+
Effect.catchAllCause((cause) => renderError(cause, accept)),
|
|
467
|
+
)
|
|
468
|
+
})
|
|
121
469
|
}
|
|
122
470
|
|
|
123
|
-
export const withErrorHandled = HttpMiddleware.make(app =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
471
|
+
export const withErrorHandled = HttpMiddleware.make(app =>
|
|
472
|
+
Effect.gen(function*() {
|
|
473
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
474
|
+
const accept = request.headers.accept ?? ""
|
|
475
|
+
return yield* app.pipe(
|
|
476
|
+
Effect.catchAllCause((cause) => renderError(cause, accept)),
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
)
|
package/src/Route.test.ts
CHANGED
|
@@ -472,6 +472,37 @@ t.it("context has only request and url when no schemas provided", () => {
|
|
|
472
472
|
)
|
|
473
473
|
})
|
|
474
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
|
+
|
|
475
506
|
t.it("schemas work with all media types", () => {
|
|
476
507
|
const PathSchema = Schema.Struct({
|
|
477
508
|
id: Schema.String,
|
|
@@ -479,23 +510,19 @@ t.it("schemas work with all media types", () => {
|
|
|
479
510
|
|
|
480
511
|
Route
|
|
481
512
|
.schemaPathParams(PathSchema)
|
|
482
|
-
.html(
|
|
483
|
-
(context)
|
|
484
|
-
Function.satisfies<string>()(context.pathParams.id)
|
|
513
|
+
.html((context) => {
|
|
514
|
+
Function.satisfies<string>()(context.pathParams.id)
|
|
485
515
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
516
|
+
return Effect.succeed("<h1>Hello</h1>")
|
|
517
|
+
})
|
|
489
518
|
|
|
490
519
|
Route
|
|
491
520
|
.schemaPathParams(PathSchema)
|
|
492
|
-
.json(
|
|
493
|
-
(context)
|
|
494
|
-
Function.satisfies<string>()(context.pathParams.id)
|
|
521
|
+
.json((context) => {
|
|
522
|
+
Function.satisfies<string>()(context.pathParams.id)
|
|
495
523
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
524
|
+
return Effect.succeed({ message: "hello" })
|
|
525
|
+
})
|
|
499
526
|
})
|
|
500
527
|
|
|
501
528
|
t.it("schemas work with generator functions", () => {
|
|
@@ -505,13 +532,11 @@ t.it("schemas work with generator functions", () => {
|
|
|
505
532
|
|
|
506
533
|
Route
|
|
507
534
|
.schemaPathParams(IdSchema)
|
|
508
|
-
.text(
|
|
509
|
-
|
|
510
|
-
Function.satisfies<string>()(context.pathParams.id)
|
|
535
|
+
.text(function*(context) {
|
|
536
|
+
Function.satisfies<string>()(context.pathParams.id)
|
|
511
537
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
)
|
|
538
|
+
return "hello"
|
|
539
|
+
})
|
|
515
540
|
})
|
|
516
541
|
|
|
517
542
|
t.it("schema property is correctly set on RouteSet", () => {
|
|
@@ -901,7 +926,7 @@ t.it("Route.layer creates RouteLayer with middleware", () => {
|
|
|
901
926
|
|
|
902
927
|
t.it("Route.layer merges multiple route sets", () => {
|
|
903
928
|
const routes1 = Route.html(Effect.succeed("<div>1</div>"))
|
|
904
|
-
const routes2 = Route.text(
|
|
929
|
+
const routes2 = Route.text("text")
|
|
905
930
|
|
|
906
931
|
const layer = Route.layer(routes1, routes2)
|
|
907
932
|
|
|
@@ -1093,7 +1118,7 @@ t.it("Route.matches returns false for different methods", () => {
|
|
|
1093
1118
|
|
|
1094
1119
|
t.it("Route.matches returns false for different media types", () => {
|
|
1095
1120
|
const route1 = Route.get(Route.html(Effect.succeed("<div>test</div>")))
|
|
1096
|
-
const route2 = Route.get(Route.json(
|
|
1121
|
+
const route2 = Route.get(Route.json({ data: "test" }))
|
|
1097
1122
|
|
|
1098
1123
|
t.expect(Route.matches(route1.set[0]!, route2.set[0]!)).toBe(false)
|
|
1099
1124
|
})
|
|
@@ -1119,7 +1144,8 @@ t.it("Route.matches returns true when one route has wildcard method", () => {
|
|
|
1119
1144
|
|
|
1120
1145
|
t.describe("Route.merge", () => {
|
|
1121
1146
|
t.it("types merged routes with union of methods", () => {
|
|
1122
|
-
const textRoute = Route.text(
|
|
1147
|
+
const textRoute = Route.text("hello")
|
|
1148
|
+
|
|
1123
1149
|
const htmlRoute = Route.html(Effect.succeed("<div>world</div>"))
|
|
1124
1150
|
|
|
1125
1151
|
const merged = Route.merge(textRoute, htmlRoute)
|
|
@@ -1138,8 +1164,8 @@ t.describe("Route.merge", () => {
|
|
|
1138
1164
|
})
|
|
1139
1165
|
|
|
1140
1166
|
t.it("types merged routes with different methods", () => {
|
|
1141
|
-
const getRoute = Route.get(Route.text(
|
|
1142
|
-
const postRoute = Route.post(Route.json(
|
|
1167
|
+
const getRoute = Route.get(Route.text("get"))
|
|
1168
|
+
const postRoute = Route.post(Route.json({ ok: true }))
|
|
1143
1169
|
|
|
1144
1170
|
const merged = Route.merge(getRoute, postRoute)
|
|
1145
1171
|
|
|
@@ -1186,8 +1212,8 @@ t.describe("Route.merge", () => {
|
|
|
1186
1212
|
})
|
|
1187
1213
|
|
|
1188
1214
|
t.it("merged route does content negotiation for text/plain", async () => {
|
|
1189
|
-
const textRoute = Route.text(
|
|
1190
|
-
const htmlRoute = Route.html(
|
|
1215
|
+
const textRoute = Route.text("plain text")
|
|
1216
|
+
const htmlRoute = Route.html("<div>html</div>")
|
|
1191
1217
|
|
|
1192
1218
|
const merged = Route.merge(textRoute, htmlRoute)
|
|
1193
1219
|
const route = merged.set[0]!
|
|
@@ -1217,8 +1243,8 @@ t.describe("Route.merge", () => {
|
|
|
1217
1243
|
})
|
|
1218
1244
|
|
|
1219
1245
|
t.it("merged route does content negotiation for text/html", async () => {
|
|
1220
|
-
const textRoute = Route.text(
|
|
1221
|
-
const htmlRoute = Route.html(
|
|
1246
|
+
const textRoute = Route.text("plain text")
|
|
1247
|
+
const htmlRoute = Route.html("<div>html</div>")
|
|
1222
1248
|
|
|
1223
1249
|
const merged = Route.merge(textRoute, htmlRoute)
|
|
1224
1250
|
const route = merged.set[0]!
|
|
@@ -1250,8 +1276,8 @@ t.describe("Route.merge", () => {
|
|
|
1250
1276
|
t.it(
|
|
1251
1277
|
"merged route does content negotiation for application/json",
|
|
1252
1278
|
async () => {
|
|
1253
|
-
const textRoute = Route.text(
|
|
1254
|
-
const jsonRoute = Route.json(
|
|
1279
|
+
const textRoute = Route.text("plain text")
|
|
1280
|
+
const jsonRoute = Route.json({ message: "json" })
|
|
1255
1281
|
|
|
1256
1282
|
const merged = Route.merge(textRoute, jsonRoute)
|
|
1257
1283
|
const route = merged.set[0]!
|
|
@@ -1282,8 +1308,8 @@ t.describe("Route.merge", () => {
|
|
|
1282
1308
|
)
|
|
1283
1309
|
|
|
1284
1310
|
t.it("merged route defaults to html for */* accept", async () => {
|
|
1285
|
-
const textRoute = Route.text(
|
|
1286
|
-
const htmlRoute = Route.html(
|
|
1311
|
+
const textRoute = Route.text("plain text")
|
|
1312
|
+
const htmlRoute = Route.html("<div>html</div>")
|
|
1287
1313
|
|
|
1288
1314
|
const merged = Route.merge(textRoute, htmlRoute)
|
|
1289
1315
|
const route = merged.set[0]!
|
|
@@ -1314,8 +1340,8 @@ t.describe("Route.merge", () => {
|
|
|
1314
1340
|
t.it(
|
|
1315
1341
|
"merged route defaults to first route when no Accept header",
|
|
1316
1342
|
async () => {
|
|
1317
|
-
const textRoute = Route.text(
|
|
1318
|
-
const htmlRoute = Route.html(
|
|
1343
|
+
const textRoute = Route.text("plain text")
|
|
1344
|
+
const htmlRoute = Route.html("<div>html</div>")
|
|
1319
1345
|
|
|
1320
1346
|
const merged = Route.merge(textRoute, htmlRoute)
|
|
1321
1347
|
const route = merged.set[0]!
|