@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
300 lines • 11.3 kB
JavaScript
/**
* @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