fortify-schema
Version:
A modern TypeScript validation library designed around familiar interface syntax and powerful conditional validation. Experience schema validation that feels natural to TypeScript developers while unlocking advanced runtime validation capabilities.
296 lines (273 loc) • 9.48 kB
text/typescript
/**
* Validation Engine - Core validation logic for schema extensions
*
* This module provides the core validation engine that powers all schema extensions.
* It acts as a bridge between extensions and the main validation system, delegating
* actual validation to the TypeValidators module to avoid duplication.
*/
import { SchemaInterface } from "../../mode/interfaces/Interface";
import { TypeValidators } from "../../mode/interfaces/validators/TypeValidators";
import { ConstraintParser } from "../../mode/interfaces/validators/ConstraintParser";
import { TypeGuards } from "../../mode/interfaces/validators/TypeGuards";
import { ValidationHelpers } from "../../mode/interfaces/validators/ValidationHelpers";
import { ErrorHandler } from "../../mode/interfaces/errors/ErrorHandler";
import { SchemaOptions } from "../../../types/SchemaValidator.type";
import { ErrorCode } from "../../mode/interfaces/errors/types/errors.type";
/**
* Core validation engine for schema validation
*/
export class ValidationEngine {
/**
* Validate a value against a schema field definition
* Delegates to TypeValidators for actual validation logic
*/
static validateField(fieldSchema: any, value: any): ValidationFieldResult {
if (typeof fieldSchema === "string") {
return this.validateStringSchema(fieldSchema, value);
}
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
* Uses TypeValidators for consistent validation logic
*/
private static validateStringSchema(
fieldSchema: string,
value: any
): ValidationFieldResult {
// Parse the field schema using ConstraintParser
const parsed = ConstraintParser.parseConstraints(fieldSchema);
const { type, constraints, optional } = parsed;
// Handle null/undefined for optional fields
if (optional && (value === null || value === undefined)) {
return { isValid: true, errors: [] };
}
// Required field cannot be null/undefined
if (!optional && (value === null || value === undefined)) {
return {
isValid: false,
errors: [`Field is required but received ${value}`],
};
}
// Use TypeValidators for actual validation
const options: SchemaOptions = { loose: false }; // Default options
let validationResult;
try {
// Delegate to appropriate TypeValidator method based on type
switch (type) {
case "string":
validationResult = TypeValidators.validateString(
value,
options,
constraints
);
break;
case "number":
case "float":
case "double":
validationResult = TypeValidators.validateNumber(
value,
options,
constraints
);
break;
case "int":
case "integer":
case "positive":
case "negative":
validationResult = TypeValidators.validateInteger(
value,
options,
constraints,
type as any
);
break;
case "boolean":
validationResult = TypeValidators.validateBoolean(value, options, {});
break;
case "date":
validationResult = TypeValidators.validateDate(
value,
options,
{},
"date"
);
break;
case "email":
validationResult = TypeValidators.validateEmail(value);
break;
case "url":
validationResult = TypeValidators.validateUrl(value, "url.web");
break;
case "uuid":
case "guid":
validationResult = TypeValidators.validateUuid(value, type as any);
break;
default:
// Handle arrays and other complex types
if (type.includes("[]")) {
// For arrays, use ValidationHelpers.validateArrayWithConstraints
const elementType = type.replace("[]", "");
validationResult = ValidationHelpers.validateArrayWithConstraints(
value,
elementType,
constraints,
(elemType: string, elemValue: any) => {
// Recursively validate each element and convert result format
const fieldResult = this.validateStringSchema(
elemType,
elemValue
);
// Convert string errors to ValidationError objects
const validationErrors = fieldResult.errors.map((errorMsg) => {
if (typeof errorMsg === "string") {
// console.log("converting string error to ValidationError");
return ErrorHandler.convertStringToError(errorMsg, ErrorCode.VALIDATION_ERROR
);
}
// If it's already a ValidationError object, return as is
if (
errorMsg &&
typeof errorMsg === "object" &&
"message" in errorMsg
) {
return errorMsg as any;
}
// Fallback: convert to string and then to ValidationError
// console.log(
// "converting unknown error format to ValidationError"
// );
return ErrorHandler.convertStringToError(
String(errorMsg),
ErrorCode.VALIDATION_ERROR
);
});
return {
success: fieldResult.isValid,
errors: validationErrors,
warnings: [],
data: elemValue,
};
}
);
} else {
// // Unknown type - accept any value
validationResult = {
success: true,
errors: [],
warnings: [],
data: value,
};
}
}
// Convert TypeValidators result format to ValidationEngine format
return {
isValid: validationResult.success,
errors: (validationResult.errors || []).map((error) => {
if (typeof error === "string") {
return error;
}
if (error && typeof error === "object" && error.message) {
return error.message;
}
// Fallback: try to extract meaningful information
if (error && typeof error === "object") {
if (
error.toString &&
error.toString !== Object.prototype.toString
) {
return error.toString();
}
return JSON.stringify(error);
}
return String(error);
}),
};
} catch (error) {
return {
isValid: false,
errors: [
`Validation error: ${error instanceof Error ? error.message : "Unknown error"}`,
],
};
}
}
/**
* Validate against object schema definitions
*/
private static validateObjectSchema(
fieldSchema: any,
value: any
): ValidationFieldResult {
const errors: string[] = [];
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) {
const errRes = nestedResult.errors.map((err) => {
// Handle both string and ValidationError object cases
const errorMessage =
typeof err === "string"
? err
: err && typeof err === "object" && "message" in err
? (err as any).message
: JSON.stringify(err);
return `${nestedField}: ${errorMessage}`;
});
errors.push(...errRes);
}
}
return { isValid: errors.length === 0, errors };
}
// All individual validation methods removed - now delegated to TypeValidators
/**
* Validate entire object against schema
*/
static validateObject(schema: SchemaInterface, data: any): ValidationResult {
const errors: Record<string, string[]> = {};
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(),
};
}
}
/**
* Type definitions
*/
export interface ValidationFieldResult {
isValid: boolean;
errors: string[];
}
export interface ValidationResult {
isValid: boolean;
data: any;
errors: Record<string, string[]>;
timestamp: Date;
}