352 lines
9.7 KiB
TypeScript
352 lines
9.7 KiB
TypeScript
// 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
|
|
export 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>[];
|