UNPKG

@gati-framework/runtime

Version:

Gati runtime execution engine for running handler-based applications

300 lines 11.3 kB
/** * @module runtime/gtype/validator * @description GType validator implementation */ import { createValidationError, validResult, invalidResult, mergeResults, } from './errors.js'; /** * Validate a value against a GType schema */ export function validate(value, schema, path = []) { // Special case: if schema explicitly expects undefined, allow it if (schema.kind === 'primitive' && schema.primitiveType === 'undefined') { if (value === undefined) { return validResult(); } return invalidResult([ createValidationError(path, 'undefined', value, undefined, schema), ]); } // Handle optional if (schema.optional && value === undefined) { return validResult(); } // Handle nullable if (schema.nullable && value === null) { return validResult(); } // Check for required value if (!schema.optional && value === undefined) { return invalidResult([ createValidationError(path, 'defined value', value, 'Value is required', schema), ]); } // Validate based on kind let result; switch (schema.kind) { case 'primitive': result = validatePrimitive(value, schema, path); break; case 'literal': result = validateLiteral(value, schema, path); break; case 'object': result = validateObject(value, schema, path); break; case 'array': result = validateArray(value, schema, path); break; case 'tuple': result = validateTuple(value, schema, path); break; case 'union': result = validateUnion(value, schema, path); break; case 'intersection': result = validateIntersection(value, schema, path); break; case 'enum': result = validateEnum(value, schema, path); break; default: return invalidResult([ createValidationError(path, 'known type', value, `Unknown schema kind: ${schema.kind}`), ]); } // Apply custom validators if (result.valid && schema.validators) { const validatorErrors = validateCustom(value, schema.validators, path, schema); if (validatorErrors.length > 0) { result = invalidResult(validatorErrors); } } return result; } /** * Validate primitive type */ function validatePrimitive(value, schema, path) { const actualType = value === null ? 'null' : typeof value; if (actualType !== schema.primitiveType) { return invalidResult([ createValidationError(path, schema.primitiveType, value, undefined, schema), ]); } return validResult(); } /** * Validate literal value */ function validateLiteral(value, schema, path) { if (value !== schema.value) { return invalidResult([ createValidationError(path, `literal ${JSON.stringify(schema.value)}`, value, undefined, schema), ]); } return validResult(); } /** * Validate object type */ function validateObject(value, schema, path) { if (typeof value !== 'object' || value === null || Array.isArray(value)) { return invalidResult([ createValidationError(path, 'object', value, undefined, schema), ]); } const obj = value; const errors = []; // Validate required properties const required = schema.required || []; for (const key of required) { if (!(key in obj)) { errors.push(createValidationError([...path, key], 'defined value', undefined, `Required property "${key}" is missing`, schema)); } } // Validate each property for (const [key, propSchema] of Object.entries(schema.properties)) { if (key in obj) { const propResult = validate(obj[key], propSchema, [...path, key]); errors.push(...propResult.errors); } } // Check for additional properties if (schema.additionalProperties === false) { const allowedKeys = new Set(Object.keys(schema.properties)); for (const key of Object.keys(obj)) { if (!allowedKeys.has(key)) { errors.push(createValidationError([...path, key], 'no additional properties', obj[key], `Additional property "${key}" is not allowed`, schema)); } } } else if (typeof schema.additionalProperties === 'object') { // Validate additional properties against schema const allowedKeys = new Set(Object.keys(schema.properties)); for (const key of Object.keys(obj)) { if (!allowedKeys.has(key)) { const propResult = validate(obj[key], schema.additionalProperties, [...path, key]); errors.push(...propResult.errors); } } } return errors.length === 0 ? validResult() : invalidResult(errors); } /** * Validate array type */ function validateArray(value, schema, path) { if (!Array.isArray(value)) { return invalidResult([ createValidationError(path, 'array', value, undefined, schema), ]); } const errors = []; // Validate length constraints if (schema.minItems !== undefined && value.length < schema.minItems) { errors.push(createValidationError(path, `array with at least ${schema.minItems} items`, value, `Array must have at least ${schema.minItems} items, got ${value.length}`, schema)); } if (schema.maxItems !== undefined && value.length > schema.maxItems) { errors.push(createValidationError(path, `array with at most ${schema.maxItems} items`, value, `Array must have at most ${schema.maxItems} items, got ${value.length}`, schema)); } // Validate each item for (let i = 0; i < value.length; i++) { const itemResult = validate(value[i], schema.items, [...path, i]); errors.push(...itemResult.errors); } return errors.length === 0 ? validResult() : invalidResult(errors); } /** * Validate tuple type */ function validateTuple(value, schema, path) { if (!Array.isArray(value)) { return invalidResult([ createValidationError(path, 'tuple', value, undefined, schema), ]); } if (value.length !== schema.items.length) { return invalidResult([ createValidationError(path, `tuple with ${schema.items.length} items`, value, `Tuple must have exactly ${schema.items.length} items, got ${value.length}`, schema), ]); } const errors = []; for (let i = 0; i < schema.items.length; i++) { const itemResult = validate(value[i], schema.items[i], [...path, i]); errors.push(...itemResult.errors); } return errors.length === 0 ? validResult() : invalidResult(errors); } /** * Validate union type (value must match at least one type) */ function validateUnion(value, schema, path) { for (const type of schema.types) { const result = validate(value, type, path); if (result.valid) { return validResult(); } } return invalidResult([ createValidationError(path, `one of ${schema.types.length} types`, value, 'Value does not match any of the union types', schema), ]); } /** * Validate intersection type (value must match all types) */ function validateIntersection(value, schema, path) { const results = schema.types.map((type) => validate(value, type, path)); return mergeResults(...results); } /** * Validate enum type */ function validateEnum(value, schema, path) { if (!schema.values.includes(value)) { return invalidResult([ createValidationError(path, `one of [${schema.values.join(', ')}]`, value, undefined, schema), ]); } return validResult(); } /** * Validate custom validators */ function validateCustom(value, validators, path, schema) { const errors = []; for (const validator of validators) { const error = validateSingle(value, validator, path, schema); if (error) { errors.push(error); } } return errors; } /** * Validate a single custom validator */ function validateSingle(value, validator, path, schema) { switch (validator.type) { case 'min': if (typeof value === 'number' && value < validator.value) { return createValidationError(path, `>= ${validator.value}`, value, validator.message || `Value must be at least ${validator.value}`, schema); } break; case 'max': if (typeof value === 'number' && value > validator.value) { return createValidationError(path, `<= ${validator.value}`, value, validator.message || `Value must be at most ${validator.value}`, schema); } break; case 'minLength': if (typeof value === 'string' && value.length < validator.value) { return createValidationError(path, `string with length >= ${validator.value}`, value, validator.message || `String must be at least ${validator.value} characters`, schema); } break; case 'maxLength': if (typeof value === 'string' && value.length > validator.value) { return createValidationError(path, `string with length <= ${validator.value}`, value, validator.message || `String must be at most ${validator.value} characters`, schema); } break; case 'pattern': if (typeof value === 'string') { const regex = new RegExp(validator.value); if (!regex.test(value)) { return createValidationError(path, `string matching ${validator.value}`, value, validator.message || `String must match pattern ${validator.value}`, schema); } } break; case 'email': if (typeof value === 'string') { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return createValidationError(path, 'valid email', value, validator.message || 'Must be a valid email address', schema); } } break; case 'url': if (typeof value === 'string') { try { new URL(value); } catch { return createValidationError(path, 'valid URL', value, validator.message || 'Must be a valid URL', schema); } } break; case 'uuid': if (typeof value === '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 createValidationError(path, 'valid UUID', value, validator.message || 'Must be a valid UUID', schema); } } break; case 'custom': if (validator.fn && !validator.fn(value)) { return createValidationError(path, 'custom validation', value, validator.message || 'Custom validation failed', schema); } break; } return null; } //# sourceMappingURL=validator.js.map