From cafb669fd1715156668e0f6f9c0e159e1158f83a Mon Sep 17 00:00:00 2001 From: ton1c Date: Wed, 12 Feb 2025 18:26:34 +0300 Subject: [PATCH] reworking errors and fixing router --- server/src/lib/apiValidator.ts | 16 +- server/src/lib/context.ts | 42 +- server/src/lib/db/dbWrapper.ts | 4 +- server/src/lib/errors.ts | 96 ++-- server/src/lib/router.ts | 13 +- server/src/lib/test.ts | 845 --------------------------------- 6 files changed, 86 insertions(+), 930 deletions(-) delete mode 100644 server/src/lib/test.ts diff --git a/server/src/lib/apiValidator.ts b/server/src/lib/apiValidator.ts index f18b539..e28e5a4 100644 --- a/server/src/lib/apiValidator.ts +++ b/server/src/lib/apiValidator.ts @@ -1,7 +1,13 @@ -import { InferSchemaType, Schema } from "@shared/utils/validator.ts"; +import { + InferSchemaType, + Schema, + SchemaValidationError, +} from "@shared/utils/validator.ts"; import { RequestValidationError, + requestValidationError, ResponseValidationError, + responseValidationError, } from "@src/lib/errors.ts"; import { ResultAsync } from "@shared/utils/resultasync.ts"; @@ -13,7 +19,7 @@ export type ExtractRouteParams = T extends string : never; type ApiError = - | RequestValidationError + | SchemaValidationError | ResponseValidationError; export class Api< @@ -51,7 +57,7 @@ export class Api< return this.schema.req .parse(reqBody) .toAsync() - .mapErr((e) => new RequestValidationError(e.input, e.detail)) + .mapErr((e) => requestValidationError(e.msg)) .andThenAsync(async (data) => { const pathSplitted = this.pathSplitted; for (const [key, value] of Object.entries(params)) { @@ -74,9 +80,7 @@ export class Api< return this.schema.res.parse(resBody).toAsync() .map((v) => v as InferSchemaType) - .mapErr((e) => - new ResponseValidationError(e.input, e.detail) - ); + .mapErr((e) => responseValidationError(e.msg)); }); } diff --git a/server/src/lib/context.ts b/server/src/lib/context.ts index ff3028a..7d935c1 100644 --- a/server/src/lib/context.ts +++ b/server/src/lib/context.ts @@ -11,7 +11,10 @@ import { } from "@shared/utils/validator.ts"; import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts"; import log from "@shared/utils/logger.ts"; -import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts"; +import { + FailedToParseRequestAsJSONError, + failedToParseRequestAsJSONError, +} from "@src/lib/errors.ts"; // https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html const SECURITY_HEADERS: Headers = new Headers({ @@ -66,7 +69,7 @@ export class Context< public schema?: { req: ReqSchema; res: ResSchema }; - public withSchema< + public setSchema< Req extends Schema, Res extends Schema, >( @@ -82,14 +85,30 @@ export class Context< return ctx as Context & { schema: { req: Req; res: Res } }; } + public setParams( + params: Params, + ): Context { + const ctx = new Context( + this.req, + this.info, + params, + ); + ctx._url = this._url; + ctx._hostname = this._hostname; + ctx._port = this._port; + ctx._cookies = this._cookies; + ctx.res = this.res; + return ctx as Context; + } + public parseBody(): ResultAsync< InferSchemaType, - SchemaValidationError | FailedToParseRequestAsJSON + SchemaValidationError | FailedToParseRequestAsJSONError > { return ResultAsync .fromPromise( this.req.json(), - (e) => new FailedToParseRequestAsJSON(getMessageFromError(e)), + (e) => failedToParseRequestAsJSONError(getMessageFromError(e)), ) .andThen((data: unknown) => { if (!this.schema) { @@ -224,21 +243,6 @@ export class Context< delete: (name: string) => deleteCookie(this.res.headers, name), }; } - - static setParams( - ctx: Context, - params: Params>, - ): Context { - const newCtx = new Context(ctx.req, ctx.info, params) as typeof ctx; - - newCtx._url = ctx._url; - newCtx._hostname = ctx._hostname; - newCtx._port = ctx._port; - newCtx._cookies = ctx._cookies; - newCtx.schema = ctx.schema; - - return newCtx; - } } type ExtractPath = S extends diff --git a/server/src/lib/db/dbWrapper.ts b/server/src/lib/db/dbWrapper.ts index d3a8827..2d0e0e5 100644 --- a/server/src/lib/db/dbWrapper.ts +++ b/server/src/lib/db/dbWrapper.ts @@ -1,6 +1,6 @@ import { Database, RestBindParameters } from "@db/sqlite"; import { err, getMessageFromError, ok, Result } from "@shared/utils/result.ts"; -import { QueryExecutionError } from "@lib/errors.ts"; +import { QueryExecutionError, queryExecutionError } from "@lib/errors.ts"; import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts"; import log from "@shared/utils/logger.ts"; @@ -13,7 +13,7 @@ export class DatabaseClient { } catch (e) { const message = getMessageFromError(e); log.error(`Failed to execute sql! Error: ${e}`); - return err(new QueryExecutionError(message)); + return err(queryExecutionError(message)); } } diff --git a/server/src/lib/errors.ts b/server/src/lib/errors.ts index bf51d8f..089b353 100644 --- a/server/src/lib/errors.ts +++ b/server/src/lib/errors.ts @@ -1,78 +1,74 @@ -import { - InferSchemaType, - Schema, - StringSchema, - z, -} from "@shared/utils/validator.ts"; +import { InferSchemaType } from "@shared/utils/validator.ts"; +import { createErrorFactory, defineError } from "@shared/utils/errors.ts"; -export class ErrorBase extends Error { - constructor(message: string = "An unknown error has occurred") { - super(message); - this.name = this.constructor.name; - } -} +export const queryExecutionErrorSchema = defineError( + "QueryExecutionError", +); +export const queryExecutionError = createErrorFactory( + queryExecutionErrorSchema, +); +export type QueryExecutionError = InferSchemaType< + typeof queryExecutionErrorSchema +>; -export function createErrorSchema< - T extends string, - I extends Schema = StringSchema, ->( - type: T, - info?: I, -) { - return z.obj({ - type: z.literal(type), - info: info ?? z.string(), - }); -} +export const noAdminEntryErrorSchema = defineError("NoAdminEntryError"); +export const noAdminEntryError = createErrorFactory(noAdminEntryErrorSchema); +export type NoAdminEntryError = InferSchemaType; -const queryExecutionErrorSchema = createErrorSchema("QueryExecutionError"); -type QueryExecutionError = InferSchemaType; - -const noAdminEntryErrorSchema = createErrorSchema("noAdminEntryError"); -type NoAdminEntryError = InferSchemaType; - -const failedToReadFileErrorSchema = createErrorSchema("failedToReadFileError"); -type failedToReadFileErrorSchema = InferSchemaType< +export const failedToReadFileErrorSchema = defineError("FailedToReadFileError"); +export const failedToReadFileError = createErrorFactory( + failedToReadFileErrorSchema, +); +export type FailedToReadFileError = InferSchemaType< typeof failedToReadFileErrorSchema >; -const failedToReadFileErrorSchemaSchema = createErrorSchema( - "FailedToReadFileErrorSchema", -); -type FailedToReadFileErrorSchema = InferSchemaType< - typeof failedToReadFileErrorSchemaSchema +export const invalidSyntaxErrorSchema = defineError("InvalidSyntaxError"); +export const invalidSyntaxError = createErrorFactory(invalidSyntaxErrorSchema); +export type InvalidSyntaxError = InferSchemaType< + typeof invalidSyntaxErrorSchema >; -const invalidSyntaxErrorSchema = createErrorSchema("InvalidSyntaxError"); -type InvalidSyntaxError = InferSchemaType; +export const invalidPathErrorSchema = defineError("InvalidPathError"); +export const invalidPathError = createErrorFactory(invalidPathErrorSchema); +export type InvalidPathError = InferSchemaType; -const invalidPathErrorSchema = createErrorSchema("InvalidPathError"); -type InvalidPathError = InferSchemaType; - -const adminPasswordNotSetErrorSchema = createErrorSchema( +export const adminPasswordNotSetErrorSchema = defineError( "AdminPasswordNotSetError", ); -type AdminPasswordNotSetError = InferSchemaType< +export const adminPasswordNotSetError = createErrorFactory( + adminPasswordNotSetErrorSchema, +); +export type AdminPasswordNotSetError = InferSchemaType< typeof adminPasswordNotSetErrorSchema >; -const requestValidationErrorSchema = createErrorSchema( +export const requestValidationErrorSchema = defineError( "RequestValidationError", ); -type RequestValidationError = InferSchemaType< +export const requestValidationError = createErrorFactory( + requestValidationErrorSchema, +); +export type RequestValidationError = InferSchemaType< typeof requestValidationErrorSchema >; -const responseValidationErrorSchema = createErrorSchema( +export const responseValidationErrorSchema = defineError( "ResponseValidationError", ); -type ResponseValidationError = InferSchemaType< +export const responseValidationError = createErrorFactory( + responseValidationErrorSchema, +); +export type ResponseValidationError = InferSchemaType< typeof responseValidationErrorSchema >; -const failedToParseRequestAsJSONErrorSchema = createErrorSchema( +export const failedToParseRequestAsJSONErrorSchema = defineError( "FailedToParseRequestAsJSONError", ); -type FailedToParseRequestAsJSONError = InferSchemaType< +export const failedToParseRequestAsJSONError = createErrorFactory( + failedToParseRequestAsJSONErrorSchema, +); +export type FailedToParseRequestAsJSONError = InferSchemaType< typeof failedToParseRequestAsJSONErrorSchema >; diff --git a/server/src/lib/router.ts b/server/src/lib/router.ts index 4ab372f..10159a1 100644 --- a/server/src/lib/router.ts +++ b/server/src/lib/router.ts @@ -6,8 +6,8 @@ import { Api } from "@src/lib/apiValidator.ts"; type RequestHandler< S extends string, - ReqSchema extends Schema = never, - ResSchema extends Schema = never, + ReqSchema extends Schema = Schema, + ResSchema extends Schema = Schema, > = (c: Context) => Promise | Response; type RequestHandlerWithSchema = { @@ -148,6 +148,7 @@ class HttpRouter { connInfo: Deno.ServeHandlerInfo, ): Promise { let ctx = new Context(req, connInfo, {}); + let routeParams: Record = {}; const path = this.pathPreprocessor ? this.pathPreprocessor(ctx.path) @@ -161,7 +162,7 @@ class HttpRouter { const route = methodHandler[req.method]; if (!route) return none; if (route.schema) { - ctx = ctx.withSchema(route.schema); + ctx = ctx.setSchema(route.schema); } const handler = route.handler; @@ -172,7 +173,7 @@ class HttpRouter { const res = (await this.executeMiddlewareChain( this.middlewares, handler, - Context.setParams(ctx, routeParams), + ctx = ctx.setParams(routeParams), )).res; return res; @@ -206,10 +207,6 @@ class HttpRouter { return c; } - - private setParams(path: string, params: string[]): Params { - path.split("/").filter((segmet) => segmet.startsWith(":")); - } } export type ExtractRouteParams = T extends string diff --git a/server/src/lib/test.ts b/server/src/lib/test.ts deleted file mode 100644 index ba8ddf2..0000000 --- a/server/src/lib/test.ts +++ /dev/null @@ -1,845 +0,0 @@ -import { err, ok, Result } from "@shared/utils/result.ts"; -import { none, Option, some } from "@shared/utils/option.ts"; - -class ParseError extends Error { - type = "ParseError"; - - public trace: NestedArray = []; - - constructor( - public input: any, - trace: NestedArray | string, - public readonly msg: string, - ) { - super(msg); - - if (Array.isArray(trace)) { - this.trace = trace; - } else { - this.trace = [trace]; - } - } - - stackParseErr(trace: string, input: any): ParseError { - this.trace = [trace, this.trace]; - this.input = input; - return this; - } -} - -function pe(input: unknown, trace: NestedArray, msg: string) { - return new ParseError(input, trace, msg); -} - -export interface Schema { - parse(input: unknown): Result; - checkIfValid(input: unknown): boolean; - nullable(): NullableSchema>; - option(): OptionSchema>; - or[]>(...schema: S): UnionSchema<[this, ...S]>; -} - -type CheckFunction = (input: T) => ParseError | void; - -export abstract class BaseSchema implements Schema { - protected checks: CheckFunction[] = []; - - public addCheck(check: CheckFunction): this { - this.checks.push(check); - return this; - } - - protected runChecks(input: T): Result { - for (const check of this.checks) { - const error = check(input); - if (error) { - return err(error); - } - } - return ok(input); - } - - checkIfValid(input: unknown): boolean { - return this.parse(input).isOk(); - } - - nullable(): NullableSchema> { - return new NullableSchema(this); - } - - or[]>(...schema: S): UnionSchema<[this, ...S]> { - return new UnionSchema(this, ...schema); - } - - option(): OptionSchema> { - return new OptionSchema(this); - } - - abstract parse(input: unknown): Result; -} - -export abstract class PrimitiveSchema extends BaseSchema { - protected abstract initialCheck(input: unknown): Result; - - protected checkPrimitive( - input: unknown, - type: - | "string" - | "number" - | "boolean" - | "bigint" - | "undefined" - | "object" - | "symbol" - | "funciton", - ): Result { - const inputType = typeof input; - - if (inputType === type) { - return ok(input as U); - } - return err( - pe(input, `Expected '${type}', received '${inputType}'`), - ); - } - - public parse(input: unknown): Result { - return this.initialCheck(input).andThen((input) => { - for (const check of this.checks) { - const e = check(input); - - if (e) { - return err(e); - } - } - - return ok(input); - }); - } -} - -export class StringSchema extends PrimitiveSchema { - private static readonly emailRegex = - /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; // https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript - - private static readonly ipRegex = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // https://stackoverflow.com/questions/4460586/javascript-regular-expression-to-check-for-ip-addresses - - protected override initialCheck( - input: unknown, - ): Result { - return this.checkPrimitive(input, "string"); - } - - public max( - length: number, - msg?: string, - ): this { - const trace = `String length must be at most ${length} characters long`; - return this.addCheck((input) => - input.length <= length ? undefined : pe(input, trace, msg) - ); - } - - public min( - length: number, - msg?: string, - ): this { - const trace = - `String length must be at least ${length} characters long`; - return this.addCheck((input) => - input.length >= length ? undefined : pe(input, trace, msg) - ); - } - - public regex( - pattern: RegExp, - msg?: string, - ): this { - const trace = `String length must match the pattern ${String(pattern)}`; - return this.addCheck((input) => - pattern.test(input) ? undefined : pe(input, trace, msg) - ); - } - - public email( - msg?: string, - ): this { - const trace = `String must be a valid email address`; - return this.addCheck((input) => - StringSchema.emailRegex.test(input) - ? undefined - : pe(input, trace, msg) - ); - } - - public ip( - msg?: string, - ): this { - const trace = `String must be a valid ip address`; - return this.addCheck((input) => - StringSchema.ipRegex.test(input) ? undefined : pe(input, trace, msg) - ); - } -} - -export class NumberSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - return this.checkPrimitive(input, "number"); - } - - public gt( - num: number, - msg?: string, - ): this { - const trace = `Number must be greates than ${num}`; - return this.addCheck((input) => - input > num ? undefined : pe(input, trace, msg) - ); - } - - public gte( - num: number, - msg?: string, - ): this { - const trace = `Number must be greates than or equal to ${num}`; - return this.addCheck((input) => - input >= num ? undefined : pe(input, trace, msg) - ); - } - - public lt( - num: number, - msg?: string, - ): this { - const trace = `Number must be less than ${num}`; - return this.addCheck((input) => - input < num ? undefined : pe(input, trace, msg) - ); - } - - public lte( - num: number, - msg?: string, - ): this { - const trace = `Number must be less than or equal to ${num}`; - return this.addCheck((input) => - input <= num ? undefined : pe(input, trace, msg) - ); - } - - public int( - msg?: string, - ): this { - const trace = `Number must be an integer`; - return this.addCheck((input) => - Number.isInteger(input) ? undefined : pe(input, trace, msg) - ); - } - - public positive( - msg?: string, - ): this { - const trace = `Number must be positive`; - return this.addCheck((input) => - input > 0 ? undefined : pe(input, trace, msg) - ); - } - - public nonnegative( - msg?: string, - ): this { - const trace = `Number must be nonnegative`; - return this.addCheck((input) => - input >= 0 ? undefined : pe(input, trace, msg) - ); - } - - public negative( - msg?: string, - ): this { - const trace = `Number must be negative`; - return this.addCheck((input) => - input < 0 ? undefined : pe(input, trace, msg) - ); - } - - public nonpositive( - msg?: string, - ): this { - const trace = `Number must be nonpositive`; - return this.addCheck((input) => - input < 0 ? undefined : pe(input, trace, msg) - ); - } - - public finite( - msg?: string, - ): this { - const trace = `Number must be finite`; - return this.addCheck((input) => - Number.isFinite(input) ? undefined : pe(input, trace, msg) - ); - } - - public safe( - msg?: string, - ): this { - const trace = `Number must be a safe integer`; - return this.addCheck((input) => - Number.isSafeInteger(input) ? undefined : pe(input, trace, msg) - ); - } - - public multipleOf( - num: number, - msg?: string, - ): this { - const trace = `Number must be a multiple of ${num}`; - return this.addCheck((input) => - input % num === 0 ? undefined : pe(input, trace, msg) - ); - } -} - -export class BigintSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - return this.checkPrimitive(input, "bigint"); - } - - public gt( - num: number | bigint, - msg?: string, - ): this { - const trace = `Bigint must be greates than ${num}`; - return this.addCheck((input) => - input > num ? undefined : pe(input, trace, msg) - ); - } - - public gte( - num: number | bigint, - msg?: string, - ): this { - const trace = `Bigint must be greates than or equal to ${num}`; - return this.addCheck((input) => - input >= num ? undefined : pe(input, trace, msg) - ); - } - - public lt( - num: number | bigint, - msg?: string, - ): this { - const trace = `Bigint must be less than ${num}`; - return this.addCheck((input) => - input < num ? undefined : pe(input, trace, msg) - ); - } - - public lte( - num: number | bigint, - msg?: string, - ): this { - const trace = `Bigint must be less than or equal to ${num}`; - return this.addCheck((input) => - input <= num ? undefined : pe(input, trace, msg) - ); - } - - public int( - msg?: string, - ): this { - const trace = `Bigint must be an integer`; - return this.addCheck((input) => - Number.isInteger(input) ? undefined : pe(input, trace, msg) - ); - } - - public positive( - msg?: string, - ): this { - const trace = `Bigint must be positive`; - return this.addCheck((input) => - input > 0 ? undefined : pe(input, trace, msg) - ); - } - - public nonnegative( - msg?: string, - ): this { - const trace = `Bigint must be nonnegative`; - return this.addCheck((input) => - input >= 0 ? undefined : pe(input, trace, msg) - ); - } - - public negative( - msg?: string, - ): this { - const trace = `Bigint must be negative`; - return this.addCheck((input) => - input < 0 ? undefined : pe(input, trace, msg) - ); - } - - public nonpositive( - msg?: string, - ): this { - const trace = `Bigint must be nonpositive`; - return this.addCheck((input) => - input < 0 ? undefined : pe(input, trace, msg) - ); - } - - public finite( - msg?: string, - ): this { - const trace = `Bigint must be finite`; - return this.addCheck((input) => - Number.isFinite(input) ? undefined : pe(input, trace, msg) - ); - } - - public safe( - msg?: string, - ): this { - const trace = `Bigint must be a safe integer`; - return this.addCheck((input) => - Number.isSafeInteger(input) ? undefined : pe(input, trace, msg) - ); - } - - public multipleOf( - num: bigint, - msg?: string, - ): this { - const trace = `Bigint must be a multiple of ${num}`; - return this.addCheck((input) => - input % num === BigInt(0) ? undefined : pe(input, trace, msg) - ); - } -} - -export class BooleanSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - return this.checkPrimitive(input, "boolean"); - } -} - -export class DateSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - return this.checkPrimitive(input, "object").andThen((obj) => { - if (obj instanceof Date) { - return ok(obj); - } - return err( - pe( - input, - `Expected instance of Date, received ${obj.constructor.name}`, - ), - ); - }); - } - - public min( - date: Date, - msg?: string, - ) { - const trace = `Date must be after ${date.toLocaleString()}`; - return this.addCheck((input) => - input >= date ? undefined : pe(input, trace, msg) - ); - } - - public max( - date: Date, - msg?: string, - ) { - const trace = `Date must be before ${date.toLocaleString()}`; - return this.addCheck((input) => - input <= date ? undefined : pe(input, trace, msg) - ); - } -} - -class UndefinedSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - return this.checkPrimitive(input, "undefined"); - } -} - -class NullSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - if (input === null) { - return ok(input); - } - return err(pe(input, "Expected 'null', received '${typeof input}'")); - } -} - -class VoidSchema extends PrimitiveSchema { - protected override initialCheck(input: unknown): Result { - if (input !== undefined && input !== null) { - return err( - pe(input, `Expected 'void', received '${typeof input}'`), - ); - } - - return ok(); - } -} - -class AnySchema extends PrimitiveSchema { - protected override initialCheck(input: any): Result { - return ok(input); - } -} - -class UnknownSchema extends PrimitiveSchema { - protected override initialCheck( - input: unknown, - ): Result { - return ok(input); - } -} - -class ObjectSchema>> - extends PrimitiveSchema<{ [K in keyof O]: InferSchema }> { - private strict: boolean = false; - - constructor( - private readonly schema: O, - ) { - super(); - } - - protected override initialCheck( - input: unknown, - ): Result<{ [K in keyof O]: InferSchema }, ParseError> { - return this.checkPrimitive(input, "object").andThen( - (objPrimitive) => { - let obj = objPrimitive as Record; - let parsedObj: Record = {}; - - for (const [key, schema] of Object.entries(this.schema)) { - const value = obj[key]; - - const checkResult = schema.parse(value); - - if (checkResult.isErr()) { - return err( - checkResult.error.stackParseErr( - `Failed to parse '${key}' attribute`, - input, - ), - ); - } - - parsedObj[key] = checkResult.value; - } - - return ok(parsedObj as { [K in keyof O]: InferSchema }); - }, - ); - } -} - -class NullableSchema> - extends PrimitiveSchema | void> { - private static readonly voidSchema = new VoidSchema(); - - constructor( - private readonly schema: S, - ) { - super(); - } - - protected override initialCheck( - input: unknown, - ): Result, ParseError> { - if (NullableSchema.voidSchema.checkIfValid(input)) { - return ok(); - } - return this.schema.parse(input); - } -} - -class LiteralSchema extends PrimitiveSchema { - constructor( - private readonly literal: L, - ) { - super(); - } - - protected override initialCheck(input: unknown): Result { - if (input === this.literal) { - return ok(this.literal); - } - return err(pe(input, `Input must match literal '${this.literal}'`)); - } -} - -type InferSchemaUnion[]> = S[number] extends - Schema ? U : never; - -class UnionSchema[]> - extends PrimitiveSchema> { - private static readonly schemasTypes: Partial< - Record - > = { - StringSchema: "string", - LiteralSchema: "string", - NumberSchema: "number", - BigintSchema: "bigint", - BooleanSchema: "boolean", - UndefinedSchema: "undefined", - VoidSchema: "undefined", - }; - private readonly primitiveTypesMap: Map[]> = - new Map(); - private readonly othersTypes: Schema[] = []; - - constructor(...schemas: S) { - super(); - - for (const schema of schemas) { - const type = UnionSchema.schemasTypes[schema.constructor.name]; - - if (type !== undefined) { - if (!this.primitiveTypesMap.has(type)) { - this.primitiveTypesMap.set(type, []); - } - const schemasForType = this.primitiveTypesMap.get(type); - schemasForType?.push(schema); - } else { - this.othersTypes.push(schema); - } - } - } - - protected override initialCheck( - input: unknown, - ): Result, ParseError> { - const schemas = this.primitiveTypesMap.get(typeof input) || - this.othersTypes; - - const errors: string[] = []; - - for (const schema of schemas) { - const checkResult = schema.parse(input); - - if (checkResult.isOk()) { - return ok(checkResult.value); - } - - errors.push( - `${schema.constructor.name} - ${ - checkResult.error.trace.join("\n") - }`, - ); - } - - const type = typeof input; - return err( - pe( - input, - [ - `UnionSchema (${ - this.primitiveTypesMap.keys().toArray().join(" | ") - }${ - this.othersTypes.length > 0 - ? "object" - : "" - }) - failed to parse input as any of the schemas:`, - errors.join("\n"), - ].join("\n"), - "Failed to match union", - ), - ); - } -} - -class ArraySchema> - extends PrimitiveSchema[]> { - constructor( - private readonly schema: S, - ) { - super(); - } - - protected override initialCheck( - input: unknown[], - ): Result[], ParseError> { - const parsed = []; - - for (let i = 0; i < input.length; i++) { - const r = this.schema.parse(input[i]); - - if (r.isErr()) { - return err( - pe( - input, - `Array. Failed to parse element at index ${i}:\n${r.error.trace}`, - ), - ); - } - - parsed.push(r.value); - } - - return ok(parsed); - } -} - -class ResultSchema extends PrimitiveSchema> { - private schema; - - constructor( - private readonly valueSchema: Schema, - private readonly errorSchema: Schema, - ) { - super(); - - this.schema = new UnionSchema( - new ObjectSchema({ - tag: new LiteralSchema("ok"), - value: valueSchema, - }), - new ObjectSchema({ - tag: new LiteralSchema("err"), - error: errorSchema, - }), - ); - } - - protected override initialCheck( - input: unknown, - ): Result, ParseError> { - return this.schema.parse(input).map((result) => { - switch (result.tag) { - case "ok": - return ok(result.value); - case "err": - return err(result.error); - } - }); - } -} - -class OptionSchema> - extends PrimitiveSchema>> { - private schema; - - constructor(private readonly valueSchema: S) { - super(); - - this.schema = new UnionSchema( - new ObjectSchema({ - tag: new LiteralSchema("some"), - value: valueSchema, - }), - new ObjectSchema({ - tag: new LiteralSchema("none"), - }), - ); - } - - protected override initialCheck( - input: unknown, - ): Result, ParseError> { - return this.schema.parse(input).map((option) => { - switch (option.tag) { - case "some": - return some(option.value); - case "none": - return none; - } - }); - } -} - -class Validator { - string(): StringSchema { - return new StringSchema(); - } - - literal(literal: L): LiteralSchema { - return new LiteralSchema(literal); - } - - number(): NumberSchema { - return new NumberSchema(); - } - - bigint(): BigintSchema { - return new BigintSchema(); - } - - boolean(): BooleanSchema { - return new BooleanSchema(); - } - - date(): DateSchema { - return new DateSchema(); - } - - undefined(): UndefinedSchema { - return new UndefinedSchema(); - } - - null(): NullSchema { - return new NullSchema(); - } - - void(): VoidSchema { - return new VoidSchema(); - } - - any(): AnySchema { - return new AnySchema(); - } - - unknown(): UnknownSchema { - return new UnknownSchema(); - } - - union[]>(...schemas: S): UnionSchema { - return new UnionSchema(...schemas); - } - - array>(elementSchema: S): ArraySchema { - return new ArraySchema(elementSchema); - } - - result( - valueSchema: Schema, - errorSchema: Schema, - ): ResultSchema { - return new ResultSchema(valueSchema, errorSchema); - } -} - -const v = new Validator(); - -const r = v.string().max(4, "too long").or(v.number()); - -const res = r.parse(some("11234")); - -console.log(res); - -type InferSchema = S extends Schema ? T : never; - -type NestedArray = T | NestedArray[];