// deno-lint-ignore-file no-explicit-any import { UUID } from "https://deno.land/x/mongo@v0.31.2/mod.ts"; import HttpError from "../HttpError.ts"; import { makeParameterModifier } from "../Router.ts"; export type PrimitiveSchema = number | 'any' | 'undefined' | 'null' | 'string' | 'number' | 'boolean' | 'true' | 'false' | 'uuid' | 'object'; export type OptionalSchema = `${T}?`; export type LiteralSchema = `:${T}`; export type OptionalLiteralSchema = `:${T}?`; export type ObjectSchema = { $optional?: boolean; } & { [key: string]: Schema }; export type OrSchema = [] | [Schema] | [Schema, Schema] | [Schema, Schema, Schema]; export type Schema = PrimitiveSchema | OptionalSchema | OrSchema | ObjectSchema | LiteralSchema | OptionalLiteralSchema; export type ObjectSchemaType = ({ [x in keyof T]: SchemaType }) | (T["$optional"] extends true ? undefined : never); export type SchemaType = T extends 'any' ? any : T extends 'null' ? null : T extends 'undefined' ? undefined : T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'uuid' ? UUID : T extends 'true' ? true : T extends 'false' ? false : T extends number ? T : T extends OptionalSchema ? SchemaType | undefined : T extends LiteralSchema ? T : T extends OptionalLiteralSchema ? T | undefined : T extends [] ? never : T extends [Schema] ? SchemaType : T extends [Schema, Schema] ? SchemaType | SchemaType : T extends [Schema, Schema, Schema] ? SchemaType | SchemaType | SchemaType : T extends ObjectSchema ? ObjectSchemaType : never; function stringify(desc: Schema): string { if (typeof desc === 'string') return desc; if (typeof desc === 'number') return desc.toString(); if (desc instanceof Array) return desc.map(stringify).join(' | '); let res = '{ '; for (const key in desc) { if (key === '$optional') continue; if (res != '{ ') res += ', '; res += key + ': '; res += stringify(desc[key]); } res += '}'; if (desc.$optional) res += '?'; return res; } async function _convert(path: string[], val: unknown, desc: Schema): Promise { const _path = path.join('.'); if (val instanceof Blob) val = await val.text(); if (val instanceof ReadableStream) { const res: Uint8Array[] = []; for await (const part of val) { if (!(part instanceof Uint8Array)) throw new Error(`${_path}: Invalid stream given.`); res.push(part); } val = await new Blob(res).text(); } if (desc === 'any') return val as any; if (typeof desc === 'string') { let type: string = desc; const opt = desc.endsWith('?'); if (opt) type = type.substring(0, desc.length - 1); if (opt && val === undefined) return val; if (type.startsWith(':')) { if (val !== type.substring(1)) return val; else throw new HttpError(`${_path}: Expected ${type.substring(1)}.`); } switch (type) { case "true": if (val === true) return true; throw new HttpError(`${_path}: Expected true.`); case "false": if (val === false) return false; throw new HttpError(`${_path}: Expected false.`); case "null": if (val === null) return null; throw new HttpError(`${_path}: Expected null.`); case "undefined": if (val === undefined) return undefined; throw new HttpError(`${_path}: Expected undefined.`); case "string": return val + ""; case "uuid": try { return new UUID(val + ""); } catch { throw new HttpError(`${_path}: Expected an uuid or a value, convertible to an uuid.`); } case "number": { const res = Number.parseFloat(val + ""); if (isNaN(res)) throw new HttpError(`${_path}: Expected a number or a value, convertible to a number.`); return res; } case "boolean": { const res = val + ""; if (res === 'true') return true; if (res === 'false') return true; throw new HttpError(`${_path}: Expected a boolean or a value, convertible to a boolean.`); } } const num = Number.parseFloat(type); if (!isNaN(num)) { const res = Number.parseFloat(val + ""); if (res !== num) throw new HttpError(`${_path}: Expected ${num}.`); return res; } throw new Error(`${_path}: Unknown type ${type}`); } else if (typeof desc === 'number') { if (desc === val) return desc; else throw new HttpError(`${_path}: Expected ${desc}.`); } else if (desc instanceof Array) { for (const type of desc) { try { return await _convert(path, val, type); } catch { /**/ } } throw new HttpError(`${_path}: Expected a ${stringify(desc)}, got ${typeof val} instead.`); } else { if (desc.$optional && val === undefined) return val; if (val === null) throw new HttpError(`${_path}: Expected an object, got null instead.`); if (typeof val === 'string') { try { val = JSON.parse(val); } catch (e) { throw new HttpError(`${_path}: Invalid JSON given for object: ${e}`); } } else throw new HttpError(`${_path}: Expected an object or a valid json string.`); for (const key in desc) { if (key === '$optional') continue; (val as any)[key] = await _convert([ ...path, key ], (val as any)[key], desc[key]); } return val; } } export function convert(val: unknown, desc: T, path?: string): Promise> { return _convert(path ? [path] : [], val, desc); } export default function schema(desc: Schema) { return makeParameterModifier(async (_req, val, name) => await _convert([name], val, desc)); }