From 699570648fc702414abd4f3fb32da085aff08bde Mon Sep 17 00:00:00 2001 From: ton1c Date: Sat, 1 Feb 2025 02:49:40 +0300 Subject: [PATCH] validator is close to be finished --- server/src/lib/test.ts | 10 - server/src/lib/test1.ts | 19 + server/src/lib/validator.ts | 813 +++++++++++++++++++++++------------- test.md | 21 + test.ts | 1 + test1.ts | 351 ++++++++++++++++ 6 files changed, 912 insertions(+), 303 deletions(-) create mode 100644 server/src/lib/test1.ts create mode 100644 test.md create mode 100644 test.ts create mode 100644 test1.ts diff --git a/server/src/lib/test.ts b/server/src/lib/test.ts index d70dff7..ba8ddf2 100644 --- a/server/src/lib/test.ts +++ b/server/src/lib/test.ts @@ -595,16 +595,6 @@ class LiteralSchema extends PrimitiveSchema { type InferSchemaUnion[]> = S[number] extends Schema ? U : never; -type TypeOfString = - | "string" - | "number" - | "bigint" - | "boolean" - | "symbol" - | "undefined" - | "object" - | "function"; - class UnionSchema[]> extends PrimitiveSchema> { private static readonly schemasTypes: Partial< diff --git a/server/src/lib/test1.ts b/server/src/lib/test1.ts new file mode 100644 index 0000000..4b76884 --- /dev/null +++ b/server/src/lib/test1.ts @@ -0,0 +1,19 @@ +class ParseError extends Error { + type = "ParseError"; + + public trace: NestedArray = []; + + constructor( + public input: any, + trace: NestedArray | string, + public readonly msg: string, + ) { + super(msg); + } +} + +type NestedArray = T | NestedArray[]; + +export interface Schema { + parse(input: unknown): Result; +} diff --git a/server/src/lib/validator.ts b/server/src/lib/validator.ts index 73abd61..758c29d 100644 --- a/server/src/lib/validator.ts +++ b/server/src/lib/validator.ts @@ -1,344 +1,571 @@ import { err, ok, Result } from "@shared/utils/result.ts"; -import console from "node:console"; +// ── Error Types ───────────────────────────────────────────────────── +type ParseErrorDetail = + | { + type: "typeError"; + expected: string; + received: string; + msg?: string; + } + | { + type: "objectStrictError" | "objectKeyError"; + key: string; + detail?: ParseErrorDetail; + msg?: string; + } + | { + type: "unionTypeError" | "unionMatchError"; + details?: ParseErrorDetail[]; + msg?: string; + } + | { + type: "arrayError"; + index: number; + detail: ParseErrorDetail; + msg?: string; + } + | { type: "validationError"; msg: string }; class ParseError extends Error { - code = "ParseError"; + public readonly type = "ParseError"; + constructor( - public readonly input: any, - public readonly msg: string, + public readonly input: unknown, + public readonly detail: ParseErrorDetail, + msg?: string, + ) { + super(msg ?? `Validation failed: ${JSON.stringify(detail)}`); + } + + public format(): Record { + return { + input: this.input, + failedAt: ParseError.formatDetail(this.detail), + }; + } + + private static formatDetail(detail: ParseErrorDetail): any { + switch (detail.type) { + case "typeError": + return detail.msg ?? + `Expected ${detail.expected}, received ${detail.received}`; + case "objectKeyError": + return { + [detail.key]: this.formatDetail(detail.detail!) || + detail.msg, + }; + case "objectStrictError": + return { + [detail.key]: detail.msg, + }; + case "arrayError": + return { + [`index_${detail.index}`]: this.formatDetail( + detail.detail!, + ) || detail.msg, + }; + case "unionMatchError": + return detail.details?.map((err): any => + this.formatDetail(err) + ); + case "unionTypeError": + return detail.msg; + case "validationError": + return detail.msg; + + default: + return "Unknown error type"; + } + } +} + +function createParseError(input: any, error: ParseErrorDetail, msg?: string) { + return new ParseError(input, error, msg); +} + +type TypeofEnum = + | "string" + | "number" + | "bigint" + | "boolean" + | "symbol" + | "undefined" + | "object" + | "function"; + +// ── Core Schema Types ─────────────────────────────────────────────── +export interface Schema { + parse(input: unknown): Result; +} + +type ValidationCheck = (value: T) => ParseError | void; + +export abstract class BaseSchema implements Schema { + protected checks: ValidationCheck[] = []; + + constructor(public readonly msg?: string) {} + + public parse(input: unknown): Result { + return this.validateType(input).andThen((value) => + this.applyChecks(value) + ); + } + + protected abstract validateType(input: unknown): Result; + + protected static validatePrimitive( + input: unknown, + expectedType: TypeofEnum, + msg?: string, + ): Result { + const receivedType = typeof input; + return receivedType === expectedType ? ok(input as U) : err( + createParseError(input, { + type: "typeError", + expected: expectedType, + received: receivedType, + msg: msg || + `Expected ${expectedType} but received ${receivedType}`, + }), + ); + } + + public addCheck(check: ValidationCheck): this { + this.checks.push(check); + return this; + } + + protected applyChecks(value: T): Result { + for (const check of this.checks) { + const error = check(value); + if (error) { + return err(error); + } + } + return ok(value); + } + + private static flattenUnion[]>( + ...schemas: S + ): Schema[] { + return schemas.map((s) => + s instanceof UnionSchema ? this.flattenUnion(s) : s + ).flat(); + } + + public or[]>( + ...schemas: S + ): UnionSchema<[this, ...S]> { + return new UnionSchema([this, ...schemas]); + } +} + +class StringSchema extends BaseSchema { + readonly type = "string"; + + protected override validateType( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive(input, "string"); + } + + public max(len: number): this { + return this.addCheck((value) => { + if (value.length > len) { + return createParseError(value, { + type: "validationError", + msg: `String must be at most ${len} characters long`, + }); + } + }); + } +} + +class LiteralSchema extends BaseSchema { + constructor( + public readonly literal: L, + msg?: string, ) { super(msg); } -} -export function pe(input: any, msg: string): ParseError { - return new ParseError(input, msg); -} - -type CheckFunction = ( - input: T, - msg?: string, -) => ParseError | void; - -export abstract class BaseSchema { - private checks: CheckFunction[] = []; - abstract initialCheck(input: unknown): Result; - public addCheck(check: CheckFunction) { - this.checks.push(check); - } - - parse(input: any): 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); - }); + protected override validateType(input: unknown): Result { + return BaseSchema.validatePrimitive(input, "string", this.msg) + .andThen((str) => + str === this.literal ? ok(str as L) : err( + createParseError( + input, + { + type: "typeError", + expected: this.literal, + received: str, + msg: `Expected '${this.literal}' but received '${str}'`, + }, + ), + ) + ); } } -export class StringSchema extends BaseSchema { - 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 - - private initialCheck(input: unknown): Result { - if (typeof input !== "string") { - return err(pe(input, `Expected string, received ${typeof input}`)); - } - return ok(input); +class NumberSchema extends BaseSchema { + protected override validateType( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive(input, "number", this.msg); } +} - max( - length: number, - msg: string = `String length must be at most ${length} characters long`, - ): this { - this.addCheck((input) => { - if (input.length > length) { - return pe( +class BigintSchema extends BaseSchema { + protected override validateType( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive(input, "bigint", this.msg); + } +} + +class BooleanSchema extends BaseSchema { + protected override validateType( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive( + input, + "boolean", + this.msg, + ); + } +} + +class DateSchema extends BaseSchema { + protected override validateType(input: unknown): Result { + return BaseSchema.validatePrimitive(input, "object", this.msg) + .andThen((o) => { + if (o instanceof Date) return ok(o as Date); + + const received = o?.constructor?.name ?? "unknown"; + return err(createParseError( input, - msg, - ); - } - }); - - return this; - } - - min( - length: number, - msg: string = - `String length must be at least ${length} characters long`, - ): this { - this.addCheck((input) => { - if (input.length < length) { - return pe( - input, - msg, - ); - } - }); - return this; - } - - regex( - pattern: RegExp, - msg: string = `String must match the pattern ${String(pattern)}`, - ): this { - this.addCheck((input) => { - if (!pattern.test(input)) { - return pe( - input, - msg, - ); - } - }); - return this; - } - - email(msg: string = `String must be a valid email address`): this { - return this.regex(StringSchema.emailRegex, msg); - } - ip(msg: string = "String must be a valid ip address"): this { - return this.regex(StringSchema.ipRegex, msg); + { + type: "typeError", + expected: "Date instance", + received, + msg: `Expected a Date instance but received ${received}`, + }, + )); + }); } } -export class NumberSchema extends BaseSchema { - private initialCheck(input: unknown): Result { - if (typeof input !== "number") { - return err(pe(input, `Expected number, recieved ${typeof input}`)); - } - return ok(input); - } - - gt(num: number, msg: string = `Number must be greater than ${num}`): this { - this.addCheck((input) => { - if (input <= num) return pe(input, msg); - }); - return this; - } - - gte( - num: number, - msg: string = `Number must be greater than or equal to ${num}`, - ): this { - this.addCheck((input) => { - if (input < num) return pe(input, msg); - }); - return this; - } - - lt(num: number, msg: string = `Number must be less than ${num}`): this { - this.addCheck((input) => { - if (input >= num) return pe(input, msg); - }); - return this; - } - - lte( - num: number, - msg: string = `Number must be less than or equal to ${num}`, - ): this { - this.addCheck((input) => { - if (input > num) return pe(input, msg); - }); - return this; - } - - int(msg: string = "Number must be an integer"): this { - this.addCheck((input) => { - if (!Number.isInteger(input)) return pe(input, msg); - }); - return this; - } - - positive(msg: string = "Number must be positive"): this { - return this.gte(0, msg); - } - - nonnegative(msg: string = "Number must be nonnegative"): this { - return this.gt(0, msg); - } - - negative(msg: string = "Number must be negative"): this { - return this.lt(0, msg); - } - - nonpositive(msg: string = "Number must be nonpositive"): this { - return this.lte(0, msg); +class SymbolSchema extends BaseSchema { + protected override validateType( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive(input, "symbol", this.msg); } } -export class BooleanSchema extends BaseSchema { - private initialCheck(input: unknown): Result { - if (typeof input !== "boolean") { - return err(pe(input, `Expected boolean, received ${typeof input}`)); - } - return ok(input); +class UndefinedSchema extends BaseSchema { + protected override validateType( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive( + input, + "undefined", + this.msg, + ); } } -export class DateSchema extends BaseSchema { - override initialCheck(input: unknown): Result { - if (typeof input === "object" && input !== null) { - if (input instanceof Date) { - return ok(input); - } - // TODO: add check for an non Date instance. Right now lsp freaks out for some reason - } +class NullSchema extends BaseSchema { + protected override validateType(input: unknown): Result { + if (input === null) return ok(input); - return err(pe(input, `Expected object, received ${typeof input}`)); - } - - min( - date: Date, - msg: string = `Date must come after ${date.toLocaleString()}`, - ) { - this.addCheck((input) => { - if (input < date) { - return pe(input, msg); - } - }); - return this; - } - - max( - date: Date, - msg: string = `Date must come before ${date.toLocaleString()}`, - ) { - this.addCheck((input) => { - if (input > date) { - return pe(input, msg); - } - }); - return this; - } -} - -const validatorsObjects = [ - StringSchema.name, - NumberSchema.name, - BooleanSchema.name, - DateSchema.name, -]; - -class UnionSchema { - private readonly schemas: Schema[]; - - constructor(...schemas: Schema[]) { - this.schemas = schemas; - } - - parse(input: unknown): Result, ParseError> { - for (const schema of this.schemas) { - const result = schema.parse(input); - - if (result.isOk()) { - return result; - } - } + const received = typeof input === "object" + ? input?.constructor?.name ?? "unknown" + : typeof input; return err( - pe( + createParseError( input, - `Union. Failed to parse a any of the schemas: ${ - this.schemas.map((s) => s.constructor.name).join(", ") - }`, + { + type: "typeError", + expected: "null", + received, + msg: this.msg, + }, ), ); } } -type Schema = - | NumberSchema - | StringSchema - | BooleanSchema - | DateSchema - | ObjectSchema> - | UnionSchema; +class VoidSchema extends BaseSchema { + protected override validateType(input: unknown): Result { + if (input === undefined || input === null) return ok(); -export class ObjectSchema< - S extends Record, -> { - // TODO: rewrite this to be a static method returning result + const received = typeof input === "object" + ? input?.constructor?.name ?? "unknown" + : typeof input; + + return err(createParseError( + input, + { + type: "typeError", + expected: "void (undefined/null)", + received, + msg: this.msg, + }, + )); + } +} + +class AnySchema extends BaseSchema { + protected override validateType(input: unknown): Result { + return ok(input); + } +} + +class UnknownSchema extends BaseSchema { + protected override validateType( + input: unknown, + ): Result { + return ok(input); + } +} + +class NeverSchema extends BaseSchema { + protected override validateType(input: unknown): Result { + return err( + createParseError( + input, + { + type: "typeError", + expected: "never", + received: typeof input, + msg: "No values are allowed for this schema (never)", + }, + ), + ); + } +} + +type InferSchemaType = S extends Schema ? T : never; + +class ObjectSchema>> + extends BaseSchema<{ [K in keyof S]: InferSchemaType }> { + private strictMode: boolean = false; + + constructor(private readonly shape: S, msg?: string) { + super(msg); + } + + protected override validateType( + input: unknown, + ): Result<{ [K in keyof S]: InferSchemaType }, ParseError> { + return BaseSchema.validatePrimitive(input, "object", this.msg) + .andThen((obj) => { + if (obj === null) { + return err( + createParseError(input, { + type: "typeError", + expected: "Non-null object", + received: "null", + msg: "Expected a non-null object", + }), + ); + } + + let resultObj: Record = {}; + + for (const key of Object.keys(obj)) { + const schema = this.shape[key]; + + if (schema === undefined) { + if (this.strictMode) { + return err( + createParseError( + input, + { + type: "objectStrictError", + key, + msg: `Encountered a key (${key}) that is not in a strict object schema`, + }, + ), + ); + } + continue; + } + + const result = schema.parse( + (obj as any)[key], + ); + if (result.isErr()) { + return err( + createParseError( + input, + { + type: "objectKeyError", + key, + detail: result.error.detail, + msg: this.msg, + }, + ), + ); + } + resultObj[key] = result.value; + } + + return ok( + resultObj as { [K in keyof S]: InferSchemaType }, + ); + }); + } + + strict(): this { + this.strictMode = true; + return this; + } +} + +type InferUnionSchemaType[]> = S[number] extends + Schema ? T : never; + +class UnionSchema[]> + extends BaseSchema> { + constructor(public readonly schemas: U, msg?: string) { + super(msg); + } + + private static getTypeFromSchemaName(name: string): string { + switch (name) { + case "StringSchema": + case "LiteralSchema": + return "string"; + case "NumberSchema": + return "number"; + case "BigintSchema": + return "bigint"; + case "BooleanSchema": + return "boolean"; + case "UndefinedSchema": + return "undefined"; + case "SymbolSchema": + return "symbol"; + default: + return "object"; + } + } + + protected validateType( + input: unknown, + ): Result, ParseError> { + const errors: ParseErrorDetail[] = []; + + let typeDoesNotMatch = true; + + for (const schema of this.schemas) { + const result = schema.parse(input); + if (result.isOk()) { + return ok(result.value); + } + typeDoesNotMatch = result.error.detail?.type === "typeError" && + typeDoesNotMatch; + errors.push(result.error.detail); + } + + if (typeDoesNotMatch) { + return err(createParseError(input, { + type: "unionTypeError", + msg: `Input (${typeof input}) did not match any of the union's types (${ + this.schemas.map((s) => + UnionSchema.getTypeFromSchemaName(s.constructor.name) + ).join(" | ") + })`, + })); + } + + return err( + createParseError(input, { + type: "unionMatchError", + msg: "Input did not match any union member", + details: errors, + }), + ); + } +} + +class ArraySchema> + extends BaseSchema[]> { constructor( private readonly schema: S, + msg?: string, ) { - for (const [_, value] of Object.entries(schema)) { - if (validatorsObjects.indexOf(schema.constructor.name) === -1) { - throw "Not a valid ObjectSchema >:("; - } - } - - console.log("Yay! It is valid"); + super(msg); } - parse( + protected override validateType( input: unknown, - ): Result<{ [K in keyof S]: InferTypeFromSchema }, ParseError> { - if (typeof input !== "object") { - return err(pe(input, `Expected object, received ${typeof input}`)); + ): Result[], ParseError> { + if (!Array.isArray(input)) { + return err(createParseError(input, { + type: "validationError", + msg: `Expected an array`, + })); } - if (input === null) { - return err(pe(input, `Expected object, received null`)); - } + for (let i = 0; i < input.length; i++) { + const r = this.schema.parse(input[i]); - const obj = input as Record; - const resultObj: Partial> = {}; - - for (const [key, schema] of Object.entries(this.schema)) { - const inputValue = obj[key]; - - if (!inputValue) { - return err(pe( - input, - `Object ${input} missing the '${key}' attribute`, - )); + if (r.isErr()) { + return err( + createParseError(input, { + type: "arrayError", + index: i, + detail: r.error.detail, + msg: `Failed to parse an element at index ${i}`, + }), + ); } - - const result = schema.parse(inputValue); - - if (result.isErr()) { - return err(result.error); - } - - resultObj[key as keyof S] = result.value; } - return ok(resultObj); + return ok(input as InferSchemaType[]); } } -type InferTypeFromSchema | Schema[]> = - S extends Record - ? { [K in keyof S]: InferTypeFromSchema } - : S extends StringSchema ? string - : S extends NumberSchema ? number - : S extends DateSchema ? Date - : S extends BooleanSchema ? boolean - : S extends ObjectSchema ? InferTypeFromSchema - : S extends UnionSchema ? InferTypeFromSchema - : never; - -export const s = { - string: () => new StringSchema(), - number: () => new NumberSchema(), - date: () => new DateSchema(), +const z = { + string: (msg?: string) => new StringSchema(msg), + literal: (lit: L, msg?: string) => + new LiteralSchema(lit, msg), + number: (msg?: string) => new NumberSchema(msg), + bigint: (msg?: string) => new BigintSchema(msg), + boolean: (msg?: string) => new BooleanSchema(msg), + date: (msg?: string) => new DateSchema(msg), + symbol: (msg?: string) => new StringSchema(msg), + undefined: (msg?: string) => new UndefinedSchema(msg), + null: (msg?: string) => new NullSchema(msg), + void: (msg?: string) => new VoidSchema(msg), + any: (msg?: string) => new AnySchema(msg), + unknown: (msg?: string) => new UnknownSchema(msg), + never: (msg?: string) => new NeverSchema(msg), + obj: >>(schema: S, msg?: string) => + new ObjectSchema(schema, msg), + union: []>(schemas: U, msg?: string) => + new UnionSchema(schemas, msg), }; -function createObjectSchema>( - schema: S, -): ObjectSchema { - return new ObjectSchema(schema); +const schema = z.obj({ + test: z.union([ + z.string().max(2), + z.number(), + z.bigint(), + ]), + test1: z.literal("ok"), +}).strict(); + +const union = z.string().or(z.number().or(z.boolean())); + +const r = schema.parse({ test: "123", test1: "ok" }); + +if (r.isErr()) { + console.log(r.error.format()); +} else { + console.log(r.value); } - -const union = new UnionSchema(s.string(), s.number()); - -console.log(union.parse({})); diff --git a/test.md b/test.md new file mode 100644 index 0000000..020f500 --- /dev/null +++ b/test.md @@ -0,0 +1,21 @@ +StringSchema -> ParseError("too long") + +Object { + test: StringSchema | NumberSchema, +} + +{ + test: true +} + +test: StringSchema -> error +test: NumberSchema -> error + +Union: Neither StringSchema nor NumberSchema worked + +Object -> [ + test: Union -> [ + StringSchema -> error + NumberSchema -> error + ] +] diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..80c9353 --- /dev/null +++ b/test.ts @@ -0,0 +1 @@ +console.log(typeof null); diff --git a/test1.ts b/test1.ts new file mode 100644 index 0000000..e9f9f75 --- /dev/null +++ b/test1.ts @@ -0,0 +1,351 @@ +// Utility Types and Functions +type NestedPath = string[]; + +class ParseError extends Error { + public type: string = "ParseError"; + public path: NestedPath; // Changed from 'trace' to 'path' for clarity + public input: any; + + constructor(input: any, path: NestedPath, msg: string) { + super(msg); + this.input = input; + this.path = path; + } + + // Method to prepend a new path segment + prependPath(segment: string): ParseError { + return new ParseError( + this.input, + [segment, ...this.path], + this.message, + ); + } + + // Method to append a new path segment + appendPath(segment: string): ParseError { + return new ParseError( + this.input, + [...this.path, segment], + this.message, + ); + } + + // Format the error message with the path + formattedMessage(): string { + return `Error at "${this.path.join(".") || "root"}": ${this.message}`; + } +} + +function createParseError( + input: any, + path: NestedPath, + msg: string, +): ParseError { + return new ParseError(input, path, msg); +} + +// Base Schema Classes +export interface Schema { + parse(input: unknown, path?: NestedPath): Result; + checkIfValid(input: unknown): boolean; + nullable(): NullableSchema>; + option(): OptionSchema>; + or[]>(...schema: S): UnionSchema<[this, ...S]>; +} + +type CheckFunction = (input: T, path: NestedPath) => 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, path: NestedPath): Result { + for (const check of this.checks) { + const error = check(input, path); + 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, path?: NestedPath): Result; +} + +export abstract class PrimitiveSchema extends BaseSchema { + protected abstract initialCheck( + input: unknown, + path: NestedPath, + ): Result; + + protected checkPrimitive( + input: unknown, + type: + | "string" + | "number" + | "bigint" + | "boolean" + | "symbol" + | "undefined" + | "object" + | "function", + path: NestedPath, + ): Result { + const inputType = typeof input; + + if (inputType === type) { + return ok(input as U); + } + return err( + createParseError( + input, + path, + `Expected type '${type}', received '${inputType}'`, + ), + ); + } + + public parse(input: unknown, path: NestedPath = []): Result { + return this.initialCheck(input, path).andThen((value) => + this.runChecks(value, path) + ); + } +} + +// Example: StringSchema with Improved Error Handling +export class StringSchema extends PrimitiveSchema { + private static readonly emailRegex = + /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + + private static readonly ipRegex = + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(?!$)|$){4}$/; + + protected initialCheck( + input: unknown, + path: NestedPath, + ): Result { + return this.checkPrimitive(input, "string", path); + } + + public max(length: number, msg?: string): this { + return this.addCheck((input, path) => { + if (input.length <= length) return; + return createParseError( + input, + path, + msg || + `String length must be at most ${length} characters long`, + ); + }); + } + + public min(length: number, msg?: string): this { + return this.addCheck((input, path) => { + if (input.length >= length) return; + return createParseError( + input, + path, + msg || + `String length must be at least ${length} characters long`, + ); + }); + } + + public regex(pattern: RegExp, msg?: string): this { + return this.addCheck((input, path) => { + if (pattern.test(input)) return; + return createParseError( + input, + path, + msg || `String does not match the pattern ${pattern}`, + ); + }); + } + + public email(msg?: string): this { + return this.regex( + StringSchema.emailRegex, + msg || "Invalid email address", + ); + } + + public ip(msg?: string): this { + return this.regex(StringSchema.ipRegex, msg || "Invalid IP address"); + } +} + +// Refactored ObjectSchema with Improved Error Handling +class ObjectSchema>> + extends BaseSchema<{ [K in keyof O]: InferSchema }> { + constructor(private readonly schema: O) { + super(); + } + + protected initialCheck( + input: unknown, + path: NestedPath, + ): Result<{ [K in keyof O]: InferSchema }, ParseError> { + if (typeof input !== "object" || input === null) { + return err( + createParseError( + input, + path, + `Expected an object, received '${typeof input}'`, + ), + ); + } + + const obj = input as Record; + const parsedObj: Partial<{ [K in keyof O]: InferSchema }> = {}; + + for (const key in this.schema) { + const value = obj[key]; + const fieldPath = [...path, key]; + const result = this.schema[key].parse(value, fieldPath); + + if (result.isErr()) { + return err(result.error); + } + + parsedObj[key] = result.value; + } + + return ok(parsedObj as { [K in keyof O]: InferSchema }); + } +} + +// Refactored UnionSchema with Simplified Error Handling +class UnionSchema[]> + extends BaseSchema> { + constructor(...schemas: S) { + super(); + this.schemas = schemas; + } + + private schemas: S; + + protected initialCheck( + input: unknown, + path: NestedPath, + ): Result, ParseError> { + const errors: ParseError[] = []; + + for (const schema of this.schemas) { + const result = schema.parse(input, path); + if (result.isOk()) { + return ok(result.value); + } + errors.push(result.error); + } + + // Combine error messages for better readability + const combinedMessage = errors.map((err) => err.formattedMessage()) + .join(" | "); + return err( + createParseError( + input, + path, + `Union validation failed: ${combinedMessage}`, + ), + ); + } +} + +// Refactored ArraySchema with Improved Error Handling +class ArraySchema> extends BaseSchema[]> { + constructor(private readonly schema: S) { + super(); + } + + protected initialCheck( + input: unknown, + path: NestedPath, + ): Result[], ParseError> { + if (!Array.isArray(input)) { + return err( + createParseError( + input, + path, + `Expected an array, received '${typeof input}'`, + ), + ); + } + + const parsedArray: InferSchema[] = []; + + for (let i = 0; i < input.length; i++) { + const elementPath = [...path, `${i}`]; + const result = this.schema.parse(input[i], elementPath); + if (result.isErr()) { + return err(result.error); + } + parsedArray.push(result.value); + } + + return ok(parsedArray); + } +} + +// NullableSchema, OptionSchema, and Other Schemas would follow similar patterns, +// ensuring that error paths are correctly managed and messages are clear. + +// Example Validator Usage +class Validator { + string(): StringSchema { + return new StringSchema(); + } + + // ... other schema methods remain unchanged ... + + union[]>(...schemas: S): UnionSchema { + return new UnionSchema(...schemas); + } + + array>(elementSchema: S): ArraySchema { + return new ArraySchema(elementSchema); + } + + // ... other schema methods remain unchanged ... +} + +const v = new Validator(); + +// Example Usage with Improved Error Handling +const schema = v.union( + v.string().max(4, "String exceeds maximum length of 4"), + v.string().min(10, "Number must be at least 10"), +); + +const res = schema.parse("11234", ["input"]); + +if (res.isErr()) { + console.error(res.error.formattedMessage()); +} else { + console.log("Parsed Value:", res.value); +} + +// Utility Types +type InferSchema = S extends Schema ? T : never; +type InferSchemaUnion[]> = S[number] extends + Schema ? U : never; +type NestedArray = T | NestedArray[];