import { createHttpError, matches, Status } from "./deps.ts";import { parse } from "./form_data.ts";import type { ServerRequest } from "./types.ts";
type JsonReviver = (key: string, value: unknown) => unknown;
export type BodyType = | "binary" | "form" | "form-data" | "json" | "text" | "unknown";
const KNOWN_BODY_TYPES: [bodyType: BodyType, knownMediaTypes: string[]][] = [ ["binary", ["image", "audio", "application/octet-stream"]], ["form", ["urlencoded"]], ["form-data", ["multipart"]], ["json", ["json", "application/*+json", "application/csp-report"]], ["text", ["text"]],];
async function readBlob( body?: ReadableStream<Uint8Array> | null, type?: string | null,): Promise<Blob> { if (!body) { return new Blob(undefined, type ? { type } : undefined); } const chunks: Uint8Array[] = []; for await (const chunk of body) { chunks.push(chunk); } return new Blob(chunks, type ? { type } : undefined);}
export class Body { #body?: ReadableStream<Uint8Array> | null; #headers?: Headers; #request?: Request; #reviver?: JsonReviver; #type?: BodyType; #used = false;
constructor( serverRequest: Pick<ServerRequest, "request" | "headers" | "getBody">, reviver?: JsonReviver, ) { if (serverRequest.request) { this.#request = serverRequest.request; } else { this.#headers = serverRequest.headers; this.#body = serverRequest.getBody(); } this.#reviver = reviver; }
get has(): boolean { return !!(this.#request ? this.#request.body : this.#body); }
get stream(): ReadableStream<Uint8Array> | null { return this.#request ? this.#request.body : this.#body!; }
get used(): boolean { return this.#request?.bodyUsed ?? this.#used; }
async arrayBuffer(): Promise<ArrayBuffer> { if (this.#request) { return this.#request.arrayBuffer(); } this.#used = true; return (await readBlob(this.#body)).arrayBuffer(); }
blob(): Promise<Blob> { if (this.#request) { return this.#request.blob(); } this.#used = true; return readBlob(this.#body, this.#headers?.get("content-type")); }
async form(): Promise<URLSearchParams> { const text = await this.text(); return new URLSearchParams(text); }
formData(): Promise<FormData> { if (this.#request) { return this.#request.formData(); } this.#used = true; if (this.#body && this.#headers) { const contentType = this.#headers.get("content-type"); if (contentType) { return parse(contentType, this.#body); } } throw createHttpError(Status.BadRequest, "Missing content type."); }
async json(): Promise<any> { try { if (this.#reviver) { const text = await this.text(); return JSON.parse(text, this.#reviver); } else if (this.#request) { const value = await this.#request.json(); return value; } else { this.#used = true; return JSON.parse(await (await readBlob(this.#body)).text()); } } catch (err) { if (err instanceof Error) { throw createHttpError(Status.BadRequest, err.message); } throw createHttpError(Status.BadRequest, JSON.stringify(err)); } }
async text(): Promise<string> { if (this.#request) { return this.#request.text(); } this.#used = true; return (await readBlob(this.#body)).text(); }
type(customMediaTypes?: Partial<Record<BodyType, string[]>>): BodyType { if (this.#type && !customMediaTypes) { return this.#type; } customMediaTypes = customMediaTypes ?? {}; const headers = this.#request?.headers ?? this.#headers; const contentType = headers?.get("content-type"); if (contentType) { for (const [bodyType, knownMediaTypes] of KNOWN_BODY_TYPES) { const customTypes = customMediaTypes[bodyType] ?? []; if (matches(contentType, [...knownMediaTypes, ...customTypes])) { this.#type = bodyType; return this.#type; } } } return this.#type = "unknown"; }
[Symbol.for("Deno.customInspect")]( inspect: (value: unknown) => string, ): string { const { has, used } = this; return `${this.constructor.name} ${inspect({ has, used })}`; }
[Symbol.for("nodejs.util.inspect.custom")]( depth: number, options: any, inspect: (value: unknown, options?: unknown) => string, ): any { if (depth < 0) { return options.stylize(`[${this.constructor.name}]`, "special"); }
const newOptions = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1, }); const { has, used } = this; return `${options.stylize(this.constructor.name, "special")} ${ inspect( { has, used }, newOptions, ) }`; }}