fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
271 lines (241 loc) • 6.74 kB
text/typescript
import { BaseSchema } from "./BaseSchema";
import { SchemaValidationResult, StringSchemaOptions } from "../../../types/types";
/**
* String schema with comprehensive validation options
*/
export class StringSchema extends BaseSchema<string> {
private _minLength?: number;
private _maxLength?: number;
private _pattern?: RegExp;
private _format?: "email" | "url" | "uuid" | "phone" | "slug" | "username";
/**
* Set minimum length
*/
min(length: number): this {
const cloned = this.clone();
cloned._minLength = length;
return cloned;
}
/**
* Set maximum length
*/
max(length: number): this {
const cloned = this.clone();
cloned._maxLength = length;
return cloned;
}
/**
* Set exact length
*/
length(length: number): this {
const cloned = this.clone();
cloned._minLength = length;
cloned._maxLength = length;
return cloned;
}
/**
* Set regex pattern
*/
regex(pattern: RegExp): this {
const cloned = this.clone();
cloned._pattern = pattern;
return cloned;
}
/**
* Validate as email
*/
email(): this {
const cloned = this.clone();
cloned._format = "email";
return cloned;
}
/**
* Validate as URL
*/
url(): this {
const cloned = this.clone();
cloned._format = "url";
return cloned;
}
/**
* Validate as UUID
*/
uuid(): this {
const cloned = this.clone();
cloned._format = "uuid";
return cloned;
}
/**
* Validate as phone number
*/
phone(): this {
const cloned = this.clone();
cloned._format = "phone";
return cloned;
}
/**
* Validate as URL slug
*/
slug(): this {
const cloned = this.clone();
cloned._format = "slug";
return cloned;
}
/**
* Validate as username
*/
username(): this {
const cloned = this.clone();
cloned._format = "username";
return cloned;
}
/**
* Validate string value
*/
validate(value: any): SchemaValidationResult<string> {
// Handle common validation (undefined/null)
const commonResult = this.handleCommonValidation(value);
if (commonResult) {
return commonResult;
}
const result: SchemaValidationResult<string> = {
success: true,
errors: [],
warnings: [],
data: undefined,
};
// Type check
if (typeof value !== "string") {
result.success = false;
result.errors.push("Expected string");
return result;
}
result.data = value;
// Length validation
if (this._minLength !== undefined && value.length < this._minLength) {
result.success = false;
result.errors.push(
`String must be at least ${this._minLength} characters`
);
}
if (this._maxLength !== undefined && value.length > this._maxLength) {
result.success = false;
result.errors.push(
`String must be at most ${this._maxLength} characters`
);
}
// Pattern validation
if (this._pattern && !this._pattern.test(value)) {
result.success = false;
result.errors.push("String does not match required pattern");
}
// Format validation
if (this._format) {
try {
this.validateFormat(value, this._format);
} catch (error) {
result.success = false;
result.errors.push((error as Error).message);
}
}
return result;
}
/**
* Validate specific string formats
*/
private validateFormat(value: string, format: string): void {
switch (format) {
case "email":
this.validateEmail(value);
break;
case "url":
this.validateURL(value);
break;
case "uuid":
this.validateUUID(value);
break;
case "phone":
this.validatePhone(value);
break;
case "slug":
this.validateSlug(value);
break;
case "username":
this.validateUsername(value);
break;
default:
throw new Error(`Unknown format: ${format}`);
}
}
/**
* Basic email validation (can be enhanced with external validators)
*/
private validateEmail(value: string): void {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error("Invalid email format");
}
}
/**
* Basic URL validation
*/
private validateURL(value: string): void {
try {
new URL(value);
} catch {
throw new Error("Invalid URL format");
}
}
/**
* Basic UUID validation
*/
private validateUUID(value: string): void {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(value)) {
throw new Error("Invalid UUID format");
}
}
/**
* Basic phone validation
*/
private validatePhone(value: string): void {
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/;
const cleanPhone = value.replace(/[\s\-\(\)\.]/g, "");
if (!phoneRegex.test(cleanPhone)) {
throw new Error("Invalid phone number format");
}
}
/**
* Slug validation
*/
private validateSlug(value: string): void {
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
if (!slugRegex.test(value)) {
throw new Error("Invalid slug format");
}
}
/**
* Username validation
*/
private validateUsername(value: string): void {
const usernameRegex = /^[a-zA-Z0-9_-]+$/;
if (!usernameRegex.test(value)) {
throw new Error("Invalid username format");
}
}
/**
* Get schema configuration
*/
getConfig(): StringSchemaOptions {
return {
minLength: this._minLength,
maxLength: this._maxLength,
pattern: this._pattern,
format: this._format,
optional: this._optional,
nullable: this._nullable,
default: this._default,
};
}
}