import type { Awaitable } from "./utils/common-types.ts";import { combine, Context } from "./context.ts";import { accepts, Accepted } from './content-negotiation.ts'import { payloadTooLarge } from 'https://ghuc.cc/worker-tools/response-creators/index.ts'
export const JSON = 'application/json';export const FORM = 'application/x-www-form-urlencoded'export const FORM_DATA = 'multipart/form-data';export const TEXT_HTML = 'text/html'export const TEXT_PLAIN = 'text/plain'export const BINARY = 'application/octet-stream';export const X_BINARY = 'application/x-binary';
export interface BodyParserOptions<J> { defaultJSON?: J, maxSize?: number}
export type BodyParsable = | typeof FORM | typeof FORM_DATA | typeof JSON | typeof BINARY | typeof X_BINARY | `application/${string}+json` | `text/${string}`
export const defaultBody: BodyParsable[] = [ FORM, FORM_DATA, JSON, BINARY, X_BINARY, TEXT_HTML, TEXT_PLAIN,]
export interface BodyJSONContext<J = any> { accepted: typeof JSON, body: J json: J}
export interface BodyVendorJSONContext<J = any> { accepted: `application/${string}+json`, body: J json: J}
export interface BodyFormContext { accepted: typeof FORM, body: URLSearchParams, form: URLSearchParams, }
export interface BodyFormDataContext { accepted: typeof FORM_DATA, body: FormData formData: FormData }
export interface BodyBinaryContext { accepted: typeof BINARY | typeof X_BINARY body: ArrayBuffer, arrayBuffer: ArrayBuffer, blob: Blob,}
export interface BodyTextContext { accepted: `text/${string}`, body: string, text: string,}
export type BodyContext<J> = | BodyJSONContext<J> | BodyBinaryContext | BodyFormContext | BodyFormDataContext | BodyTextContext | BodyVendorJSONContext<J>
const _isString = (x: [string, FormDataEntryValue]): x is [string, string] => !(x[1] instanceof File)const _isFile = (x: [string, FormDataEntryValue]): x is [string, File] => x[1] instanceof File
export type BodyParserDeps = Context & Accepted<BodyParsable>
const isBodyTextContext = <J = any>(nx: BodyContext<J>): nx is BodyTextContext => nx.accepted?.startsWith('text/')
const isBodyVendorJSONContext = <J = any>(nx: BodyContext<J>): nx is BodyVendorJSONContext<J> => nx.accepted?.startsWith('application/') && nx.accepted.endsWith('+json')
const MB = 1024**2
async function checkSize(req: Request, maxSize: number) { let size = 0; await req.clone().body!.pipeTo( new WritableStream({ write(chunk, ctrl) { size += chunk.byteLength if (size > maxSize) { ctrl.error(new Error('Payload too large')) } } })) return size <= maxSize}
export const bodyParser = <J = any>( opts: BodyParserOptions<J> = {},) => async <X extends BodyParserDeps>( ax: Awaitable<X>): Promise<X & BodyContext<J>> => { const x = await ax; const nx = x as X & BodyContext<J>;
const ok = await checkSize(x.request, opts.maxSize ?? 1 * MB) if (!ok) throw payloadTooLarge()
switch (nx.accepted) { case JSON: { nx.body = nx.json = await x.request.json() return nx; } case FORM: { const _form = nx.body = nx.form = new URLSearchParams(await x.request.text()) return nx; } case FORM_DATA: { const _formData = nx.body = nx.formData = await x.request.formData(); return nx; } case BINARY: case X_BINARY: { nx.body = nx.arrayBuffer = await x.request.arrayBuffer(); nx.blob = new Blob([nx.arrayBuffer]) return nx; } default: { if (isBodyTextContext(nx)) { nx.body = nx.text = await x.request.text(); } else if (isBodyVendorJSONContext(nx)) { nx.body = nx.json = await x.request.json() return nx; } else { (<any>nx).body = (<any>nx).buffer = await x.request.arrayBuffer(); (<any>nx).blob = new Blob([(<any>nx).buffer]) return nx; } return nx; } } }
export const defaultBodyParser = <J = any>(options?: BodyParserOptions<J>) => combine(accepts(defaultBody), bodyParser(options));