Keyborg/test1.ts
2025-02-12 16:31:36 +03:00

351 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)
);
}
}
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>[];