Initial Commit
This commit is contained in:
214
packages/foundation/data/validation/src/schemas/string/string.ts
Normal file
214
packages/foundation/data/validation/src/schemas/string/string.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user