From fc1605cdb2536dad49fe05655aa58c348e16b04f Mon Sep 17 00:00:00 2001 From: ton1c Date: Wed, 29 Jan 2025 20:21:44 +0300 Subject: [PATCH] working on validator --- server/src/lib/test.ts | 442 ++++++++++++++++++++++++------------ server/src/lib/validator.ts | 2 +- shared/utils/option.ts | 4 +- shared/utils/result.ts | 4 +- 4 files changed, 298 insertions(+), 154 deletions(-) diff --git a/server/src/lib/test.ts b/server/src/lib/test.ts index c09f957..d70dff7 100644 --- a/server/src/lib/test.ts +++ b/server/src/lib/test.ts @@ -3,23 +3,39 @@ import { none, Option, some } from "@shared/utils/option.ts"; class ParseError extends Error { type = "ParseError"; + + public trace: NestedArray = []; + constructor( - public readonly input: any, + 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, msg: string) { - return new ParseError(input, msg); +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; + nullable(): NullableSchema>; + option(): OptionSchema>; or[]>(...schema: S): UnionSchema<[this, ...S]>; } @@ -47,7 +63,7 @@ export abstract class BaseSchema implements Schema { return this.parse(input).isOk(); } - nullable(): NullableSchema { + nullable(): NullableSchema> { return new NullableSchema(this); } @@ -55,27 +71,14 @@ export abstract class BaseSchema implements Schema { return new UnionSchema(this, ...schema); } - option(): OptionSchema { + option(): OptionSchema> { return new OptionSchema(this); } abstract parse(input: unknown): Result; } -export abstract class PrimitiveSchema< - T extends - | string - | number - | boolean - | bigint - | undefined - | null - | object - | void - | any - | unknown - | never, -> extends BaseSchema { +export abstract class PrimitiveSchema extends BaseSchema { protected abstract initialCheck(input: unknown): Result; protected checkPrimitive( @@ -130,42 +133,53 @@ export class StringSchema extends PrimitiveSchema { public max( length: number, - msg: string = `String length must be at most ${length} characters long`, + msg?: string, ): this { + const trace = `String length must be at most ${length} characters long`; return this.addCheck((input) => - input.length < length ? pe(input, msg) : undefined + input.length <= length ? undefined : pe(input, trace, msg) ); } public min( length: number, - msg: string = - `String length must be at least ${length} characters long`, + msg?: string, ): this { + const trace = + `String length must be at least ${length} characters long`; return this.addCheck((input) => - input.length < length ? pe(input, msg) : undefined + input.length >= length ? undefined : pe(input, trace, msg) ); } public regex( pattern: RegExp, - msg: string = `String must match the patter ${String(pattern)}`, + msg?: string, ): this { + const trace = `String length must match the pattern ${String(pattern)}`; return this.addCheck((input) => - pattern.test(input) ? undefined : pe(input, msg) + pattern.test(input) ? undefined : pe(input, trace, msg) ); } public email( - msg: string = `String must match a valid email address`, + msg?: string, ): this { - return this.regex(StringSchema.emailRegex, msg); + 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 = `String must match a valid ip address`, + msg?: string, ): this { - return this.regex(StringSchema.ipRegex, msg); + const trace = `String must be a valid ip address`; + return this.addCheck((input) => + StringSchema.ipRegex.test(input) ? undefined : pe(input, trace, msg) + ); } } @@ -176,76 +190,116 @@ export class NumberSchema extends PrimitiveSchema { return this.checkPrimitive(input, "number"); } - gt(num: number, msg: string = `Number must be greater than ${num}`): this { - return this.addCheck((input) => - input <= num ? pe(input, msg) : undefined - ); - } - - gte( + public gt( num: number, - msg: string = `Number must be greater than or equal to ${num}`, + msg?: string, ): this { + const trace = `Number must be greates than ${num}`; return this.addCheck((input) => - input < num ? pe(input, msg) : undefined + input > num ? undefined : pe(input, trace, msg) ); } - lt(num: number, msg: string = `Number must be less than ${num}`): this { - return this.addCheck((input) => - input >= num ? pe(input, msg) : undefined - ); - } - - lte( + public gte( num: number, - msg: string = `Number must be less than or equal to ${num}`, + msg?: string, ): this { + const trace = `Number must be greates than or equal to ${num}`; return this.addCheck((input) => - input > num ? pe(input, msg) : undefined + input >= num ? undefined : pe(input, trace, msg) ); } - int(msg: string = "Number must be an integer"): this { - return this.addCheck((input) => - Number.isInteger(input) ? pe(input, msg) : undefined - ); - } - - 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); - } - - finite(msg: string = "Number must be finite"): this { - return this.addCheck((input) => - Number.isFinite(input) ? undefined : pe(input, msg) - ); - } - - safe(msg: string = "Number must be a safe integer"): this { - return this.addCheck((input) => - Number.isSafeInteger(input) ? undefined : pe(input, msg) - ); - } - - multipleOf( + public lt( num: number, - msg: string = `Number must be a multiple of ${num}`, + msg?: string, ): this { + const trace = `Number must be less than ${num}`; return this.addCheck((input) => - input % num ? undefined : pe(input, msg) + 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) ); } } @@ -257,82 +311,116 @@ export class BigintSchema extends PrimitiveSchema { return this.checkPrimitive(input, "bigint"); } - gt( - num: bigint | number, - msg: string = `Bigint must be greater than ${num}`, + public gt( + num: number | bigint, + msg?: string, ): this { + const trace = `Bigint must be greates than ${num}`; return this.addCheck((input) => - input <= num ? pe(input, msg) : undefined + input > num ? undefined : pe(input, trace, msg) ); } - gte( - num: bigint | number, - msg: string = `Bigint must be greater than or equal to ${num}`, + 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 ? pe(input, msg) : undefined + input >= num ? undefined : pe(input, trace, msg) ); } - lt( - num: bigint | number, - msg: string = `Bigint must be less than ${num}`, + public lt( + num: number | bigint, + msg?: string, ): this { + const trace = `Bigint must be less than ${num}`; return this.addCheck((input) => - input >= num ? pe(input, msg) : undefined + input < num ? undefined : pe(input, trace, msg) ); } - lte( - num: bigint | number, - msg: string = `Bigint must be less than or equal to ${num}`, + 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 ? pe(input, msg) : undefined + input <= num ? undefined : pe(input, trace, msg) ); } - int(msg: string = "Bigint must be an integer"): this { + public int( + msg?: string, + ): this { + const trace = `Bigint must be an integer`; return this.addCheck((input) => - Number.isInteger(input) ? pe(input, msg) : undefined + Number.isInteger(input) ? undefined : pe(input, trace, msg) ); } - positive(msg: string = "Bigint must be positive"): this { - return this.gte(0, msg); - } - - nonnegative(msg: string = "Bigint must be nonnegative"): this { - return this.gt(0, msg); - } - - negative(msg: string = "Bigint must be negative"): this { - return this.lt(0, msg); - } - - nonpositive(msg: string = "Bigint must be nonpositive"): this { - return this.lte(0, msg); - } - - finite(msg: string = "Bigint must be finite"): this { + public positive( + msg?: string, + ): this { + const trace = `Bigint must be positive`; return this.addCheck((input) => - Number.isFinite(input) ? undefined : pe(input, msg) + input > 0 ? undefined : pe(input, trace, msg) ); } - safe(msg: string = "Bigint must be a safe integer"): this { + public nonnegative( + msg?: string, + ): this { + const trace = `Bigint must be nonnegative`; return this.addCheck((input) => - Number.isSafeInteger(input) ? undefined : pe(input, msg) + input >= 0 ? undefined : pe(input, trace, msg) ); } - multipleOf( + 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 = `Bigint must be a multiple of ${num}`, + msg?: string, ): this { + const trace = `Bigint must be a multiple of ${num}`; return this.addCheck((input) => - input % num ? undefined : pe(input, msg) + input % num === BigInt(0) ? undefined : pe(input, trace, msg) ); } } @@ -362,21 +450,23 @@ export class DateSchema extends PrimitiveSchema { }); } - min( + public min( date: Date, - msg: string = `Date must be after ${date.toLocaleString()}`, + msg?: string, ) { + const trace = `Date must be after ${date.toLocaleString()}`; return this.addCheck((input) => - input <= date ? pe(input, msg) : undefined + input >= date ? undefined : pe(input, trace, msg) ); } - max( + public max( date: Date, - msg: string = `Date must be before ${date.toLocaleString()}`, + msg?: string, ) { + const trace = `Date must be before ${date.toLocaleString()}`; return this.addCheck((input) => - input >= date ? pe(input, msg) : undefined + input <= date ? undefined : pe(input, trace, msg) ); } } @@ -413,7 +503,7 @@ class VoidSchema extends PrimitiveSchema { } class AnySchema extends PrimitiveSchema { - protected override initialCheck(input: unknown): Result { + protected override initialCheck(input: any): Result { return ok(input); } } @@ -426,8 +516,6 @@ class UnknownSchema extends PrimitiveSchema { } } -type InferSchema = S extends Schema ? T : never; - class ObjectSchema>> extends PrimitiveSchema<{ [K in keyof O]: InferSchema }> { private strict: boolean = false; @@ -453,9 +541,9 @@ class ObjectSchema>> if (checkResult.isErr()) { return err( - pe( + checkResult.error.stackParseErr( + `Failed to parse '${key}' attribute`, input, - `Failed to parse '${key}' attribute: ${checkResult.error.msg}`, ), ); } @@ -507,21 +595,60 @@ 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 readonly schemas: S; + 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(); - this.schemas = schemas; + + 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 this.schemas) { + for (const schema of schemas) { const checkResult = schema.parse(input); if (checkResult.isOk()) { @@ -529,17 +656,27 @@ class UnionSchema[]> } errors.push( - `${schema.constructor.name} - ${checkResult.error.msg}`, + `${schema.constructor.name} - ${ + checkResult.error.trace.join("\n") + }`, ); } + const type = typeof input; return err( pe( input, [ - "No matching schema found for a union:", + `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", ), ); } @@ -565,7 +702,7 @@ class ArraySchema> return err( pe( input, - `Element at index ${i} does not conform to schema:\n${r.error.msg}`, + `Array. Failed to parse element at index ${i}:\n${r.error.trace}`, ), ); } @@ -612,10 +749,11 @@ class ResultSchema extends PrimitiveSchema> { } } -class OptionSchema extends PrimitiveSchema> { +class OptionSchema> + extends PrimitiveSchema>> { private schema; - constructor(private readonly valueSchema: Schema) { + constructor(private readonly valueSchema: S) { super(); this.schema = new UnionSchema( @@ -706,6 +844,12 @@ class Validator { const v = new Validator(); -const r = v.array(v.union(v.string(), v.number().gt(5))); +const r = v.string().max(4, "too long").or(v.number()); -console.log(r.parse(["5", true])); +const res = r.parse(some("11234")); + +console.log(res); + +type InferSchema = S extends Schema ? T : never; + +type NestedArray = T | NestedArray[]; diff --git a/server/src/lib/validator.ts b/server/src/lib/validator.ts index 6687b49..73abd61 100644 --- a/server/src/lib/validator.ts +++ b/server/src/lib/validator.ts @@ -247,7 +247,7 @@ class UnionSchema { return err( pe( input, - `Failed to parse as one of the types: ${ + `Union. Failed to parse a any of the schemas: ${ this.schemas.map((s) => s.constructor.name).join(", ") }`, ), diff --git a/shared/utils/option.ts b/shared/utils/option.ts index ce550f5..e702d54 100644 --- a/shared/utils/option.ts +++ b/shared/utils/option.ts @@ -146,7 +146,7 @@ interface IOption { * @template `T` The type of the value inside the `Some`. */ export class Some implements IOption { - public readonly tag = "Some"; + public readonly tag = "some"; /** * Creates a new `Some` instance. @@ -240,7 +240,7 @@ export class Some implements IOption { * @template `T` The type that would be inside the Option, but it is not used because this is a None. */ export class None implements IOption { - public readonly tag = "None"; + public readonly tag = "none"; /** * Creates a new `None` instance. diff --git a/shared/utils/result.ts b/shared/utils/result.ts index 4140f79..b3b133e 100644 --- a/shared/utils/result.ts +++ b/shared/utils/result.ts @@ -39,7 +39,7 @@ interface IResult { } export class Ok implements IResult { - public readonly tag = "Ok"; + public readonly tag = "ok"; constructor(public readonly value: T) { this.value = value; @@ -182,7 +182,7 @@ export class Ok implements IResult { } export class Err implements IResult { - public readonly tag = "Err"; + public readonly tag = "err"; constructor(public readonly error: E) { this.error = error;