215 lines
6.8 KiB
TypeScript
215 lines
6.8 KiB
TypeScript
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<string, string> {
|
|
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();
|
|
}
|