UNPKG

fortify-schema

Version:

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

267 lines (264 loc) 9.16 kB
'use strict'; /** * Validation Engine - Core validation logic for schema extensions * * This module provides the core validation engine that powers all schema extensions. * It handles type checking, constraint validation, and error reporting. */ /** * Core validation engine for schema validation */ class ValidationEngine { /** * Validate a value against a schema field definition */ static validateField(fieldSchema, value) { if (typeof fieldSchema === "string") { const result = this.validateStringSchema(fieldSchema, value); return result; } if (typeof fieldSchema === "object" && !Array.isArray(fieldSchema)) { return this.validateObjectSchema(fieldSchema, value); } // Default: accept any value return { isValid: true, errors: [] }; } /** * Validate against string-based schema definitions */ static validateStringSchema(fieldSchema, value) { const errors = []; const isOptional = fieldSchema.includes("?"); const cleanType = fieldSchema.replace("?", ""); // Handle null/undefined for optional fields if (isOptional && (value === null || value === undefined)) { return { isValid: true, errors: [] }; } // Required field cannot be null/undefined if (!isOptional && (value === null || value === undefined)) { errors.push(`Field is required but received ${value}`); return { isValid: false, errors }; } // Type-specific validation switch (cleanType) { case "email": return this.validateEmail(value); case "url": return this.validateUrl(value); case "uuid": return this.validateUuid(value); case "number": return this.validateNumber(value); case "positive": return this.validatePositiveNumber(value); case "int": return this.validateInteger(value); case "boolean": return this.validateBoolean(value); case "date": return this.validateDate(value); case "string": return this.validateString(value); default: // Handle string constraints and arrays if (cleanType.startsWith("string")) { return this.validateStringWithConstraints(cleanType, value); } if (cleanType.includes("[]")) { return this.validateArray(cleanType, value); } return { isValid: true, errors: [] }; } } /** * Validate against object schema definitions */ static validateObjectSchema(fieldSchema, value) { const errors = []; if (typeof value !== "object" || value === null) { errors.push("Expected object but received " + typeof value); return { isValid: false, errors }; } // Validate all nested fields for (const [nestedField, nestedSchema] of Object.entries(fieldSchema)) { const nestedResult = this.validateField(nestedSchema, value[nestedField]); if (!nestedResult.isValid) { errors.push(...nestedResult.errors.map(err => `${nestedField}: ${err}`)); } } return { isValid: errors.length === 0, errors }; } /** * Email validation */ static validateEmail(value) { if (typeof value !== "string") { return { isValid: false, errors: ["Email must be a string"] }; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return { isValid: false, errors: ["Invalid email format"] }; } return { isValid: true, errors: [] }; } /** * URL validation */ static validateUrl(value) { if (typeof value !== "string") { return { isValid: false, errors: ["URL must be a string"] }; } try { new URL(value); return { isValid: true, errors: [] }; } catch { return { isValid: false, errors: ["Invalid URL format"] }; } } /** * UUID validation */ static validateUuid(value) { if (typeof value !== "string") { return { isValid: false, errors: ["UUID must be a string"] }; } 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)) { return { isValid: false, errors: ["Invalid UUID format"] }; } return { isValid: true, errors: [] }; } /** * Number validation */ static validateNumber(value) { if (typeof value !== "number" || isNaN(value)) { return { isValid: false, errors: ["Must be a valid number"] }; } return { isValid: true, errors: [] }; } /** * Positive number validation */ static validatePositiveNumber(value) { const numberResult = this.validateNumber(value); if (!numberResult.isValid) return numberResult; if (value <= 0) { return { isValid: false, errors: ["Must be a positive number"] }; } return { isValid: true, errors: [] }; } /** * Integer validation */ static validateInteger(value) { const numberResult = this.validateNumber(value); if (!numberResult.isValid) return numberResult; if (!Number.isInteger(value)) { return { isValid: false, errors: ["Must be an integer"] }; } return { isValid: true, errors: [] }; } /** * Boolean validation */ static validateBoolean(value) { if (typeof value !== "boolean") { return { isValid: false, errors: ["Must be a boolean"] }; } return { isValid: true, errors: [] }; } /** * Date validation */ static validateDate(value) { if (value instanceof Date) { return { isValid: true, errors: [] }; } if (typeof value === "string" && !isNaN(Date.parse(value))) { return { isValid: true, errors: [] }; } return { isValid: false, errors: ["Must be a valid date"] }; } /** * String validation */ static validateString(value) { if (typeof value !== "string") { return { isValid: false, errors: ["Must be a string"] }; } return { isValid: true, errors: [] }; } /** * String validation with constraints */ static validateStringWithConstraints(cleanType, value) { const stringResult = this.validateString(value); if (!stringResult.isValid) return stringResult; const errors = []; const match = cleanType.match(/string\((\d+)?,?(\d+)?\)/); if (match) { const minLength = match[1] ? parseInt(match[1]) : 0; const maxLength = match[2] ? parseInt(match[2]) : Infinity; if (value.length < minLength) { errors.push(`String must be at least ${minLength} characters long`); } if (value.length > maxLength) { errors.push(`String must be at most ${maxLength} characters long`); } } return { isValid: errors.length === 0, errors }; } /** * Array validation */ static validateArray(cleanType, value) { if (!Array.isArray(value)) { return { isValid: false, errors: ["Must be an array"] }; } const elementType = cleanType.replace("[]", ""); const errors = []; value.forEach((item, index) => { const itemResult = this.validateField(elementType, item); if (!itemResult.isValid) { errors.push(...itemResult.errors.map(err => `[${index}]: ${err}`)); } }); return { isValid: errors.length === 0, errors }; } /** * Validate entire object against schema */ static validateObject(schema, data) { const errors = {}; let isValid = true; if (!data || typeof data !== "object") { return { isValid: false, data, errors: { _root: ["Expected object but received " + typeof data] }, timestamp: new Date() }; } // Validate each field in the schema for (const [fieldName, fieldSchema] of Object.entries(schema)) { const fieldResult = this.validateField(fieldSchema, data[fieldName]); if (!fieldResult.isValid) { errors[fieldName] = fieldResult.errors; isValid = false; } } return { isValid, data, errors, timestamp: new Date() }; } } exports.ValidationEngine = ValidationEngine; //# sourceMappingURL=validation-engine.js.map