1/**
2 * Attaches HTTP API metadata to Effect Schema values.
3 *
4 * This module is the schema-side bridge for HttpApi endpoint builders,
5 * generated clients, and OpenAPI support. It does not define routes or perform
6 * IO. Instead, the helpers annotate schemas so the surrounding HTTP API tooling
7 * can choose response status codes, content types, body codecs, multipart
8 * handling, and no-body response behavior.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Sse from "../encoding/Sse.ts"
14import { constVoid, type LazyArg } from "../Function.ts"
15import { hasBody, type HttpMethod } from "../http/HttpMethod.ts"
16import * as HttpStatus from "../http/HttpStatus.ts"
17import type * as Multipart_ from "../http/Multipart.ts"
18import * as Predicate from "../Predicate.ts"
19import * as Schema from "../Schema.ts"
20import * as SchemaAST from "../SchemaAST.ts"
21import * as SchemaTransformation from "../SchemaTransformation.ts"
22import * as Stream from "../Stream.ts"
23
24declare module "../Schema.ts" {
25 namespace Annotations {
26 interface Augment {
27 readonly httpApiStatus?: number | undefined
28 /**
29 * The encoding of the payload or response.
30 * This is kept internal because encodings are only exposed through the `as*` functions.
31 * @internal
32 */
33 readonly "~httpApiEncoding"?: Encoding | undefined
34 /**
35 * Marks schemas produced by `encodeToWithHeaders`, carrying the body and
36 * headers schemas so integrations can split the encoded pair.
37 * @internal
38 */
39 readonly "~httpApiWithHeaders"?: WithHeadersAnnotation | undefined
40 }
41 }
42}
43
44/**
45 * Annotation payload attached by `encodeToWithHeaders`.
46 *
47 * @internal
48 */
49export interface WithHeadersAnnotation {
50 readonly body: Schema.Top
51 readonly headers: Schema.Top
52 /**
53 * Codecs from the body and headers types to their response representation.
54 * `HttpApiEndpoint` replaces them with the response encoding codecs.
55 */
56 readonly bodyCodec: Schema.Top
57 readonly headersCodec: Schema.Top
58 /**
59 * The user mapping between the `{ body, headers }` pair and the schema type.
60 */
61 readonly transformation: {
62 readonly decode: (pair: { readonly body: unknown; readonly headers: unknown }) => unknown
63 readonly encode: (value: unknown) => { readonly body: unknown; readonly headers: unknown }
64 }
65}
66
67/**
68 * HTTP API body encoding metadata used by payloads and responses.
69 *
70 * @stability unstable
71 * @category models
72 * @since 4.0.0
73 */
74export type Encoding = PayloadEncoding | ResponseEncoding
75
76/**
77 * HTTP API request payload encoding metadata.
78 *
79 * @stability unstable
80 * @category models
81 * @since 4.0.0
82 */
83export type PayloadEncoding =
84 | {
85 readonly _tag: "Multipart"
86 readonly mode: "buffered" | "stream"
87 readonly contentType: string
88 readonly limits?: Multipart_.withLimits.Options | undefined
89 }
90 | {
91 readonly _tag: "Json" | "FormUrlEncoded" | "Uint8Array" | "Text"
92 readonly contentType: string
93 }
94
95/**
96 * HTTP API response body encoding metadata.
97 *
98 * @stability unstable
99 * @category models
100 * @since 4.0.0
101 */
102export type ResponseEncoding = {
103 readonly _tag: "Json" | "FormUrlEncoded" | "Uint8Array" | "Text"
104 readonly contentType: string
105}
106
107const StreamSchemaTypeId = "~effect/http-api/HttpApiSchema/Stream"
108
109/**
110 * Common HTTP status code literals accepted by {@link status}.
111 *
112 * @stability unstable
113 * @category models
114 * @since 4.0.0
115 */
116export type StatusLiteral = HttpStatus.Literal
117
118/**
119 * Sets the HTTP status code of a schema.
120 *
121 * **Details**
122 *
123 * This is equivalent to calling `.annotate({ httpApiStatus: code })` on the
124 * schema. You can pass either a numeric status code (for example, `201`) or a
125 * common literal name (for example, `"Created"`).
126 *
127 * @stability unstable
128 * @category schemas
129 * @since 4.0.0
130 */
131export function status(code: number): {
132 <S extends Schema.Top>(self: S): S["Rebuild"]
133}
134/**
135 * @stability unstable
136 */
137export function status(code: StatusLiteral): {
138 <S extends Schema.Top>(self: S): S["Rebuild"]
139}
140/**
141 * @stability unstable
142 */
143export function status(code: number | StatusLiteral) {
144 const statusCode = typeof code === "string" ? HttpStatus.fromLiteral(code) : code
145 return <S extends Schema.Top>(self: S): S["Rebuild"] => self.annotate({ httpApiStatus: statusCode })
146}
147
148/**
149 * Creates a void schema with the given HTTP status code.
150 * This is used to represent empty responses with a specific status code.
151 *
152 * @see {@link NoContent} for the predefined 204 no content schema.
153 *
154 * @stability unstable
155 * @category constructors
156 * @since 4.0.0
157 */
158export const Empty = (code: number): Schema.Void => Schema.Void.pipe(status(code))
159
160/**
161 * Type of the `NoContent` schema, a void schema annotated with HTTP status code 204.
162 *
163 * @stability unstable
164 * @category models
165 * @since 4.0.0
166 */
167export interface NoContent extends Schema.Void {}
168
169/**
170 * Schema for empty HTTP responses with status code 204.
171 *
172 * @stability unstable
173 * @category schemas
174 * @since 4.0.0
175 */
176export const NoContent: NoContent = Empty(204)
177
178/**
179 * Type of the `Created` schema, a void schema annotated with HTTP status code 201.
180 *
181 * @stability unstable
182 * @category models
183 * @since 4.0.0
184 */
185export interface Created extends Schema.Void {}
186
187/**
188 * Schema for empty HTTP responses with status code 201.
189 *
190 * @stability unstable
191 * @category schemas
192 * @since 4.0.0
193 */
194export const Created: Created = Empty(201)
195
196/**
197 * Type of the `Accepted` schema, a void schema annotated with HTTP status code 202.
198 *
199 * @stability unstable
200 * @category models
201 * @since 4.0.0
202 */
203export interface Accepted extends Schema.Void {}
204
205/**
206 * Schema for empty HTTP responses with status code 202.
207 *
208 * @stability unstable
209 * @category schemas
210 * @since 4.0.0
211 */
212export const Accepted: Accepted = Empty(202)
213
214/**
215 * Schema type returned by `asNoContent`, encoding as `void` while decoding to the original schema type.
216 *
217 * @stability unstable
218 * @category schemas
219 * @since 4.0.0
220 */
221export interface asNoContent<S extends Schema.Constraint> extends Schema.decodeTo<Schema.toType<S>, Schema.Void> {}
222
223/**
224 * Marks a schema as a no-content response while preserving a decoded client value.
225 *
226 * **Details**
227 *
228 * The server encodes the response as `void`; generated clients call `decode` to
229 * produce the schema's decoded value when the response has no body.
230 *
231 * @see {@link NoContent} for a void schema with the status code 204.
232 * @see {@link Empty} for creating a void schema with a specific status code.
233 *
234 * @stability unstable
235 * @category encoding
236 * @since 4.0.0
237 */
238export function asNoContent<S extends Schema.Constraint>(options: {
239 readonly decode: LazyArg<S["Type"]>
240}) {
241 return (self: S): asNoContent<S> => {
242 return Schema.Void.pipe(
243 Schema.decodeTo(
244 Schema.toType(self),
245 SchemaTransformation.transform({
246 decode: options.decode,
247 encode: constVoid
248 })
249 )
250 )
251 }
252}
253
254type StreamMode = "sse" | "uint8array"
255
256/**
257 * Mode describing whether an SSE stream emits full events or raw data values.
258 *
259 * @stability unstable
260 * @category models
261 * @since 4.0.0
262 */
263export type StreamSseMode = "events" | "data"
264
265/**
266 * Schema for a Server-Sent Events success response.
267 *
268 * **Details**
269 *
270 * `events` describes successful application events emitted by the stream, and
271 * `error` describes typed stream failures that will be encoded by later
272 * endpoint/server/client integrations using the reserved failure event. If
273 * `error` is omitted, it defaults to `Schema.Never`. When `StreamSse` is
274 * constructed from `data`, handlers and clients expose raw data values while
275 * the server and client still use an SSE event schema internally.
276 *
277 * **Gotchas**
278 *
279 * The client treats `effect/http-api/stream/failure` as a stream failure only
280 * when its decoded `data` is a `Cause`. If an event schema accepts that name
281 * dynamically but decodes `data` to another value, the client emits it as an
282 * application event. Endpoint construction rejects event schemas that declare
283 * the reserved name statically.
284 *
285 * @stability unstable
286 * @category models
287 * @since 4.0.0
288 */
289export interface StreamSse<
290 Events extends Sse.EventCodec,
291 Error extends Schema.Constraint,
292 Value = Events["Type"]
293> extends
294 Schema.BottomLazy<
295 SchemaAST.Declaration,
296 StreamSse<Events, Error, Value>
297 >
298{
299 readonly "Type": Stream.Stream<Value, Error["Type"], never>
300 readonly "Encoded": Stream.Stream<Value, Error["Type"], never>
301 readonly "DecodingServices": Events["DecodingServices"] | Error["DecodingServices"]
302 readonly "EncodingServices": Events["EncodingServices"] | Error["EncodingServices"]
303 readonly "Rebuild": StreamSse<Events, Error, Value>
304 readonly "~type.make.in": Stream.Stream<Value, Error["Type"], never>
305 readonly "~type.make": Stream.Stream<Value, Error["Type"], never>
306 readonly "Iso": Stream.Stream<Value, Error["Type"], never>
307 readonly [StreamSchemaTypeId]: typeof StreamSchemaTypeId
308 readonly _tag: "StreamSse"
309 readonly mode: "sse"
310 readonly sseMode: StreamSseMode
311 readonly contentType: string
312 readonly events: Events
313 readonly error: Error
314 readonly "~Value"?: Value | undefined
315}
316
317/**
318 * Event schema produced when {@link StreamSse} is constructed from a JSON data schema.
319 *
320 * @stability unstable
321 * @category models
322 * @since 4.0.0
323 */
324export interface SseEventFromData<Data extends Schema.Constraint> extends
325 Schema.ConstraintCodec<
326 {
327 readonly id?: string | undefined
328 readonly event: string
329 readonly data: Data["Type"]
330 },
331 {
332 readonly id?: string | undefined
333 readonly event?: string | undefined
334 readonly data: string
335 },
336 Data["DecodingServices"],
337 Data["EncodingServices"]
338 >
339{}
340
341/**
342 * Schema for a streaming `Uint8Array` success response.
343 *
344 * **Details**
345 *
346 * This declaration stores the response content type for later endpoint,
347 * server, client, and OpenAPI integrations. It is intentionally separate from
348 * the buffered `asUint8Array` response encoding.
349 *
350 * @stability unstable
351 * @category models
352 * @since 4.0.0
353 */
354export interface StreamUint8Array extends
355 Schema.Bottom<
356 Stream.Stream<Uint8Array, unknown, never>,
357 Stream.Stream<Uint8Array, unknown, never>,
358 never,
359 never,
360 SchemaAST.Declaration,
361 StreamUint8Array
362 >
363{
364 readonly "Rebuild": StreamUint8Array
365 readonly [StreamSchemaTypeId]: typeof StreamSchemaTypeId
366 readonly _tag: "StreamUint8Array"
367 readonly mode: "uint8array"
368 readonly contentType: string
369}
370
371/**
372 * Schema for a streaming HTTP API success response.
373 *
374 * @stability unstable
375 * @category models
376 * @since 4.0.0
377 */
378export type StreamSchema = StreamSse<Sse.EventCodec, Schema.Top, unknown> | StreamUint8Array
379
380const streamSchema = Schema.declare(Stream.isStream)
381
382/**
383 * Creates a Server-Sent Events streaming success response schema.
384 *
385 * @stability unstable
386 * @category constructors
387 * @since 4.0.0
388 */
389export const StreamSse: {
390 <Events extends Sse.EventCodec, Error extends Schema.Constraint = Schema.Never>(options: {
391 readonly contentType?: string | undefined
392 readonly events: Events
393 readonly error?: Error | undefined
394 }): StreamSse<Events, Error, Events["Type"]>
395 <Data extends Schema.Constraint, Error extends Schema.Constraint = Schema.Never>(options: {
396 readonly contentType?: string | undefined
397 readonly data: Data
398 readonly error?: Error | undefined
399 }): StreamSse<SseEventFromData<Data>, Error, Data["Type"]>
400} = (options: {
401 readonly contentType?: string | undefined
402 readonly events?: Sse.EventCodec | undefined
403 readonly data?: Schema.Constraint | undefined
404 readonly error?: Schema.Constraint | undefined
405}): StreamSse<Sse.EventCodec, Schema.Top, unknown> => {
406 const events = options.events ?? (options.data === undefined ? undefined : Schema.Struct({
407 ...Sse.EventEncoded.fields,
408 data: Schema.fromJsonString(options.data)
409 }))
410 if (events === undefined) {
411 throw new Error("StreamSse requires either an events schema or a data schema")
412 }
413 return Schema.make<StreamSse<Sse.EventCodec, Schema.Top, unknown>>(streamSchema.ast, {
414 [StreamSchemaTypeId]: StreamSchemaTypeId,
415 _tag: "StreamSse",
416 mode: "sse",
417 sseMode: options.events === undefined ? "data" : "events",
418 contentType: options.contentType ?? defaultStreamContentType("sse"),
419 events,
420 error: options.error ?? Schema.Never
421 })
422}
423
424/**
425 * Creates a streaming `Uint8Array` success response schema.
426 *
427 * @stability unstable
428 * @category constructors
429 * @since 4.0.0
430 */
431export const StreamUint8Array = (options?: {
432 readonly contentType?: string | undefined
433}): StreamUint8Array =>
434 Schema.make<StreamUint8Array>(streamSchema.ast, {
435 [StreamSchemaTypeId]: StreamSchemaTypeId,
436 _tag: "StreamUint8Array",
437 mode: "uint8array",
438 contentType: options?.contentType ?? defaultStreamContentType("uint8array")
439 })
440
441/**
442 * @internal
443 */
444export const isStreamSchema = (u: unknown): u is StreamSchema =>
445 Schema.isSchema(u) && Predicate.hasProperty(u, StreamSchemaTypeId)
446
447/**
448 * @internal
449 */
450export const isStreamSse = (u: unknown): u is StreamSse<Sse.EventCodec, Schema.Top, unknown> =>
451 isStreamSchema(u) && u._tag === "StreamSse"
452
453/**
454 * @internal
455 */
456export const isStreamUint8Array = (u: unknown): u is StreamUint8Array =>
457 isStreamSchema(u) && u._tag === "StreamUint8Array"
458
459function defaultStreamContentType(mode: StreamMode): string {
460 switch (mode) {
461 case "sse":
462 return "text/event-stream"
463 case "uint8array":
464 return "application/octet-stream"
465 }
466}
467
468/**
469 * Runtime brand key used to mark `WithHeaders` response schemas.
470 *
471 * @stability unstable
472 * @category type IDs
473 * @since 4.0.0
474 */
475export const WithHeadersTypeId = "~effect/http-api/HttpApiSchema/WithHeaders"
476
477/**
478 * Type-level brand identifier used by `WithHeaders`.
479 *
480 * @stability unstable
481 * @category type IDs
482 * @since 4.0.0
483 */
484export type WithHeadersTypeId = typeof WithHeadersTypeId
485
486/**
487 * Runtime brand key used to mark `WithHeaders` response values.
488 *
489 * @stability unstable
490 * @category type IDs
491 * @since 4.0.0
492 */
493export const WithHeadersValueTypeId = "~effect/http-api/HttpApiSchema/WithHeadersValue"
494
495/**
496 * Type-level brand identifier used by `WithHeaders` response values.
497 *
498 * @stability unstable
499 * @category type IDs
500 * @since 4.0.0
501 */
502export type WithHeadersValueTypeId = typeof WithHeadersValueTypeId
503
504/**
505 * A response schema wrapping a body schema together with a response headers
506 * schema.
507 *
508 * **Details**
509 *
510 * `WithHeaders` is a branded declaration schema: it carries the inner success
511 * schema and the headers schema as properties, and server, client, and OpenAPI
512 * integrations detect the brand and handle body and headers separately. It is
513 * supported for error responses, though {@link encodeToWithHeaders} is usually
514 * more convenient there because handlers can fail with the domain error value.
515 *
516 * - `schema` is the inner response schema. Success responses may wrap
517 * `StreamSse` and `StreamUint8Array`; error responses remain non-streaming.
518 * Nesting `WithHeaders` is rejected at construction.
519 * - `headers` is any schema; endpoint construction applies
520 * `Schema.toCodecStringTree` unless codecs are disabled, so leaves become
521 * `string | undefined` on the wire and `undefined` leaves are omitted from the response.
522 * - Status and response-encoding annotations are resolved from the wrapper
523 * first, falling through to the inner schema.
524 * - A header-carrying response cannot share its status and content type with
525 * another response in the same success or error union. Endpoint construction
526 * rejects ambiguous declarations, including those made with
527 * {@link encodeToWithHeaders}.
528 * - `Rebuild` preserves the brand and both parts, so `.annotate` keeps the
529 * wrapper intact.
530 *
531 * @stability unstable
532 * @category models
533 * @since 4.0.0
534 */
535export interface WithHeaders<S extends Schema.Top, H extends Schema.Top> extends
536 Schema.Bottom<
537 withHeaders<S["Type"], H["Type"]>,
538 withHeaders<S["Encoded"], Schema.StringTree>,
539 S["DecodingServices"] | H["DecodingServices"],
540 S["EncodingServices"] | H["EncodingServices"],
541 SchemaAST.Declaration,
542 WithHeaders<S, H>
543 >
544{
545 readonly "Rebuild": WithHeaders<S, H>
546 readonly [WithHeadersTypeId]: typeof WithHeadersTypeId
547 readonly schema: S
548 readonly headers: H
549}
550
551/**
552 * The Type of a `WithHeaders` schema: what handlers return and what the
553 * client resolves to, constructed via {@link withHeaders}.
554 *
555 * **Details**
556 *
557 * `body` is the inner success value. For stream success schemas it is the
558 * `Stream` itself, so headers are decided before the body starts streaming.
559 *
560 * @stability unstable
561 * @category models
562 * @since 4.0.0
563 */
564export interface withHeaders<A, H> {
565 readonly [WithHeadersValueTypeId]: WithHeadersValueTypeId
566 readonly body: A
567 readonly headers: H
568}
569
570/**
571 * @internal
572 */
573export const isWithHeadersValue = (u: unknown): u is withHeaders<unknown, unknown> =>
574 Predicate.hasProperty(u, WithHeadersValueTypeId)
575
576const withHeadersValueSchema = Schema.declare(isWithHeadersValue)
577
578/**
579 * Wraps a success schema with a response headers schema.
580 *
581 * **Details**
582 *
583 * Headers accept either a schema or a fields shorthand, mirroring the
584 * request-side headers option.
585 *
586 * **Example** (Adding response headers to a success schema)
587 *
588 * ```ts import.meta.vitest
589 * import { Schema } from "effect"
590 * import { HttpApiSchema } from "effect/http-api"
591 *
592 * const schema = HttpApiSchema.WithHeaders(Schema.String, {
593 * "x-total-count": Schema.FiniteFromString
594 * })
595 * const response: typeof schema.Type = HttpApiSchema.withHeaders({
596 * body: "created",
597 * headers: { "x-total-count": 1 }
598 * })
599 *
600 * HttpApiSchema.isWithHeaders(schema) // => true
601 * response.body // => "created"
602 * response.headers // => { "x-total-count": 1 }
603 * ```
604 *
605 * @stability unstable
606 * @category constructors
607 * @since 4.0.0
608 */
609export function WithHeaders<S extends Schema.Top, H extends Schema.Struct.Fields>(
610 schema: S,
611 headers: H
612): WithHeaders<S, Schema.Struct<H>>
613/**
614 * @stability unstable
615 */
616export function WithHeaders<S extends Schema.Top, H extends Schema.Top>(
617 schema: S,
618 headers: H
619): WithHeaders<S, H>
620/**
621 * @stability unstable
622 */
623export function WithHeaders(
624 schema: Schema.Top,
625 headers: Schema.Top | Schema.Struct.Fields
626): WithHeaders<Schema.Top, Schema.Top> {
627 if (isWithHeaders(schema)) {
628 throw new Error("WithHeaders schemas cannot be nested")
629 }
630 return Schema.make<WithHeaders<Schema.Top, Schema.Top>>(withHeadersValueSchema.ast, {
631 [WithHeadersTypeId]: WithHeadersTypeId,
632 schema,
633 headers: Schema.isSchema(headers) ? headers : Schema.Struct(headers)
634 })
635}
636
637/**
638 * Constructs a `WithHeaders` response value from a body and headers.
639 *
640 * **Details**
641 *
642 * The returned value is branded so servers and clients can detect it exactly,
643 * including in mixed success unions. The same shape is used on both sides: a
644 * value received from a client can be returned from another handler unchanged.
645 *
646 * See {@link WithHeaders} for an example that constructs a schema and its
647 * corresponding response value.
648 *
649 * @stability unstable
650 * @category constructors
651 * @since 4.0.0
652 */
653export const withHeaders = <A, H>(options: {
654 readonly body: A
655 readonly headers: H
656}): withHeaders<A, H> => ({
657 [WithHeadersValueTypeId]: WithHeadersValueTypeId,
658 body: options.body,
659 headers: options.headers
660})
661
662/**
663 * Returns `true` when a schema is a `WithHeaders` response schema.
664 *
665 * **Example** (Detecting a response headers schema)
666 *
667 * ```ts import.meta.vitest
668 * import { Schema } from "effect"
669 * import { HttpApiSchema } from "effect/http-api"
670 *
671 * const schema = HttpApiSchema.WithHeaders(Schema.String, {
672 * "x-request-id": Schema.String
673 * })
674 *
675 * HttpApiSchema.isWithHeaders(schema) // => true
676 * HttpApiSchema.isWithHeaders(Schema.String) // => false
677 * ```
678 *
679 * @stability unstable
680 * @category predicates
681 * @since 4.0.0
682 */
683export const isWithHeaders = (u: unknown): u is WithHeaders<Schema.Top, Schema.Top> =>
684 Schema.isSchema(u) && Predicate.hasProperty(u, WithHeadersTypeId)
685
686/**
687 * @internal
688 */
689export function rebuildWithHeaders(
690 self: WithHeaders<Schema.Top, Schema.Top>,
691 schema: Schema.Top,
692 headers: Schema.Top
693): WithHeaders<Schema.Top, Schema.Top> {
694 return Schema.make<WithHeaders<Schema.Top, Schema.Top>>(self.ast, {
695 [WithHeadersTypeId]: WithHeadersTypeId,
696 schema,
697 headers
698 })
699}
700
701/**
702 * Schema type returned by `encodeToWithHeaders`, encoding as a `{ body, headers }`
703 * pair while decoding to the source schema type.
704 *
705 * @stability unstable
706 * @category schemas
707 * @since 4.0.0
708 */
709export interface encodeToWithHeaders<
710 S extends Schema.Top,
711 Body extends Schema.Top,
712 Headers extends Schema.Struct.Fields
713> extends
714 Schema.decodeTo<
715 Schema.toType<S>,
716 Schema.Struct<{
717 readonly body: Body
718 readonly headers: Schema.Struct<Headers>
719 }>
720 >
721{}
722
723/**
724 * Encodes a schema as a `{ body, headers }` pair, folding response headers into
725 * an opaque domain type such as an error class.
726 *
727 * **Details**
728 *
729 * The encoded side is the pair of the body schema and the headers fields; the
730 * Type stays the source schema's Type. The body schema is authoritative for
731 * everything wire-level: status, content type, and response encoding resolve
732 * from the body schema's annotations.
733 *
734 * The mappings are pure total functions: validation lives in the body and
735 * header schemas, the mappings only reshape valid data.
736 *
737 * Streams used as the body schema turn mid-stream transport errors into
738 * defects on the client. Stream responses should use {@link WithHeaders},
739 * which preserves the body stream's error channel in the generated client.
740 *
741 * **Example** (Encoding an error with headers)
742 *
743 * ```ts import.meta.vitest
744 * import { Schema } from "effect"
745 * import { HttpApiSchema } from "effect/http-api"
746 *
747 * class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
748 * userId: Schema.Int
749 * }) {}
750 *
751 * const UserNotFoundWithHeaders = UserNotFound.pipe(
752 * HttpApiSchema.encodeToWithHeaders({
753 * body: HttpApiSchema.Empty(404),
754 * headers: {
755 * "x-user-id": Schema.Int
756 * }
757 * }, {
758 * decode: ({ headers }) => new UserNotFound({ userId: headers["x-user-id"] }),
759 * encode: (error) => ({
760 * headers: { "x-user-id": error.userId },
761 * body: undefined
762 * })
763 * })
764 * )
765 *
766 * const encoded = Schema.encodeSync(UserNotFoundWithHeaders)(new UserNotFound({ userId: 123 }))
767 * encoded // => { body: undefined, headers: { "x-user-id": 123 } }
768 * ```
769 *
770 * @see {@link WithHeaders} for the structural wrapper recommended for success
771 * responses, including streams.
772 *
773 * @stability unstable
774 * @category encoding
775 * @since 4.0.0
776 */
777export function encodeToWithHeaders<
778 S extends Schema.Top,
779 Body extends Schema.Top,
780 Headers extends Schema.Struct.Fields
781>(options: {
782 readonly body: Body
783 readonly headers: Headers
784}, transformation: {
785 readonly decode: (
786 value: Schema.Struct.Type<{ readonly body: Body; readonly headers: Schema.Struct<Headers> }>
787 ) => S["Type"]
788 readonly encode: (
789 value: S["Type"]
790 ) => Schema.Struct.Type<{ readonly body: Body; readonly headers: Schema.Struct<Headers> }>
791}) {
792 return (self: S): encodeToWithHeaders<S, Body, Headers> => {
793 const body = options.body
794 const headers = Schema.Struct(options.headers)
795 const status = resolveHttpApiStatus(body.ast)
796 const encoding = resolveHttpApiEncoding(body.ast)
797 return Schema.Struct({ body, headers }).pipe(
798 Schema.decodeTo(
799 Schema.toType(self),
800 SchemaTransformation.transform(transformation)
801 )
802 ).annotate({
803 "~httpApiWithHeaders": {
804 body,
805 headers,
806 bodyCodec: body,
807 headersCodec: headers,
808 transformation
809 } as WithHeadersAnnotation,
810 httpApiStatus: status,
811 "~httpApiEncoding": encoding
812 })
813 }
814}
815
816/**
817 * Runtime brand key used to mark schemas as buffered multipart payloads.
818 *
819 * @stability unstable
820 * @category type IDs
821 * @since 4.0.0
822 */
823export const MultipartTypeId = "~effect/http-api/HttpApiSchema/Multipart"
824
825/**
826 * Type-level brand identifier used by `asMultipart`.
827 *
828 * @stability unstable
829 * @category type IDs
830 * @since 4.0.0
831 */
832export type MultipartTypeId = typeof MultipartTypeId
833
834/**
835 * Schema type returned by `asMultipart` for buffered multipart payloads.
836 *
837 * @stability unstable
838 * @category schemas
839 * @since 4.0.0
840 */
841export interface asMultipart<S extends Schema.Top> extends Schema.brand<S["Rebuild"], MultipartTypeId> {}
842
843/**
844 * Marks a schema as a multipart payload.
845 *
846 * @see {@link asMultipartStream} for a multipart stream payload.
847 *
848 * @stability unstable
849 * @category encoding
850 * @since 4.0.0
851 */
852export function asMultipart(options?: Multipart_.withLimits.Options) {
853 return <S extends Schema.Top>(self: S): asMultipart<S> =>
854 self.pipe(Schema.brand(MultipartTypeId)).annotate({
855 "~httpApiEncoding": {
856 _tag: "Multipart",
857 mode: "buffered",
858 contentType: defaultContentType("Multipart"),
859 limits: options
860 }
861 })
862}
863
864/**
865 * Runtime brand key used to mark schemas as streaming multipart payloads.
866 *
867 * @stability unstable
868 * @category type IDs
869 * @since 4.0.0
870 */
871export const MultipartStreamTypeId = "~effect/http-api/HttpApiSchema/MultipartStream"
872
873/**
874 * Type-level brand identifier used by `asMultipartStream`.
875 *
876 * @stability unstable
877 * @category type IDs
878 * @since 4.0.0
879 */
880export type MultipartStreamTypeId = typeof MultipartStreamTypeId
881
882/**
883 * Schema type returned by `asMultipartStream` for streaming multipart payloads.
884 *
885 * @stability unstable
886 * @category schemas
887 * @since 4.0.0
888 */
889export interface asMultipartStream<S extends Schema.Top> extends Schema.brand<S["Rebuild"], MultipartStreamTypeId> {}
890
891/**
892 * Marks a schema as a multipart stream payload.
893 *
894 * @see {@link asMultipart} for a buffered multipart payload.
895 *
896 * @stability unstable
897 * @category encoding
898 * @since 4.0.0
899 */
900export function asMultipartStream(options?: Multipart_.withLimits.Options) {
901 return <S extends Schema.Top>(self: S): asMultipartStream<S> =>
902 self.pipe(Schema.brand(MultipartStreamTypeId)).annotate({
903 "~httpApiEncoding": {
904 _tag: "Multipart",
905 mode: "stream",
906 contentType: defaultContentType("Multipart"),
907 limits: options
908 }
909 })
910}
911
912function asNonMultipartEncoding<S extends Schema.Top>(self: S, options: {
913 readonly _tag: "Json" | "FormUrlEncoded" | "Uint8Array" | "Text"
914 readonly contentType?: string | undefined
915}): S["Rebuild"] {
916 return self.annotate({
917 "~httpApiEncoding": {
918 _tag: options._tag,
919 contentType: options.contentType ?? defaultContentType(options._tag)
920 }
921 })
922}
923
924function defaultContentType(_tag: Encoding["_tag"]): string {
925 switch (_tag) {
926 case "Multipart":
927 return "multipart/form-data"
928 case "Json":
929 return "application/json"
930 case "FormUrlEncoded":
931 return "application/x-www-form-urlencoded"
932 case "Uint8Array":
933 return "application/octet-stream"
934 case "Text":
935 return "text/plain"
936 }
937}
938
939/**
940 * Marks a schema as a JSON payload / response.
941 *
942 * @stability unstable
943 * @category encoding
944 * @since 4.0.0
945 */
946export function asJson(options?: {
947 readonly contentType?: string
948}) {
949 return <S extends Schema.Top>(self: S) => asNonMultipartEncoding(self, { _tag: "Json", ...options })
950}
951
952/**
953 * Marks a schema as an `application/x-www-form-urlencoded` payload or response.
954 *
955 * **Details**
956 *
957 * The schema's encoded side must be a record of strings.
958 *
959 * @stability unstable
960 * @category encoding
961 * @since 4.0.0
962 */
963export function asFormUrlEncoded(options?: {
964 readonly contentType?: string
965}) {
966 return <S extends Schema.Top>(
967 self: S
968 ) => asNonMultipartEncoding(self, { _tag: "FormUrlEncoded", ...options })
969}
970
971/**
972 * Marks a schema as a text payload / response.
973 *
974 * **Details**
975 *
976 * The schema encoded side must be a string.
977 *
978 * @stability unstable
979 * @category encoding
980 * @since 4.0.0
981 */
982export function asText(options?: {
983 readonly contentType?: string
984}) {
985 return <S extends Schema.Top & { readonly Encoded: string }>(self: S) =>
986 asNonMultipartEncoding(self, { _tag: "Text", ...options })
987}
988
989/**
990 * Marks a schema as a binary payload / response.
991 *
992 * **Details**
993 *
994 * The schema encoded side must be a `Uint8Array`.
995 *
996 * @stability unstable
997 * @category encoding
998 * @since 4.0.0
999 */
1000export function asUint8Array(options?: {
1001 readonly contentType?: string
1002}) {
1003 return <S extends Schema.Top & { readonly Encoded: Uint8Array }>(self: S) =>
1004 asNonMultipartEncoding(self, { _tag: "Uint8Array", ...options })
1005}
1006/**
1007 * Returns `true` when a schema AST represents a no-content response.
1008 *
1009 * **Details**
1010 *
1011 * The check succeeds for direct `void` schemas and schemas whose encoded or
1012 * transformation target is `void`.
1013 *
1014 * @stability unstable
1015 * @category predicates
1016 * @since 4.0.0
1017 */
1018export const isNoContent = (ast: SchemaAST.AST): boolean => {
1019 if (SchemaAST.isVoid(ast)) return true
1020 const encoded = SchemaAST.toEncoded(ast)
1021 if (SchemaAST.isVoid(encoded)) return true
1022 const target = ast.encoding?.[0].to
1023 if (target === undefined) return false
1024 return SchemaAST.isVoid(target)
1025}
1026
1027const resolveHttpApiEncoding = SchemaAST.resolveAt<Encoding>("~httpApiEncoding")
1028
1029/**
1030 * @internal
1031 */
1032export const getWithHeadersAnnotation = SchemaAST.resolveAt<WithHeadersAnnotation>("~httpApiWithHeaders")
1033
1034const resolveHttpApiStatus = SchemaAST.resolveAt<number>("httpApiStatus")
1035
1036const defaultJsonEncoding: Encoding = {
1037 _tag: "Json",
1038 contentType: "application/json"
1039}
1040const defaultUrlEncodedEncoding: Encoding = {
1041 _tag: "FormUrlEncoded",
1042 contentType: "application/x-www-form-urlencoded"
1043}
1044
1045function getEncoding(ast: SchemaAST.AST): Encoding {
1046 return resolveHttpApiEncoding(ast) ?? defaultJsonEncoding
1047}
1048
1049/**
1050 * @internal
1051 */
1052export function getPayloadEncoding(ast: SchemaAST.AST, method: HttpMethod): PayloadEncoding {
1053 const encoding = resolveHttpApiEncoding(ast)
1054 if (encoding) return encoding
1055 return hasBody(method) ? defaultJsonEncoding : defaultUrlEncodedEncoding
1056}
1057
1058/**
1059 * @internal
1060 */
1061export function getResponseEncoding(ast: SchemaAST.AST): ResponseEncoding {
1062 const out = getEncoding(ast)
1063 if (out._tag === "Multipart") {
1064 throw new Error("Multipart is not supported in response")
1065 }
1066 return out
1067}
1068
1069/**
1070 * @internal
1071 */
1072export function getStatusSuccess(self: SchemaAST.AST): number {
1073 return resolveHttpApiStatus(self) ?? 200
1074}
1075
1076/**
1077 * @internal
1078 */
1079export function getStatusSuccessSchema(schema: Schema.Constraint): number {
1080 if (isWithHeaders(schema)) {
1081 return resolveHttpApiStatus(schema.ast) ?? getStatusSuccess(schema.schema.ast)
1082 }
1083 return getStatusSuccess(schema.ast)
1084}
1085
1086/**
1087 * @internal
1088 */
1089export function getResponseEncodingSchema(schema: Schema.Constraint): ResponseEncoding {
1090 if (isWithHeaders(schema) && resolveHttpApiEncoding(schema.ast) === undefined) {
1091 return getResponseEncoding(schema.schema.ast)
1092 }
1093 return getResponseEncoding(schema.ast)
1094}
1095
1096/**
1097 * @internal
1098 */
1099export function getStatusError(self: SchemaAST.AST): number {
1100 return resolveHttpApiStatus(self) ?? 500
1101}
1102
1103/**
1104 * @internal
1105 */
1106export function getStatusErrorSchema(schema: Schema.Constraint): number {
1107 if (isWithHeaders(schema)) {
1108 return resolveHttpApiStatus(schema.ast) ?? getStatusError(schema.schema.ast)
1109 }
1110 return getStatusError(schema.ast)
1111}