UNPKG

fortify-schema

Version:

TypeScript interface-like schema validation system that's easier to use than Zod

455 lines (391 loc) 11.4 kB
/** * 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, }; } }