182 lines
6.5 KiB
JavaScript
182 lines
6.5 KiB
JavaScript
/**
|
|
* @claude-flow/mcp - JSON Schema Validator
|
|
*
|
|
* Lightweight JSON Schema validation for tool inputs
|
|
* Implements JSON Schema Draft 2020-12 subset
|
|
*/
|
|
/**
|
|
* Validate data against JSON Schema
|
|
*/
|
|
export function validateSchema(data, schema, path = '') {
|
|
const errors = [];
|
|
// Type validation
|
|
if (schema.type) {
|
|
const typeValid = validateType(data, schema.type);
|
|
if (!typeValid) {
|
|
errors.push({
|
|
path: path || 'root',
|
|
message: `Expected type "${schema.type}", got "${typeof data}"`,
|
|
keyword: 'type',
|
|
params: { expected: schema.type, actual: typeof data },
|
|
});
|
|
return { valid: false, errors };
|
|
}
|
|
}
|
|
// Null check
|
|
if (data === null || data === undefined) {
|
|
if (schema.type && schema.type !== 'null') {
|
|
errors.push({
|
|
path: path || 'root',
|
|
message: 'Value cannot be null or undefined',
|
|
keyword: 'type',
|
|
});
|
|
}
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
// String validations
|
|
if (schema.type === 'string' && typeof data === 'string') {
|
|
if (schema.minLength !== undefined && data.length < schema.minLength) {
|
|
errors.push({
|
|
path,
|
|
message: `String length must be >= ${schema.minLength}`,
|
|
keyword: 'minLength',
|
|
params: { limit: schema.minLength, actual: data.length },
|
|
});
|
|
}
|
|
if (schema.maxLength !== undefined && data.length > schema.maxLength) {
|
|
errors.push({
|
|
path,
|
|
message: `String length must be <= ${schema.maxLength}`,
|
|
keyword: 'maxLength',
|
|
params: { limit: schema.maxLength, actual: data.length },
|
|
});
|
|
}
|
|
if (schema.pattern) {
|
|
const regex = new RegExp(schema.pattern);
|
|
if (!regex.test(data)) {
|
|
errors.push({
|
|
path,
|
|
message: `String must match pattern "${schema.pattern}"`,
|
|
keyword: 'pattern',
|
|
params: { pattern: schema.pattern },
|
|
});
|
|
}
|
|
}
|
|
if (schema.enum && !schema.enum.includes(data)) {
|
|
errors.push({
|
|
path,
|
|
message: `Value must be one of: ${schema.enum.join(', ')}`,
|
|
keyword: 'enum',
|
|
params: { allowedValues: schema.enum },
|
|
});
|
|
}
|
|
}
|
|
// Number validations
|
|
if ((schema.type === 'number' || schema.type === 'integer') && typeof data === 'number') {
|
|
if (schema.type === 'integer' && !Number.isInteger(data)) {
|
|
errors.push({
|
|
path,
|
|
message: 'Value must be an integer',
|
|
keyword: 'type',
|
|
});
|
|
}
|
|
if (schema.minimum !== undefined && data < schema.minimum) {
|
|
errors.push({
|
|
path,
|
|
message: `Value must be >= ${schema.minimum}`,
|
|
keyword: 'minimum',
|
|
params: { limit: schema.minimum, actual: data },
|
|
});
|
|
}
|
|
if (schema.maximum !== undefined && data > schema.maximum) {
|
|
errors.push({
|
|
path,
|
|
message: `Value must be <= ${schema.maximum}`,
|
|
keyword: 'maximum',
|
|
params: { limit: schema.maximum, actual: data },
|
|
});
|
|
}
|
|
}
|
|
// Array validations
|
|
if (schema.type === 'array' && Array.isArray(data)) {
|
|
if (schema.items) {
|
|
for (let i = 0; i < data.length; i++) {
|
|
const itemResult = validateSchema(data[i], schema.items, `${path}[${i}]`);
|
|
errors.push(...itemResult.errors);
|
|
}
|
|
}
|
|
}
|
|
// Object validations
|
|
if (schema.type === 'object' && typeof data === 'object' && !Array.isArray(data)) {
|
|
const obj = data;
|
|
// Required properties
|
|
if (schema.required) {
|
|
for (const requiredProp of schema.required) {
|
|
if (!(requiredProp in obj)) {
|
|
errors.push({
|
|
path: path ? `${path}.${requiredProp}` : requiredProp,
|
|
message: `Required property "${requiredProp}" is missing`,
|
|
keyword: 'required',
|
|
params: { missingProperty: requiredProp },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Property validations
|
|
if (schema.properties) {
|
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
if (propName in obj) {
|
|
const propPath = path ? `${path}.${propName}` : propName;
|
|
const propResult = validateSchema(obj[propName], propSchema, propPath);
|
|
errors.push(...propResult.errors);
|
|
}
|
|
}
|
|
}
|
|
// Additional properties check
|
|
if (schema.additionalProperties === false && schema.properties) {
|
|
const allowedProps = new Set(Object.keys(schema.properties));
|
|
for (const propName of Object.keys(obj)) {
|
|
if (!allowedProps.has(propName)) {
|
|
errors.push({
|
|
path: path ? `${path}.${propName}` : propName,
|
|
message: `Additional property "${propName}" is not allowed`,
|
|
keyword: 'additionalProperties',
|
|
params: { additionalProperty: propName },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return { valid: errors.length === 0, errors };
|
|
}
|
|
/**
|
|
* Validate type
|
|
*/
|
|
function validateType(data, expectedType) {
|
|
if (expectedType === 'null') {
|
|
return data === null;
|
|
}
|
|
if (expectedType === 'array') {
|
|
return Array.isArray(data);
|
|
}
|
|
if (expectedType === 'integer') {
|
|
return typeof data === 'number' && Number.isInteger(data);
|
|
}
|
|
if (expectedType === 'object') {
|
|
return typeof data === 'object' && data !== null && !Array.isArray(data);
|
|
}
|
|
return typeof data === expectedType;
|
|
}
|
|
/**
|
|
* Format validation errors for display
|
|
*/
|
|
export function formatValidationErrors(errors) {
|
|
return errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
}
|
|
/**
|
|
* Create a validator function for a specific schema
|
|
*/
|
|
export function createValidator(schema) {
|
|
return (data) => validateSchema(data, schema);
|
|
}
|
|
//# sourceMappingURL=schema-validator.js.map
|