1/**
2 * Converts supported values into HTTP server responses.
3 *
4 * Server-side errors and helper values can implement `Respondable` when they
5 * know which status, headers, cookies, or body should be sent to the client.
6 * This module detects those values and converts them to `HttpServerResponse`
7 * values, with fallback handling for schema errors and missing values.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Cause from "../Cause.ts"
13import * as Effect from "../Effect.ts"
14import * as Schema from "../internal/schemaError.ts"
15import { hasProperty } from "../Predicate.ts"
16import type { HttpServerResponse } from "./HttpServerResponse.ts"
17import * as Response from "./HttpServerResponse.ts"
18
19/**
20 * Protocol key used by values that can render themselves as
21 * `HttpServerResponse` values.
22 *
23 * @stability unstable
24 * @category type IDs
25 * @since 4.0.0
26 */
27export const symbol = "~effect/http/HttpServerRespondable"
28
29/**
30 * Protocol for values that can be converted into an `HttpServerResponse`.
31 *
32 * **Details**
33 *
34 * Implement the protocol method to describe the response that should be sent for
35 * the value.
36 *
37 * @stability unstable
38 * @category models
39 * @since 4.0.0
40 */
41export interface Respondable {
42 [symbol](): Effect.Effect<HttpServerResponse, unknown>
43}
44
45/**
46 * Returns `true` when the supplied value implements the `Respondable` protocol.
47 *
48 * @stability unstable
49 * @category guards
50 * @since 4.0.0
51 */
52export const isRespondable = (u: unknown): u is Respondable => hasProperty(u, symbol)
53
54const badRequest = Response.empty({ status: 400 })
55const notFound = Response.empty({ status: 404 })
56
57/**
58 * Converts a `Respondable` value into an `HttpServerResponse`.
59 *
60 * **Details**
61 *
62 * If the value is already an HTTP server response it is returned directly; errors
63 * from the response conversion are converted to defects.
64 *
65 * @stability unstable
66 * @category accessors
67 * @since 4.0.0
68 */
69export const toResponse = (self: Respondable): Effect.Effect<HttpServerResponse> => {
70 if (Response.isHttpServerResponse(self)) {
71 return Effect.succeed(self)
72 }
73 return Effect.orDie(self[symbol]())
74}
75
76/**
77 * Attempts to convert an unknown value into an `HttpServerResponse`, falling back
78 * to the supplied response when no conversion is available.
79 *
80 * **Details**
81 *
82 * `HttpServerResponse` and `Respondable` values are used directly, schema errors
83 * become `400` responses, and no-such-element errors become `404` responses.
84 *
85 * @stability unstable
86 * @category accessors
87 * @since 4.0.0
88 */
89export const toResponseOrElse = (u: unknown, orElse: HttpServerResponse): Effect.Effect<HttpServerResponse> => {
90 if (Response.isHttpServerResponse(u)) {
91 return Effect.succeed(u)
92 } else if (isRespondable(u)) {
93 return Effect.catchCause(u[symbol](), () => Effect.succeed(orElse))
94 // add support for some commmon types
95 } else if (Schema.isSchemaError(u)) {
96 return Effect.succeed(badRequest)
97 } else if (Cause.isNoSuchElementError(u)) {
98 return Effect.succeed(notFound)
99 }
100 return Effect.succeed(orElse)
101}
102
103/**
104 * Attempts to convert an unknown defect into an `HttpServerResponse`, falling
105 * back to the supplied response when no conversion is available.
106 *
107 * **Details**
108 *
109 * Only `HttpServerResponse` and `Respondable` values receive special handling.
110 *
111 * @stability unstable
112 * @category accessors
113 * @since 4.0.0
114 */
115export const toResponseOrElseDefect = (u: unknown, orElse: HttpServerResponse): Effect.Effect<HttpServerResponse> => {
116 if (Response.isHttpServerResponse(u)) {
117 return Effect.succeed(u)
118 } else if (isRespondable(u)) {
119 return Effect.catchCause(u[symbol](), () => Effect.succeed(orElse))
120 }
121 return Effect.succeed(orElse)
122}