import { addIssue, BaseSchema, type INVALID, } from "../../core/schema/schema.js"; import type { ParseContext } from "../../types/common.js"; // --------------------------------------------------------------------------- // Constraints stored on the schema // --------------------------------------------------------------------------- interface StringConstraints { min?: number; max?: number; length?: number; regex?: RegExp; email?: boolean; url?: boolean; uuid?: boolean; startsWith?: string; endsWith?: string; includes?: string; trim?: boolean; toLowerCase?: boolean; toUpperCase?: boolean; } const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // --------------------------------------------------------------------------- // StringSchema // --------------------------------------------------------------------------- export class StringSchema extends BaseSchema { private readonly _constraints: StringConstraints; constructor(constraints: StringConstraints = {}) { super(); this._constraints = constraints; } _parse(input: unknown, ctx: ParseContext): string | INVALID { if (typeof input !== "string") { return addIssue(ctx, { code: "VALIDATION.INVALID_TYPE", path: [...ctx.path], message: `Expected string, received ${typeof input}`, input, }); } let value = input; const c = this._constraints; // Transforms (applied before constraint checks) if (c.trim) value = value.trim(); if (c.toLowerCase) value = value.toLowerCase(); if (c.toUpperCase) value = value.toUpperCase(); // Constraints if (c.min !== undefined && value.length < c.min) { return addIssue(ctx, { code: "VALIDATION.STRING.TOO_SHORT", path: [...ctx.path], message: `String must be at least ${c.min} character(s)`, input, }); } if (c.max !== undefined && value.length > c.max) { return addIssue(ctx, { code: "VALIDATION.STRING.TOO_LONG", path: [...ctx.path], message: `String must be at most ${c.max} character(s)`, input, }); } if (c.length !== undefined && value.length !== c.length) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: `String must be exactly ${c.length} character(s)`, input, }); } if (c.regex !== undefined && !c.regex.test(value)) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: `String does not match pattern ${c.regex.toString()}`, input, }); } if (c.email && !EMAIL_RE.test(value)) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: "Invalid email address", input, }); } if ( c.url && !value.startsWith("http://") && !value.startsWith("https://") ) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: "Invalid URL", input, }); } if (c.uuid && !UUID_RE.test(value)) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: "Invalid UUID", input, }); } if (c.startsWith !== undefined && !value.startsWith(c.startsWith)) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: `String must start with "${c.startsWith}"`, input, }); } if (c.endsWith !== undefined && !value.endsWith(c.endsWith)) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: `String must end with "${c.endsWith}"`, input, }); } if (c.includes !== undefined && !value.includes(c.includes)) { return addIssue(ctx, { code: "VALIDATION.STRING.INVALID_FORMAT", path: [...ctx.path], message: `String must include "${c.includes}"`, input, }); } return value; } // ------------------------------------------------------------------------- // Constraint builders (return new instance for immutability) // ------------------------------------------------------------------------- min(n: number): StringSchema { return new StringSchema({ ...this._constraints, min: n }); } max(n: number): StringSchema { return new StringSchema({ ...this._constraints, max: n }); } length(n: number): StringSchema { return new StringSchema({ ...this._constraints, length: n }); } regex(re: RegExp): StringSchema { return new StringSchema({ ...this._constraints, regex: re }); } email(): StringSchema { return new StringSchema({ ...this._constraints, email: true }); } url(): StringSchema { return new StringSchema({ ...this._constraints, url: true }); } uuid(): StringSchema { return new StringSchema({ ...this._constraints, uuid: true }); } startsWith(prefix: string): StringSchema { return new StringSchema({ ...this._constraints, startsWith: prefix }); } endsWith(suffix: string): StringSchema { return new StringSchema({ ...this._constraints, endsWith: suffix }); } includes(substr: string): StringSchema { return new StringSchema({ ...this._constraints, includes: substr }); } trim(): StringSchema { return new StringSchema({ ...this._constraints, trim: true }); } toLowerCase(): StringSchema { return new StringSchema({ ...this._constraints, toLowerCase: true }); } toUpperCase(): StringSchema { return new StringSchema({ ...this._constraints, toUpperCase: true }); } } // --------------------------------------------------------------------------- // Factory // --------------------------------------------------------------------------- export function string(): StringSchema { return new StringSchema(); }