import * as Data from "effect/Data" import * as Effect from "effect/Effect" import { identity } from "effect/Function" import * as Inspectable from "effect/Inspectable" import * as Schema from "effect/Schema" import type { ParseOptions } from "effect/SchemaAST" import * as Stream_ from "effect/Stream" import type * as PlatformError from "../Error.js" import * as FileSystem from "../FileSystem.js" import type * as Body from "../HttpBody.js" import * as UrlParams from "../UrlParams.js" /** @internal */ export const TypeId: Body.TypeId = Symbol.for( "@effect/platform/HttpBody" ) as Body.TypeId /** @internal */ export const ErrorTypeId: Body.ErrorTypeId = Symbol.for( "@effect/platform/HttpBody/HttpBodyError" ) as Body.ErrorTypeId const bodyError = Data.tagged("HttpBodyError") /** @internal */ export const HttpBodyError = (reason: Body.ErrorReason): Body.HttpBodyError => bodyError({ [ErrorTypeId]: ErrorTypeId, reason }) abstract class BodyBase implements Body.HttpBody.Proto { readonly [TypeId]: Body.TypeId abstract readonly _tag: string constructor() { this[TypeId] = TypeId } abstract toJSON(): unknown [Inspectable.NodeInspectSymbol](): unknown { return this.toJSON() } toString(): string { return Inspectable.format(this) } } class EmptyImpl extends BodyBase implements Body.Empty { readonly _tag = "Empty" toJSON(): unknown { return { _id: "@effect/platform/HttpBody", _tag: "Empty" } } } /** @internal */ export const empty: Body.Empty = new EmptyImpl() class RawImpl extends BodyBase implements Body.Raw { readonly _tag = "Raw" constructor( readonly body: unknown, readonly contentType?: string | undefined, readonly contentLength?: number | undefined ) { super() } toJSON(): unknown { return { _id: "@effect/platform/HttpBody", _tag: "Raw", body: this.body, contentType: this.contentType, contentLength: this.contentLength } } } /** @internal */ export const raw = (body: unknown, options?: { readonly contentType?: string | undefined readonly contentLength?: number | undefined }): Body.Raw => new RawImpl(body, options?.contentType, options?.contentLength) class Uint8ArrayImpl extends BodyBase implements Body.Uint8Array { readonly _tag = "Uint8Array" constructor( readonly body: Uint8Array, readonly contentType: string ) { super() } get contentLength(): number { return this.body.length } toJSON(): unknown { const toString = this.contentType.startsWith("text/") || this.contentType.endsWith("json") return { _id: "@effect/platform/HttpBody", _tag: "Uint8Array", body: toString ? new TextDecoder().decode(this.body) : `Uint8Array(${this.body.length})`, contentType: this.contentType, contentLength: this.contentLength } } } /** @internal */ export const uint8Array = (body: Uint8Array, contentType?: string): Body.Uint8Array => new Uint8ArrayImpl(body, contentType ?? "application/octet-stream") const encoder = new TextEncoder() /** @internal */ export const text = (body: string, contentType?: string): Body.Uint8Array => uint8Array(encoder.encode(body), contentType ?? "text/plain") /** @internal */ export const unsafeJson = (body: unknown, contentType?: string): Body.Uint8Array => text(JSON.stringify(body), contentType ?? "application/json") /** @internal */ export const json = (body: unknown, contentType?: string): Effect.Effect => Effect.try({ try: () => unsafeJson(body, contentType), catch: (error) => HttpBodyError({ _tag: "JsonError", error }) }) /** @internal */ export const urlParams = (urlParams: UrlParams.UrlParams, contentType?: string): Body.Uint8Array => text(UrlParams.toString(urlParams), contentType ?? "application/x-www-form-urlencoded") /** @internal */ export const jsonSchema = (schema: Schema.Schema, options?: ParseOptions) => { const encode = Schema.encode(schema, options) return (body: A, contentType?: string): Effect.Effect => Effect.flatMap( Effect.mapError(encode(body), (error) => HttpBodyError({ _tag: "SchemaError", error })), (body) => json(body, contentType) ) } /** @internal */ export const file = ( path: string, options?: FileSystem.StreamOptions & { readonly contentType?: string } ): Effect.Effect => Effect.flatMap( FileSystem.FileSystem, (fs) => Effect.map(fs.stat(path), (info) => stream( fs.stream(path, options), options?.contentType, Number(info.size) )) ) /** @internal */ export const fileInfo = ( path: string, info: FileSystem.File.Info, options?: FileSystem.StreamOptions & { readonly contentType?: string } ): Effect.Effect => Effect.map( FileSystem.FileSystem, (fs) => stream( fs.stream(path, options), options?.contentType, Number(info.size) ) ) /** @internal */ export const fileWeb = (file: Body.HttpBody.FileLike): Body.Stream => stream(Stream_.fromReadableStream(() => file.stream() as ReadableStream, identity), file.type, file.size) class FormDataImpl extends BodyBase implements Body.FormData { readonly _tag = "FormData" constructor( readonly formData: FormData ) { super() } toJSON(): unknown { return { _id: "@effect/platform/HttpBody", _tag: "FormData", formData: this.formData } } } /** @internal */ export const formData = (body: FormData): Body.FormData => new FormDataImpl(body) /** @internal */ export const formDataRecord = ( entries: Body.FormDataInput ): Body.FormData => { const formData = new FormData() for (const [key, value] of Object.entries(entries)) { if (Array.isArray(value)) { for (const item of value) { if (item == null) continue formData.append(key, typeof value === "object" ? item as any : String(item)) } } else if (value != null) { formData.append(key, typeof value === "object" ? value as any : String(value)) } } return new FormDataImpl(formData) } class StreamImpl extends BodyBase implements Body.Stream { readonly _tag = "Stream" constructor( readonly stream: Stream_.Stream, readonly contentType: string, readonly contentLength?: number | undefined ) { super() } toJSON(): unknown { return { _id: "@effect/platform/HttpBody", _tag: "Stream", contentType: this.contentType, contentLength: this.contentLength } } } /** @internal */ export const stream = ( body: Stream_.Stream, contentType?: string | undefined, contentLength?: number | undefined ): Body.Stream => new StreamImpl(body, contentType ?? "application/octet-stream", contentLength)