UNPKG

@xynehq/jaf

Version:

Juspay Agent Framework - A purely functional agent framework with immutable state and composable tools

463 lines 15.9 kB
/** * JAF ADK Layer - Schema Validation * * Functional schema validation system */ import { throwValidationError } from '../types'; // Re-export createValidationError for external use export { createValidationError } from '../types'; // ========== Schema Validator Creation ========== export const createSchemaValidator = (schema, validator) => ({ schema, validate: (data) => { try { // First run the type guard if (!validator(data)) { return { success: false, errors: ['Data does not match expected type'] }; } // Then run JSON schema validation const schemaValidation = validateAgainstJsonSchema(data, schema); if (!schemaValidation.success) { return { success: false, errors: schemaValidation.errors }; } return { success: true, data: data }; } catch (error) { return { success: false, errors: [`Validation error: ${error instanceof Error ? error.message : String(error)}`] }; } } }); // ========== JSON Schema Validation ========== export const validateAgainstJsonSchema = (data, schema) => { const errors = []; // Type validation if (!validateType(data, schema.type)) { errors.push(`Expected type ${schema.type}, got ${typeof data}`); return { success: false, errors }; } // Specific validations based on type switch (schema.type) { case 'object': { const objectValidation = validateObject(data, schema); if (!objectValidation.success) { errors.push(...(objectValidation.errors || [])); } break; } case 'array': { const arrayValidation = validateArray(data, schema); if (!arrayValidation.success) { errors.push(...(arrayValidation.errors || [])); } break; } case 'string': { const stringValidation = validateString(data, schema); if (!stringValidation.success) { errors.push(...(stringValidation.errors || [])); } break; } case 'number': { const numberValidation = validateNumber(data, schema); if (!numberValidation.success) { errors.push(...(numberValidation.errors || [])); } break; } } // Enum validation if (schema.enum && !schema.enum.includes(data)) { errors.push(`Value must be one of: ${schema.enum.join(', ')}`); } if (errors.length > 0) { return { success: false, errors }; } return { success: true, data }; }; const validateType = (data, type) => { switch (type) { case 'string': return typeof data === 'string'; case 'number': return typeof data === 'number'; case 'boolean': return typeof data === 'boolean'; case 'object': return typeof data === 'object' && data !== null && !Array.isArray(data); case 'array': return Array.isArray(data); case 'null': return data === null; default: return true; // Unknown types pass } }; const validateObject = (data, schema) => { const errors = []; // Check required properties if (schema.required) { for (const requiredProp of schema.required) { if (!(requiredProp in data)) { errors.push(`Missing required property: ${requiredProp}`); } } } // Validate properties if (schema.properties) { for (const [propName, propSchema] of Object.entries(schema.properties)) { if (propName in data) { const propValidation = validateAgainstJsonSchema(data[propName], propSchema); if (!propValidation.success) { errors.push(`Property '${propName}': ${propValidation.errors?.join(', ')}`); } } } } if (errors.length > 0) { return { success: false, errors }; } return { success: true, data }; }; const validateArray = (data, schema) => { const errors = []; // Min items validation if (schema.minItems !== undefined && data.length < schema.minItems) { errors.push(`Array must have at least ${schema.minItems} items`); } // Max items validation if (schema.maxItems !== undefined && data.length > schema.maxItems) { errors.push(`Array must have at most ${schema.maxItems} items`); } // Unique items validation if (schema.uniqueItems) { const seen = new Set(); for (const item of data) { const key = JSON.stringify(item); if (seen.has(key)) { errors.push('Array must contain unique items'); break; } seen.add(key); } } // Validate items if (schema.items) { for (let i = 0; i < data.length; i++) { const itemValidation = validateAgainstJsonSchema(data[i], schema.items); if (!itemValidation.success) { errors.push(`Item ${i}: ${itemValidation.errors?.join(', ')}`); } } } if (errors.length > 0) { return { success: false, errors }; } return { success: true, data }; }; const validateString = (data, schema) => { const errors = []; // Min length validation if (schema.minLength !== undefined && data.length < schema.minLength) { errors.push(`String length must be at least ${schema.minLength}`); } // Max length validation if (schema.maxLength !== undefined && data.length > schema.maxLength) { errors.push(`String length must be at most ${schema.maxLength}`); } // Pattern validation if (schema.pattern) { try { const regex = new RegExp(schema.pattern); if (!regex.test(data)) { errors.push(`String does not match pattern: ${schema.pattern}`); } } catch { errors.push(`Invalid regex pattern: ${schema.pattern}`); } } // Format validation (basic common formats) if (schema.format) { switch (schema.format) { case 'email': if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) { errors.push('Invalid email format'); } break; case 'uri': case 'url': try { new URL(data); } catch { errors.push('Invalid URL format'); } break; case 'date': if (!/^\d{4}-\d{2}-\d{2}$/.test(data)) { errors.push('Invalid date format (expected YYYY-MM-DD)'); } break; case 'date-time': if (isNaN(Date.parse(data))) { errors.push('Invalid date-time format'); } break; case 'uuid': if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(data)) { errors.push('Invalid UUID format'); } break; } } if (errors.length > 0) { return { success: false, errors }; } return { success: true, data }; }; const validateNumber = (data, schema) => { const errors = []; // Minimum validation if (schema.minimum !== undefined) { if (schema.exclusiveMinimum && data <= schema.minimum) { errors.push(`Number must be greater than ${schema.minimum}`); } else if (!schema.exclusiveMinimum && data < schema.minimum) { errors.push(`Number must be at least ${schema.minimum}`); } } // Maximum validation if (schema.maximum !== undefined) { if (schema.exclusiveMaximum && data >= schema.maximum) { errors.push(`Number must be less than ${schema.maximum}`); } else if (!schema.exclusiveMaximum && data > schema.maximum) { errors.push(`Number must be at most ${schema.maximum}`); } } // Multiple of validation if (schema.multipleOf !== undefined) { const remainder = data % schema.multipleOf; // Handle floating point precision issues if (Math.abs(remainder) > 0.0000001 && Math.abs(remainder - schema.multipleOf) > 0.0000001) { errors.push(`Number must be a multiple of ${schema.multipleOf}`); } } // Integer validation if (schema.type === 'integer' && !Number.isInteger(data)) { errors.push('Number must be an integer'); } if (errors.length > 0) { return { success: false, errors }; } return { success: true, data }; }; // ========== Common Type Guards ========== export const isString = (value) => { return typeof value === 'string'; }; export const isNumber = (value) => { return typeof value === 'number'; }; export const isBoolean = (value) => { return typeof value === 'boolean'; }; export const isObject = (value) => { return typeof value === 'object' && value !== null && !Array.isArray(value); }; export const isArray = (value) => { return Array.isArray(value); }; export const isNull = (value) => { return value === null; }; export const isUndefined = (value) => { return value === undefined; }; // ========== Schema Builders ========== export const stringSchema = (options) => ({ type: 'string', description: options?.description, enum: options?.enum, default: options?.default }); export const numberSchema = (options) => ({ type: 'number', description: options?.description, enum: options?.enum, default: options?.default }); export const booleanSchema = (options) => ({ type: 'boolean', description: options?.description, default: options?.default }); export const objectSchema = (properties, required, options) => ({ type: 'object', properties, required, description: options?.description }); export const arraySchema = (items, options) => ({ type: 'array', items, description: options?.description }); // ========== Common Validators ========== export const createStringValidator = (options) => { return createSchemaValidator(stringSchema(options), isString); }; export const createNumberValidator = (options) => { return createSchemaValidator(numberSchema(options), isNumber); }; export const createBooleanValidator = (options) => { return createSchemaValidator(booleanSchema(options), isBoolean); }; export const createObjectValidator = (properties, required, typeGuard) => { const schema = objectSchema(properties, required); const guard = typeGuard || isObject; return createSchemaValidator(schema, guard); }; export const createArrayValidator = (items, typeGuard) => { const schema = arraySchema(items); const guard = typeGuard || isArray; return createSchemaValidator(schema, guard); }; // ========== Composite Type Guards ========== export const isOptional = (guard) => { return (value) => { return value === undefined || guard(value); }; }; export const isNullable = (guard) => { return (value) => { return value === null || guard(value); }; }; export const isUnion = (...guards) => { return (value) => { return guards.some(guard => guard(value)); }; }; export const hasProperty = (key) => { return (value) => { return isObject(value) && key in value; }; }; export const hasProperties = (...keys) => { return (value) => { if (!isObject(value)) return false; return keys.every(key => key in value); }; }; // ========== Validation Utilities ========== export const validateInput = (validator, data) => { return validator.validate(data); }; export const validateOutput = (validator, data) => { return validator.validate(data); }; export const assertValid = (validator, data) => { const result = validator.validate(data); if (!result.success) { throwValidationError(`Validation failed: ${result.errors?.join(', ')}`, result.errors || [], { data }); } return result.data; }; export const isValid = (validator, data) => { const result = validator.validate(data); return result.success; }; // ========== Schema Transformation ========== export const transformAndValidate = (validator, transformer, data) => { try { const transformed = transformer(data); return validator.validate(transformed); } catch (error) { return { success: false, errors: [`Transformation failed: ${error instanceof Error ? error.message : String(error)}`] }; } }; export const validateAndTransform = (inputValidator, transformer, data) => { const inputResult = inputValidator.validate(data); if (!inputResult.success) { return { success: false, errors: inputResult.errors }; } try { const transformed = transformer(inputResult.data); return { success: true, data: transformed }; } catch (error) { return { success: false, errors: [`Transformation failed: ${error instanceof Error ? error.message : String(error)}`] }; } }; export const weatherQueryValidator = createObjectValidator({ location: stringSchema({ description: 'City or location name' }), units: stringSchema({ description: 'Temperature units', enum: ['celsius', 'fahrenheit'], default: 'celsius' }), includeHourly: booleanSchema({ description: 'Include hourly forecast', default: false }) }, ['location'], (value) => { return isObject(value) && hasProperty('location')(value) && isString(value.location); }); export const weatherResponseValidator = createObjectValidator({ location: stringSchema({ description: 'Location name' }), temperature: numberSchema({ description: 'Current temperature' }), condition: stringSchema({ description: 'Weather condition' }), humidity: numberSchema({ description: 'Humidity percentage' }), forecast: arraySchema(objectSchema({ time: stringSchema({ description: 'Forecast time' }), temperature: numberSchema({ description: 'Forecast temperature' }), condition: stringSchema({ description: 'Forecast condition' }) }, ['time', 'temperature', 'condition'])) }, ['location', 'temperature', 'condition', 'humidity']); // ========== Schema Error Handling ========== // Note: createValidationError is now imported from types.ts as a factory function export const withSchemaValidation = (fn, inputValidators, outputValidator) => { return (...args) => { // Validate inputs if (inputValidators) { for (let i = 0; i < inputValidators.length && i < args.length; i++) { const validation = inputValidators[i].validate(args[i]); if (!validation.success) { throwValidationError(`Input validation failed for argument ${i}`, validation.errors || [], { argument: i, value: args[i] }); } } } // Execute function const result = fn(...args); // Validate output if (outputValidator) { const validation = outputValidator.validate(result); if (!validation.success) { throwValidationError('Output validation failed', validation.errors || [], { result }); } } return result; }; }; //# sourceMappingURL=index.js.map