validator is close to be finished
This commit is contained in:
@ -595,16 +595,6 @@ class LiteralSchema<L extends string> extends PrimitiveSchema<L> {
|
|||||||
type InferSchemaUnion<S extends Schema<any>[]> = S[number] extends
|
type InferSchemaUnion<S extends Schema<any>[]> = S[number] extends
|
||||||
Schema<infer U> ? U : never;
|
Schema<infer U> ? U : never;
|
||||||
|
|
||||||
type TypeOfString =
|
|
||||||
| "string"
|
|
||||||
| "number"
|
|
||||||
| "bigint"
|
|
||||||
| "boolean"
|
|
||||||
| "symbol"
|
|
||||||
| "undefined"
|
|
||||||
| "object"
|
|
||||||
| "function";
|
|
||||||
|
|
||||||
class UnionSchema<S extends Schema<any>[]>
|
class UnionSchema<S extends Schema<any>[]>
|
||||||
extends PrimitiveSchema<InferSchemaUnion<S>> {
|
extends PrimitiveSchema<InferSchemaUnion<S>> {
|
||||||
private static readonly schemasTypes: Partial<
|
private static readonly schemasTypes: Partial<
|
||||||
|
|||||||
19
server/src/lib/test1.ts
Normal file
19
server/src/lib/test1.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class ParseError extends Error {
|
||||||
|
type = "ParseError";
|
||||||
|
|
||||||
|
public trace: NestedArray<string> = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public input: any,
|
||||||
|
trace: NestedArray<string> | string,
|
||||||
|
public readonly msg: string,
|
||||||
|
) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NestedArray<T> = T | NestedArray<T>[];
|
||||||
|
|
||||||
|
export interface Schema<T> {
|
||||||
|
parse(input: unknown): Result<T, ParseError>;
|
||||||
|
}
|
||||||
@ -1,344 +1,571 @@
|
|||||||
import { err, ok, Result } from "@shared/utils/result.ts";
|
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 {
|
class ParseError extends Error {
|
||||||
code = "ParseError";
|
public readonly type = "ParseError";
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly input: any,
|
public readonly input: unknown,
|
||||||
public readonly msg: string,
|
public readonly detail: ParseErrorDetail,
|
||||||
|
msg?: string,
|
||||||
|
) {
|
||||||
|
super(msg ?? `Validation failed: ${JSON.stringify(detail)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public format(): Record<string, any> {
|
||||||
|
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<T> {
|
||||||
|
parse(input: unknown): Result<T, ParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationCheck<T> = (value: T) => ParseError | void;
|
||||||
|
|
||||||
|
export abstract class BaseSchema<T> implements Schema<T> {
|
||||||
|
protected checks: ValidationCheck<T>[] = [];
|
||||||
|
|
||||||
|
constructor(public readonly msg?: string) {}
|
||||||
|
|
||||||
|
public parse(input: unknown): Result<T, ParseError> {
|
||||||
|
return this.validateType(input).andThen((value) =>
|
||||||
|
this.applyChecks(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract validateType(input: unknown): Result<T, ParseError>;
|
||||||
|
|
||||||
|
protected static validatePrimitive<U>(
|
||||||
|
input: unknown,
|
||||||
|
expectedType: TypeofEnum,
|
||||||
|
msg?: string,
|
||||||
|
): Result<U, ParseError> {
|
||||||
|
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<T>): this {
|
||||||
|
this.checks.push(check);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected applyChecks(value: T): Result<T, ParseError> {
|
||||||
|
for (const check of this.checks) {
|
||||||
|
const error = check(value);
|
||||||
|
if (error) {
|
||||||
|
return err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static flattenUnion<S extends Schema<any>[]>(
|
||||||
|
...schemas: S
|
||||||
|
): Schema<any>[] {
|
||||||
|
return schemas.map((s) =>
|
||||||
|
s instanceof UnionSchema ? this.flattenUnion(s) : s
|
||||||
|
).flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
public or<S extends Schema<any>[]>(
|
||||||
|
...schemas: S
|
||||||
|
): UnionSchema<[this, ...S]> {
|
||||||
|
return new UnionSchema([this, ...schemas]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringSchema extends BaseSchema<string> {
|
||||||
|
readonly type = "string";
|
||||||
|
|
||||||
|
protected override validateType(
|
||||||
|
input: unknown,
|
||||||
|
): Result<string, ParseError> {
|
||||||
|
return BaseSchema.validatePrimitive<string>(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<L extends string> extends BaseSchema<L> {
|
||||||
|
constructor(
|
||||||
|
public readonly literal: L,
|
||||||
|
msg?: string,
|
||||||
) {
|
) {
|
||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function pe(input: any, msg: string): ParseError {
|
protected override validateType(input: unknown): Result<L, ParseError> {
|
||||||
return new ParseError(input, msg);
|
return BaseSchema.validatePrimitive<string>(input, "string", this.msg)
|
||||||
}
|
.andThen((str) =>
|
||||||
|
str === this.literal ? ok(str as L) : err(
|
||||||
type CheckFunction<T> = (
|
createParseError(
|
||||||
input: T,
|
input,
|
||||||
msg?: string,
|
{
|
||||||
) => ParseError | void;
|
type: "typeError",
|
||||||
|
expected: this.literal,
|
||||||
export abstract class BaseSchema<T> {
|
received: str,
|
||||||
private checks: CheckFunction<T>[] = [];
|
msg: `Expected '${this.literal}' but received '${str}'`,
|
||||||
abstract initialCheck(input: unknown): Result<T, ParseError>;
|
},
|
||||||
public addCheck(check: CheckFunction<T>) {
|
),
|
||||||
this.checks.push(check);
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
parse(input: any): Result<T, ParseError> {
|
|
||||||
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 BaseSchema<string> {
|
class NumberSchema extends BaseSchema<number> {
|
||||||
private static readonly emailRegex =
|
protected override validateType(
|
||||||
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; // https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript
|
input: unknown,
|
||||||
|
): Result<number, ParseError> {
|
||||||
private static readonly ipRegex =
|
return BaseSchema.validatePrimitive<number>(input, "number", this.msg);
|
||||||
/^(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<string, ParseError> {
|
|
||||||
if (typeof input !== "string") {
|
|
||||||
return err(pe(input, `Expected string, received ${typeof input}`));
|
|
||||||
}
|
|
||||||
return ok(input);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
max(
|
class BigintSchema extends BaseSchema<bigint> {
|
||||||
length: number,
|
protected override validateType(
|
||||||
msg: string = `String length must be at most ${length} characters long`,
|
input: unknown,
|
||||||
): this {
|
): Result<bigint, ParseError> {
|
||||||
this.addCheck((input) => {
|
return BaseSchema.validatePrimitive<bigint>(input, "bigint", this.msg);
|
||||||
if (input.length > length) {
|
}
|
||||||
return pe(
|
}
|
||||||
|
|
||||||
|
class BooleanSchema extends BaseSchema<boolean> {
|
||||||
|
protected override validateType(
|
||||||
|
input: unknown,
|
||||||
|
): Result<boolean, ParseError> {
|
||||||
|
return BaseSchema.validatePrimitive<boolean>(
|
||||||
|
input,
|
||||||
|
"boolean",
|
||||||
|
this.msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateSchema extends BaseSchema<Date> {
|
||||||
|
protected override validateType(input: unknown): Result<Date, ParseError> {
|
||||||
|
return BaseSchema.validatePrimitive<object>(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,
|
input,
|
||||||
msg,
|
{
|
||||||
);
|
type: "typeError",
|
||||||
}
|
expected: "Date instance",
|
||||||
});
|
received,
|
||||||
|
msg: `Expected a Date instance but received ${received}`,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumberSchema extends BaseSchema<number> {
|
class SymbolSchema extends BaseSchema<symbol> {
|
||||||
private initialCheck(input: unknown): Result<number, ParseError> {
|
protected override validateType(
|
||||||
if (typeof input !== "number") {
|
input: unknown,
|
||||||
return err(pe(input, `Expected number, recieved ${typeof input}`));
|
): Result<symbol, ParseError> {
|
||||||
}
|
return BaseSchema.validatePrimitive<symbol>(input, "symbol", this.msg);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BooleanSchema extends BaseSchema<boolean> {
|
class UndefinedSchema extends BaseSchema<undefined> {
|
||||||
private initialCheck(input: unknown): Result<boolean, ParseError> {
|
protected override validateType(
|
||||||
if (typeof input !== "boolean") {
|
input: unknown,
|
||||||
return err(pe(input, `Expected boolean, received ${typeof input}`));
|
): Result<undefined, ParseError> {
|
||||||
}
|
return BaseSchema.validatePrimitive<undefined>(
|
||||||
return ok(input);
|
input,
|
||||||
|
"undefined",
|
||||||
|
this.msg,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DateSchema extends BaseSchema<Date> {
|
class NullSchema extends BaseSchema<null> {
|
||||||
override initialCheck(input: unknown): Result<Date, ParseError> {
|
protected override validateType(input: unknown): Result<null, ParseError> {
|
||||||
if (typeof input === "object" && input !== null) {
|
if (input === null) return ok(input);
|
||||||
if (input instanceof Date) {
|
|
||||||
return ok(input);
|
|
||||||
}
|
|
||||||
// TODO: add check for an non Date instance. Right now lsp freaks out for some reason
|
|
||||||
}
|
|
||||||
|
|
||||||
return err(pe(input, `Expected object, received ${typeof input}`));
|
const received = typeof input === "object"
|
||||||
}
|
? input?.constructor?.name ?? "unknown"
|
||||||
|
: 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<U extends Schema[]> {
|
|
||||||
private readonly schemas: Schema[];
|
|
||||||
|
|
||||||
constructor(...schemas: Schema[]) {
|
|
||||||
this.schemas = schemas;
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(input: unknown): Result<InferTypeFromSchema<U>, ParseError> {
|
|
||||||
for (const schema of this.schemas) {
|
|
||||||
const result = schema.parse(input);
|
|
||||||
|
|
||||||
if (result.isOk()) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err(
|
return err(
|
||||||
pe(
|
createParseError(
|
||||||
input,
|
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 =
|
class VoidSchema extends BaseSchema<void> {
|
||||||
| NumberSchema
|
protected override validateType(input: unknown): Result<void, ParseError> {
|
||||||
| StringSchema
|
if (input === undefined || input === null) return ok();
|
||||||
| BooleanSchema
|
|
||||||
| DateSchema
|
|
||||||
| ObjectSchema<Record<string, Schema>>
|
|
||||||
| UnionSchema<Schema[]>;
|
|
||||||
|
|
||||||
export class ObjectSchema<
|
const received = typeof input === "object"
|
||||||
S extends Record<string, Schema>,
|
? input?.constructor?.name ?? "unknown"
|
||||||
> {
|
: typeof input;
|
||||||
// TODO: rewrite this to be a static method returning result
|
|
||||||
|
return err(createParseError(
|
||||||
|
input,
|
||||||
|
{
|
||||||
|
type: "typeError",
|
||||||
|
expected: "void (undefined/null)",
|
||||||
|
received,
|
||||||
|
msg: this.msg,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnySchema extends BaseSchema<any> {
|
||||||
|
protected override validateType(input: unknown): Result<any, ParseError> {
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnknownSchema extends BaseSchema<unknown> {
|
||||||
|
protected override validateType(
|
||||||
|
input: unknown,
|
||||||
|
): Result<unknown, ParseError> {
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NeverSchema extends BaseSchema<never> {
|
||||||
|
protected override validateType(input: unknown): Result<never, ParseError> {
|
||||||
|
return err(
|
||||||
|
createParseError(
|
||||||
|
input,
|
||||||
|
{
|
||||||
|
type: "typeError",
|
||||||
|
expected: "never",
|
||||||
|
received: typeof input,
|
||||||
|
msg: "No values are allowed for this schema (never)",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InferSchemaType<S> = S extends Schema<infer T> ? T : never;
|
||||||
|
|
||||||
|
class ObjectSchema<S extends Record<string, Schema<any>>>
|
||||||
|
extends BaseSchema<{ [K in keyof S]: InferSchemaType<S[K]> }> {
|
||||||
|
private strictMode: boolean = false;
|
||||||
|
|
||||||
|
constructor(private readonly shape: S, msg?: string) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override validateType(
|
||||||
|
input: unknown,
|
||||||
|
): Result<{ [K in keyof S]: InferSchemaType<S[K]> }, ParseError> {
|
||||||
|
return BaseSchema.validatePrimitive<object>(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<string, unknown> = {};
|
||||||
|
|
||||||
|
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<S[K]> },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
strict(): this {
|
||||||
|
this.strictMode = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InferUnionSchemaType<S extends Schema<any>[]> = S[number] extends
|
||||||
|
Schema<infer T> ? T : never;
|
||||||
|
|
||||||
|
class UnionSchema<U extends Schema<any>[]>
|
||||||
|
extends BaseSchema<keyof InferUnionSchemaType<U>> {
|
||||||
|
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<InferUnionSchemaType<U>, 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<S extends Schema<any>>
|
||||||
|
extends BaseSchema<InferSchemaType<S>[]> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly schema: S,
|
private readonly schema: S,
|
||||||
|
msg?: string,
|
||||||
) {
|
) {
|
||||||
for (const [_, value] of Object.entries(schema)) {
|
super(msg);
|
||||||
if (validatorsObjects.indexOf(schema.constructor.name) === -1) {
|
|
||||||
throw "Not a valid ObjectSchema >:(";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Yay! It is valid");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(
|
protected override validateType(
|
||||||
input: unknown,
|
input: unknown,
|
||||||
): Result<{ [K in keyof S]: InferTypeFromSchema<S[K]> }, ParseError> {
|
): Result<InferSchemaType<S>[], ParseError> {
|
||||||
if (typeof input !== "object") {
|
if (!Array.isArray(input)) {
|
||||||
return err(pe(input, `Expected object, received ${typeof input}`));
|
return err(createParseError(input, {
|
||||||
|
type: "validationError",
|
||||||
|
msg: `Expected an array`,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input === null) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
return err(pe(input, `Expected object, received null`));
|
const r = this.schema.parse(input[i]);
|
||||||
}
|
|
||||||
|
|
||||||
const obj = input as Record<string, unknown>;
|
if (r.isErr()) {
|
||||||
const resultObj: Partial<InferTypeFromSchema<S>> = {};
|
return err(
|
||||||
|
createParseError(input, {
|
||||||
for (const [key, schema] of Object.entries(this.schema)) {
|
type: "arrayError",
|
||||||
const inputValue = obj[key];
|
index: i,
|
||||||
|
detail: r.error.detail,
|
||||||
if (!inputValue) {
|
msg: `Failed to parse an element at index ${i}`,
|
||||||
return err(pe(
|
}),
|
||||||
input,
|
);
|
||||||
`Object ${input} missing the '${key}' attribute`,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<S>[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type InferTypeFromSchema<S extends Schema | Record<string, Schema> | Schema[]> =
|
const z = {
|
||||||
S extends Record<string, Schema>
|
string: (msg?: string) => new StringSchema(msg),
|
||||||
? { [K in keyof S]: InferTypeFromSchema<S[K]> }
|
literal: <L extends string>(lit: L, msg?: string) =>
|
||||||
: S extends StringSchema ? string
|
new LiteralSchema<L>(lit, msg),
|
||||||
: S extends NumberSchema ? number
|
number: (msg?: string) => new NumberSchema(msg),
|
||||||
: S extends DateSchema ? Date
|
bigint: (msg?: string) => new BigintSchema(msg),
|
||||||
: S extends BooleanSchema ? boolean
|
boolean: (msg?: string) => new BooleanSchema(msg),
|
||||||
: S extends ObjectSchema<infer O> ? InferTypeFromSchema<O>
|
date: (msg?: string) => new DateSchema(msg),
|
||||||
: S extends UnionSchema<infer U> ? InferTypeFromSchema<U[number]>
|
symbol: (msg?: string) => new StringSchema(msg),
|
||||||
: never;
|
undefined: (msg?: string) => new UndefinedSchema(msg),
|
||||||
|
null: (msg?: string) => new NullSchema(msg),
|
||||||
export const s = {
|
void: (msg?: string) => new VoidSchema(msg),
|
||||||
string: () => new StringSchema(),
|
any: (msg?: string) => new AnySchema(msg),
|
||||||
number: () => new NumberSchema(),
|
unknown: (msg?: string) => new UnknownSchema(msg),
|
||||||
date: () => new DateSchema(),
|
never: (msg?: string) => new NeverSchema(msg),
|
||||||
|
obj: <S extends Record<string, Schema<any>>>(schema: S, msg?: string) =>
|
||||||
|
new ObjectSchema<S>(schema, msg),
|
||||||
|
union: <U extends Schema<any>[]>(schemas: U, msg?: string) =>
|
||||||
|
new UnionSchema<U>(schemas, msg),
|
||||||
};
|
};
|
||||||
|
|
||||||
function createObjectSchema<S extends Record<string, Schema>>(
|
const schema = z.obj({
|
||||||
schema: S,
|
test: z.union([
|
||||||
): ObjectSchema<S> {
|
z.string().max(2),
|
||||||
return new ObjectSchema(schema);
|
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({}));
|
|
||||||
|
|||||||
21
test.md
Normal file
21
test.md
Normal file
@ -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
|
||||||
|
]
|
||||||
|
]
|
||||||
351
test1.ts
Normal file
351
test1.ts
Normal file
@ -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<T> {
|
||||||
|
parse(input: unknown, path?: NestedPath): Result<T, ParseError>;
|
||||||
|
checkIfValid(input: unknown): boolean;
|
||||||
|
nullable(): NullableSchema<Schema<T>>;
|
||||||
|
option(): OptionSchema<Schema<T>>;
|
||||||
|
or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckFunction<T> = (input: T, path: NestedPath) => ParseError | void;
|
||||||
|
|
||||||
|
export abstract class BaseSchema<T> implements Schema<T> {
|
||||||
|
protected checks: CheckFunction<T>[] = [];
|
||||||
|
|
||||||
|
public addCheck(check: CheckFunction<T>): this {
|
||||||
|
this.checks.push(check);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected runChecks(input: T, path: NestedPath): Result<T, ParseError> {
|
||||||
|
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<Schema<T>> {
|
||||||
|
return new NullableSchema(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]> {
|
||||||
|
return new UnionSchema(this, ...schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
option(): OptionSchema<Schema<T>> {
|
||||||
|
return new OptionSchema(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract parse(input: unknown, path?: NestedPath): Result<T, ParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class PrimitiveSchema<T> extends BaseSchema<T> {
|
||||||
|
protected abstract initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
path: NestedPath,
|
||||||
|
): Result<T, ParseError>;
|
||||||
|
|
||||||
|
protected checkPrimitive<U = T>(
|
||||||
|
input: unknown,
|
||||||
|
type:
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "bigint"
|
||||||
|
| "boolean"
|
||||||
|
| "symbol"
|
||||||
|
| "undefined"
|
||||||
|
| "object"
|
||||||
|
| "function",
|
||||||
|
path: NestedPath,
|
||||||
|
): Result<U, ParseError> {
|
||||||
|
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<T, ParseError> {
|
||||||
|
return this.initialCheck(input, path).andThen((value) =>
|
||||||
|
this.runChecks(value, path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: StringSchema with Improved Error Handling
|
||||||
|
export class StringSchema extends PrimitiveSchema<string> {
|
||||||
|
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<string, ParseError> {
|
||||||
|
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<O extends Record<string, Schema<any>>>
|
||||||
|
extends BaseSchema<{ [K in keyof O]: InferSchema<O[K]> }> {
|
||||||
|
constructor(private readonly schema: O) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
path: NestedPath,
|
||||||
|
): Result<{ [K in keyof O]: InferSchema<O[K]> }, ParseError> {
|
||||||
|
if (typeof input !== "object" || input === null) {
|
||||||
|
return err(
|
||||||
|
createParseError(
|
||||||
|
input,
|
||||||
|
path,
|
||||||
|
`Expected an object, received '${typeof input}'`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = input as Record<string, any>;
|
||||||
|
const parsedObj: Partial<{ [K in keyof O]: InferSchema<O[K]> }> = {};
|
||||||
|
|
||||||
|
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<O[K]> });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refactored UnionSchema with Simplified Error Handling
|
||||||
|
class UnionSchema<S extends Schema<any>[]>
|
||||||
|
extends BaseSchema<InferSchemaUnion<S>> {
|
||||||
|
constructor(...schemas: S) {
|
||||||
|
super();
|
||||||
|
this.schemas = schemas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private schemas: S;
|
||||||
|
|
||||||
|
protected initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
path: NestedPath,
|
||||||
|
): Result<InferSchemaUnion<S>, 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<S extends Schema<any>> extends BaseSchema<InferSchema<S>[]> {
|
||||||
|
constructor(private readonly schema: S) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
path: NestedPath,
|
||||||
|
): Result<InferSchema<S>[], ParseError> {
|
||||||
|
if (!Array.isArray(input)) {
|
||||||
|
return err(
|
||||||
|
createParseError(
|
||||||
|
input,
|
||||||
|
path,
|
||||||
|
`Expected an array, received '${typeof input}'`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedArray: InferSchema<S>[] = [];
|
||||||
|
|
||||||
|
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<S extends Schema<any>[]>(...schemas: S): UnionSchema<S> {
|
||||||
|
return new UnionSchema(...schemas);
|
||||||
|
}
|
||||||
|
|
||||||
|
array<S extends Schema<any>>(elementSchema: S): ArraySchema<S> {
|
||||||
|
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> = S extends Schema<infer T> ? T : never;
|
||||||
|
type InferSchemaUnion<S extends Schema<any>[]> = S[number] extends
|
||||||
|
Schema<infer U> ? U : never;
|
||||||
|
type NestedArray<T> = T | NestedArray<T>[];
|
||||||
Reference in New Issue
Block a user