working on validator

This commit is contained in:
2025-01-29 20:21:44 +03:00
parent 3612a8a86d
commit fc1605cdb2
4 changed files with 298 additions and 154 deletions

View File

@ -3,23 +3,39 @@ import { none, Option, some } from "@shared/utils/option.ts";
class ParseError extends Error {
type = "ParseError";
public trace: NestedArray<string> = [];
constructor(
public readonly input: any,
public input: any,
trace: NestedArray<string> | 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<string>, msg: string) {
return new ParseError(input, trace, msg);
}
export interface Schema<T> {
parse(input: unknown): Result<T, ParseError>;
checkIfValid(input: unknown): boolean;
nullable(): NullableSchema<this>;
option(): OptionSchema<T>;
nullable(): NullableSchema<Schema<T>>;
option(): OptionSchema<Schema<T>>;
or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]>;
}
@ -47,7 +63,7 @@ export abstract class BaseSchema<T> implements Schema<T> {
return this.parse(input).isOk();
}
nullable(): NullableSchema<this> {
nullable(): NullableSchema<Schema<T>> {
return new NullableSchema(this);
}
@ -55,27 +71,14 @@ export abstract class BaseSchema<T> implements Schema<T> {
return new UnionSchema(this, ...schema);
}
option(): OptionSchema<T> {
option(): OptionSchema<Schema<T>> {
return new OptionSchema(this);
}
abstract parse(input: unknown): Result<T, ParseError>;
}
export abstract class PrimitiveSchema<
T extends
| string
| number
| boolean
| bigint
| undefined
| null
| object
| void
| any
| unknown
| never,
> extends BaseSchema<T> {
export abstract class PrimitiveSchema<T> extends BaseSchema<T> {
protected abstract initialCheck(input: unknown): Result<T, ParseError>;
protected checkPrimitive<U = T>(
@ -130,42 +133,53 @@ export class StringSchema extends PrimitiveSchema<string> {
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<number> {
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<bigint> {
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<object> {
});
}
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<void> {
}
class AnySchema extends PrimitiveSchema<any> {
protected override initialCheck(input: unknown): Result<any, ParseError> {
protected override initialCheck(input: any): Result<any, ParseError> {
return ok(input);
}
}
@ -426,8 +516,6 @@ class UnknownSchema extends PrimitiveSchema<unknown> {
}
}
type InferSchema<S> = S extends Schema<infer T> ? T : never;
class ObjectSchema<O extends Record<string, Schema<any>>>
extends PrimitiveSchema<{ [K in keyof O]: InferSchema<O[K]> }> {
private strict: boolean = false;
@ -453,9 +541,9 @@ class ObjectSchema<O extends Record<string, Schema<any>>>
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<L extends string> extends PrimitiveSchema<L> {
type InferSchemaUnion<S extends Schema<any>[]> = S[number] extends
Schema<infer U> ? U : never;
type TypeOfString =
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
class UnionSchema<S extends Schema<any>[]>
extends PrimitiveSchema<InferSchemaUnion<S>> {
private readonly schemas: S;
private static readonly schemasTypes: Partial<
Record<string, TypeOfString>
> = {
StringSchema: "string",
LiteralSchema: "string",
NumberSchema: "number",
BigintSchema: "bigint",
BooleanSchema: "boolean",
UndefinedSchema: "undefined",
VoidSchema: "undefined",
};
private readonly primitiveTypesMap: Map<TypeOfString, Schema<any>[]> =
new Map();
private readonly othersTypes: Schema<any>[] = [];
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<InferSchemaUnion<S>, 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<S extends Schema<any>[]>
}
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<S extends Schema<any>>
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<T, E> extends PrimitiveSchema<Result<T, E>> {
}
}
class OptionSchema<T> extends PrimitiveSchema<Option<T>> {
class OptionSchema<S extends Schema<any>>
extends PrimitiveSchema<Option<InferSchema<S>>> {
private schema;
constructor(private readonly valueSchema: Schema<T>) {
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> = S extends Schema<infer T> ? T : never;
type NestedArray<T> = T | NestedArray<T>[];

View File

@ -247,7 +247,7 @@ class UnionSchema<U extends Schema[]> {
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(", ")
}`,
),

View File

@ -146,7 +146,7 @@ interface IOption<T> {
* @template `T` The type of the value inside the `Some<T>`.
*/
export class Some<T> implements IOption<T> {
public readonly tag = "Some";
public readonly tag = "some";
/**
* Creates a new `Some<T>` instance.
@ -240,7 +240,7 @@ export class Some<T> implements IOption<T> {
* @template `T` The type that would be inside the Option, but it is not used because this is a None.
*/
export class None<T> implements IOption<T> {
public readonly tag = "None";
public readonly tag = "none";
/**
* Creates a new `None` instance.

View File

@ -39,7 +39,7 @@ interface IResult<T, E> {
}
export class Ok<T, E> implements IResult<T, E> {
public readonly tag = "Ok";
public readonly tag = "ok";
constructor(public readonly value: T) {
this.value = value;
@ -182,7 +182,7 @@ export class Ok<T, E> implements IResult<T, E> {
}
export class Err<T, E> implements IResult<T, E> {
public readonly tag = "Err";
public readonly tag = "err";
constructor(public readonly error: E) {
this.error = error;