fortify-schema
Version:
TypeScript interface-like schema validation system that's easier to use than Zod
455 lines (391 loc) • 11.4 kB
text/typescript
/**
* Validation Helpers Module
*
* Contains optimized helper functions for validation operations
* extracted from InterfaceSchema to improve maintainability.
*/
import { SchemaValidationResult } from "../../../../types/types";
import { SchemaOptions } from "../Interface";
import { TypeValidators } from "./TypeValidators";
// Cache for parsed constant values to avoid repeated parsing
const constantCache = new Map<string, any>();
// Pre-compiled regex patterns for better performance
const NUMERIC_PATTERN = /^\d+(\.\d+)?$/;
const BOOLEAN_PATTERN = /^(true|false)$/;
/**
* Helper functions for validation operations
*/
export class ValidationHelpers {
/**
* Validate constant types (e.g., "=admin", "=user")
* Optimized with caching for repeated constant validations
*/
static validateConstantType(
constantValue: string,
value: any
): SchemaValidationResult {
// Check cache first
let expectedValue = constantCache.get(constantValue);
if (expectedValue === undefined) {
// Parse and cache the constant value
expectedValue = this.parseConstantValue(constantValue);
constantCache.set(constantValue, expectedValue);
}
if (value !== expectedValue) {
return this.createErrorResult(
`Expected constant value: ${expectedValue}, got ${value}`,
value
);
}
return this.createSuccessResult(value);
}
/**
* Parse constant value with type coercion
*/
private static parseConstantValue(constantValue: string): any {
// Numeric check
if (NUMERIC_PATTERN.test(constantValue)) {
return parseFloat(constantValue);
}
// Boolean check
if (BOOLEAN_PATTERN.test(constantValue)) {
return constantValue === "true";
}
return constantValue;
}
/**
* Validate union types (e.g., "pending|accepted|rejected")
* Optimized with Set for O(1) lookup instead of array includes
*/
static validateUnionType(
unionType: string,
value: any
): SchemaValidationResult {
const allowedValues = new Set(unionType.split("|").map((v) => v.trim()));
const stringValue = String(value);
if (!allowedValues.has(stringValue)) {
return this.createErrorResult(
`Expected one of: ${Array.from(allowedValues).join(", ")}, got ${value}`,
value
);
}
return this.createSuccessResult(value);
}
/**
* Validate record types (e.g., "record<string,any>")
* Optimized with early returns and better error handling
*/
static validateRecordType(
type: string,
value: any,
validateFieldType: (fieldType: string, value: any) => SchemaValidationResult
): SchemaValidationResult {
const recordMatch = type.match(/^record<([^,]+),(.+)>$/);
if (!recordMatch) {
return this.createErrorResult(
`Invalid Record type format: ${type}`,
value
);
}
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return this.createErrorResult("Expected object for Record type", value);
}
const [, keyType, valueType] = recordMatch;
const trimmedValueType = valueType.trim();
const errors: string[] = [];
// Batch validation for better performance
for (const [key, val] of Object.entries(value)) {
// Validate key type (currently only string keys supported)
if (keyType === "string" && typeof key !== "string") {
errors.push(`Record key must be string, got ${typeof key}`);
continue;
}
// Validate value type
const valueResult = validateFieldType(trimmedValueType, val);
if (!valueResult.success) {
errors.push(
`Record value for key "${key}": ${valueResult.errors.join(", ")}`
);
}
}
if (errors.length > 0) {
return {
success: false,
errors,
warnings: [],
data: value,
};
}
return this.createSuccessResult(value);
}
/**
* Validate array with constraints
* Optimized with pre-checks and batch validation
*/
static validateArrayWithConstraints(
value: any,
elementType: string,
constraints: any,
validateElementType: (
elementType: string,
value: any
) => SchemaValidationResult
): SchemaValidationResult {
if (!Array.isArray(value)) {
return this.createErrorResult("Expected array", value);
}
// Pre-check constraints to fail fast
const constraintErrors = this.validateArrayConstraints(value, constraints);
if (constraintErrors.length > 0) {
return {
success: false,
errors: constraintErrors,
warnings: [],
data: value,
};
}
// Validate elements
const validatedArray: any[] = [];
const errors: string[] = [];
for (let i = 0; i < value.length; i++) {
const elementResult = validateElementType(elementType, value[i]);
if (!elementResult.success) {
errors.push(`Element ${i}: ${elementResult.errors.join(", ")}`);
} else {
validatedArray.push(elementResult.data);
}
}
if (errors.length > 0) {
return {
success: false,
errors,
warnings: [],
data: value,
};
}
// Check uniqueness constraint efficiently
if (constraints.unique) {
const uniqueCheck = this.checkArrayUniqueness(validatedArray);
if (!uniqueCheck.success) {
return uniqueCheck;
}
}
return this.createSuccessResult(validatedArray);
}
/**
* Validate array constraints separately for better modularity
*/
private static validateArrayConstraints(
value: any[],
constraints: any
): string[] {
const errors: string[] = [];
if (
constraints.minItems !== undefined &&
value.length < constraints.minItems
) {
errors.push(`Array must have at least ${constraints.minItems} items`);
}
if (
constraints.maxItems !== undefined &&
value.length > constraints.maxItems
) {
errors.push(`Array must have at most ${constraints.maxItems} items`);
}
return errors;
}
/**
* Check array uniqueness efficiently
*/
private static checkArrayUniqueness(array: any[]): SchemaValidationResult {
const seen = new Set();
for (const item of array) {
const key = typeof item === "object" ? JSON.stringify(item) : item;
if (seen.has(key)) {
return this.createErrorResult("Array values must be unique", array);
}
seen.add(key);
}
return this.createSuccessResult(array);
}
/**
* Handle undefined and null values based on optional flag
* Optimized with early returns
*/
static handleOptionalValue(
value: any,
isOptional: boolean,
defaultValue?: any
): SchemaValidationResult | null {
if (value === undefined) {
return isOptional
? this.createSuccessResult(defaultValue)
: this.createErrorResult("Required field is missing");
}
if (value === null) {
return isOptional
? this.createSuccessResult(null)
: this.createErrorResult("Field cannot be null");
}
return null; // Continue with normal validation
}
/**
* Route validation to appropriate type validator
* Optimized with Map-based lookup for better performance
*/
static routeTypeValidation(
type: string,
value: any,
options: SchemaOptions,
constraints: any
): SchemaValidationResult {
// Use switch with grouped cases for better optimization
switch (type) {
case "string":
return TypeValidators.validateString(value, options, constraints);
case "number":
case "float":
return TypeValidators.validateNumber(
value,
options,
constraints,
type as "number" | "float"
);
case "int":
case "integer":
case "positive":
case "negative":
return TypeValidators.validateInteger(
value,
options,
constraints,
type as "int" | "integer" | "positive" | "negative"
);
case "double":
return TypeValidators.validateFloat(
value,
options,
constraints,
"double"
);
case "boolean":
case "bool":
return TypeValidators.validateBoolean(value, options, constraints);
case "date":
case "datetime":
case "timestamp":
return TypeValidators.validateDate(
value,
options,
constraints,
type as "date" | "datetime" | "timestamp"
);
case "email":
return TypeValidators.validateEmail(value);
case "url":
case "uri":
return TypeValidators.validateUrl(value, type as "url" | "uri");
case "uuid":
case "guid":
return TypeValidators.validateUuid(value, type as "uuid" | "guid");
case "phone":
return TypeValidators.validatePhone(value);
case "slug":
return TypeValidators.validateSlug(value);
case "username":
return TypeValidators.validateUsername(value);
case "password":
return TypeValidators.validatePassword(value);
case "text":
return TypeValidators.validateText(value);
case "json":
return TypeValidators.validateJson(value);
case "object":
return TypeValidators.validateObject(value);
case "unknown":
case "void":
case "null":
case "undefined":
case "any":
return TypeValidators.validateSpecialType(
value,
type as "unknown" | "void" | "null" | "undefined" | "any"
);
default:
return this.createErrorResult(`Unknown type: ${type}`, value);
}
}
/**
* Merge validation results efficiently
* Optimized to avoid unnecessary array operations
*/
static mergeResults(
results: SchemaValidationResult[]
): SchemaValidationResult {
if (results.length === 0) {
return this.createSuccessResult(undefined);
}
if (results.length === 1) {
return results[0];
}
let success = true;
const errors: string[] = [];
const warnings: string[] = [];
for (const result of results) {
if (!result.success) {
success = false;
}
if (result.errors.length > 0) {
errors.push(...result.errors);
}
if (result.warnings.length > 0) {
warnings.push(...result.warnings);
}
}
return {
success,
errors,
warnings,
data: undefined,
};
}
/**
* Create error result efficiently
*/
static createErrorResult(error: string, value?: any): SchemaValidationResult {
return {
success: false,
errors: [error],
warnings: [],
data: value,
};
}
/**
* Create success result efficiently
*/
static createSuccessResult(
data: any,
warnings: string[] = []
): SchemaValidationResult {
return {
success: true,
errors: [],
warnings,
data,
};
}
/**
* Clear caches for memory management in long-running applications
*/
static clearCaches(): void {
constantCache.clear();
}
/**
* Get cache statistics for monitoring
*/
static getCacheStats(): { constantCacheSize: number } {
return {
constantCacheSize: constantCache.size,
};
}
}