rename 'validation' folder->'validators' (#1340)
Co-authored-by: richarda23 <richard.c.adams@infinityworks.com> Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
This commit is contained in:
parent
108f2f3ab6
commit
8cdb4bb402
10 changed files with 18 additions and 18 deletions
|
|
@ -102,10 +102,10 @@ The SDK uses `zod/v4` internally. Schema utilities live in:
|
|||
|
||||
### Validation
|
||||
|
||||
Pluggable JSON Schema validation (`packages/core/src/validation/`):
|
||||
Pluggable JSON Schema validation (`packages/core/src/validators/`):
|
||||
|
||||
- `ajv-provider.ts` - Default Ajv-based validator
|
||||
- `cfworker-provider.ts` - Cloudflare Workers-compatible alternative
|
||||
- `ajvProvider.ts` - Default Ajv-based validator
|
||||
- `cfWorkerProvider.ts` - Cloudflare Workers-compatible alternative
|
||||
|
||||
### Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
* @module
|
||||
*/
|
||||
|
||||
import { AjvJsonSchemaValidator } from './validation/ajvProvider.js';
|
||||
import { CfWorkerJsonSchemaValidator } from './validation/cfWorkerProvider.js';
|
||||
import { AjvJsonSchemaValidator } from './validators/ajvProvider.js';
|
||||
import { CfWorkerJsonSchemaValidator } from './validators/cfWorkerProvider.js';
|
||||
|
||||
/**
|
||||
* Example: AJV validator for Node.js.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export * from './util/schema.js';
|
|||
|
||||
// experimental exports
|
||||
export * from './experimental/index.js';
|
||||
export * from './validation/ajvProvider.js';
|
||||
export * from './validation/cfWorkerProvider.js';
|
||||
export * from './validators/ajvProvider.js';
|
||||
export * from './validators/cfWorkerProvider.js';
|
||||
/**
|
||||
* JSON Schema validation
|
||||
*
|
||||
|
|
@ -24,11 +24,11 @@ export * from './validation/cfWorkerProvider.js';
|
|||
* Choose a validator based on your runtime environment:
|
||||
*
|
||||
* - {@linkcode AjvJsonSchemaValidator}: Best for Node.js (default, fastest)
|
||||
* Import from: @modelcontextprotocol/sdk/validation/ajv
|
||||
* Import from: @modelcontextprotocol/sdk/validators/ajv
|
||||
* Requires peer dependencies: ajv, ajv-formats
|
||||
*
|
||||
* - {@linkcode CfWorkerJsonSchemaValidator}: Best for edge runtimes
|
||||
* Import from: @modelcontextprotocol/sdk/validation/cfworker
|
||||
* Import from: @modelcontextprotocol/sdk/validators/cfworker
|
||||
* Requires peer dependency: @cfworker/json-schema
|
||||
*
|
||||
* @example For Node.js with AJV
|
||||
|
|
@ -45,4 +45,4 @@ export * from './validation/cfWorkerProvider.js';
|
|||
*/
|
||||
|
||||
// Core types only - implementations are exported via separate entry points
|
||||
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validation/types.js';
|
||||
export type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './validators/types.js';
|
||||
|
|
|
|||
48
packages/core/src/validators/ajvProvider.examples.ts
Normal file
48
packages/core/src/validators/ajvProvider.examples.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Type-checked examples for `ajvProvider.ts`.
|
||||
*
|
||||
* These examples are synced into JSDoc comments via the sync-snippets script.
|
||||
* Each function's region markers define the code snippet that appears in the docs.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { Ajv } from 'ajv';
|
||||
import _addFormats from 'ajv-formats';
|
||||
|
||||
import { AjvJsonSchemaValidator } from './ajvProvider.js';
|
||||
|
||||
const addFormats = _addFormats as unknown as typeof _addFormats.default;
|
||||
|
||||
/**
|
||||
* Example: Default AJV instance.
|
||||
*/
|
||||
function AjvJsonSchemaValidator_default() {
|
||||
//#region AjvJsonSchemaValidator_default
|
||||
const validator = new AjvJsonSchemaValidator();
|
||||
//#endregion AjvJsonSchemaValidator_default
|
||||
return validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Custom AJV instance.
|
||||
*/
|
||||
function AjvJsonSchemaValidator_customInstance() {
|
||||
//#region AjvJsonSchemaValidator_customInstance
|
||||
const ajv = new Ajv({ strict: true, allErrors: true });
|
||||
const validator = new AjvJsonSchemaValidator(ajv);
|
||||
//#endregion AjvJsonSchemaValidator_customInstance
|
||||
return validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Constructor with advanced AJV configuration including formats.
|
||||
*/
|
||||
function AjvJsonSchemaValidator_constructor_withFormats() {
|
||||
//#region AjvJsonSchemaValidator_constructor_withFormats
|
||||
const ajv = new Ajv({ validateFormats: true });
|
||||
addFormats(ajv);
|
||||
const validator = new AjvJsonSchemaValidator(ajv);
|
||||
//#endregion AjvJsonSchemaValidator_constructor_withFormats
|
||||
return validator;
|
||||
}
|
||||
94
packages/core/src/validators/ajvProvider.ts
Normal file
94
packages/core/src/validators/ajvProvider.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* AJV-based JSON Schema validator provider
|
||||
*/
|
||||
|
||||
import { Ajv } from 'ajv';
|
||||
import _addFormats from 'ajv-formats';
|
||||
|
||||
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types.js';
|
||||
|
||||
function createDefaultAjvInstance(): Ajv {
|
||||
const ajv = new Ajv({
|
||||
strict: false,
|
||||
validateFormats: true,
|
||||
validateSchema: false,
|
||||
allErrors: true
|
||||
});
|
||||
|
||||
const addFormats = _addFormats as unknown as typeof _addFormats.default;
|
||||
addFormats(ajv);
|
||||
|
||||
return ajv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @example Use with default AJV instance (recommended)
|
||||
* ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default"
|
||||
* const validator = new AjvJsonSchemaValidator();
|
||||
* ```
|
||||
*
|
||||
* @example Use with custom AJV instance
|
||||
* ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_customInstance"
|
||||
* const ajv = new Ajv({ strict: true, allErrors: true });
|
||||
* const validator = new AjvJsonSchemaValidator(ajv);
|
||||
* ```
|
||||
*
|
||||
* @see {@linkcode CfWorkerJsonSchemaValidator} for an edge-runtime-compatible alternative
|
||||
*/
|
||||
export class AjvJsonSchemaValidator implements jsonSchemaValidator {
|
||||
private _ajv: Ajv;
|
||||
|
||||
/**
|
||||
* Create an AJV validator
|
||||
*
|
||||
* @param ajv - Optional pre-configured AJV instance. If not provided, a default instance will be created.
|
||||
*
|
||||
* @example Use default configuration (recommended for most cases)
|
||||
* ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default"
|
||||
* const validator = new AjvJsonSchemaValidator();
|
||||
* ```
|
||||
*
|
||||
* @example Provide custom AJV instance for advanced configuration
|
||||
* ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_constructor_withFormats"
|
||||
* const ajv = new Ajv({ validateFormats: true });
|
||||
* addFormats(ajv);
|
||||
* const validator = new AjvJsonSchemaValidator(ajv);
|
||||
* ```
|
||||
*/
|
||||
constructor(ajv?: Ajv) {
|
||||
this._ajv = ajv ?? createDefaultAjvInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validator for the given JSON Schema
|
||||
*
|
||||
* The validator is compiled once and can be reused multiple times.
|
||||
* If the schema has an `$id`, it will be cached by AJV automatically.
|
||||
*
|
||||
* @param schema - Standard JSON Schema object
|
||||
* @returns A validator function that validates input data
|
||||
*/
|
||||
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
||||
// Check if schema has $id and is already compiled/cached
|
||||
const ajvValidator =
|
||||
'$id' in schema && typeof schema.$id === 'string'
|
||||
? (this._ajv.getSchema(schema.$id) ?? this._ajv.compile(schema))
|
||||
: this._ajv.compile(schema);
|
||||
|
||||
return (input: unknown): JsonSchemaValidatorResult<T> => {
|
||||
const valid = ajvValidator(input);
|
||||
|
||||
return valid
|
||||
? {
|
||||
valid: true,
|
||||
data: input as T,
|
||||
errorMessage: undefined
|
||||
}
|
||||
: {
|
||||
valid: false,
|
||||
data: undefined,
|
||||
errorMessage: this._ajv.errorsText(ajvValidator.errors)
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
33
packages/core/src/validators/cfWorkerProvider.examples.ts
Normal file
33
packages/core/src/validators/cfWorkerProvider.examples.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Type-checked examples for `cfWorkerProvider.ts`.
|
||||
*
|
||||
* These examples are synced into JSDoc comments via the sync-snippets script.
|
||||
* Each function's region markers define the code snippet that appears in the docs.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { CfWorkerJsonSchemaValidator } from './cfWorkerProvider.js';
|
||||
|
||||
/**
|
||||
* Example: Default configuration.
|
||||
*/
|
||||
function CfWorkerJsonSchemaValidator_default() {
|
||||
//#region CfWorkerJsonSchemaValidator_default
|
||||
const validator = new CfWorkerJsonSchemaValidator();
|
||||
//#endregion CfWorkerJsonSchemaValidator_default
|
||||
return validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Custom configuration with all errors reported.
|
||||
*/
|
||||
function CfWorkerJsonSchemaValidator_customConfig() {
|
||||
//#region CfWorkerJsonSchemaValidator_customConfig
|
||||
const validator = new CfWorkerJsonSchemaValidator({
|
||||
draft: '2020-12',
|
||||
shortcircuit: false // Report all errors
|
||||
});
|
||||
//#endregion CfWorkerJsonSchemaValidator_customConfig
|
||||
return validator;
|
||||
}
|
||||
79
packages/core/src/validators/cfWorkerProvider.ts
Normal file
79
packages/core/src/validators/cfWorkerProvider.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Cloudflare Worker-compatible JSON Schema validator provider
|
||||
*
|
||||
* This provider uses @cfworker/json-schema for validation without code generation,
|
||||
* making it compatible with edge runtimes like Cloudflare Workers that restrict
|
||||
* eval and new Function.
|
||||
*
|
||||
* @see {@linkcode AjvJsonSchemaValidator} for the Node.js alternative
|
||||
*/
|
||||
|
||||
import { Validator } from '@cfworker/json-schema';
|
||||
|
||||
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, JsonSchemaValidatorResult } from './types.js';
|
||||
|
||||
/**
|
||||
* JSON Schema draft version supported by @cfworker/json-schema
|
||||
*/
|
||||
export type CfWorkerSchemaDraft = '4' | '7' | '2019-09' | '2020-12';
|
||||
|
||||
/**
|
||||
*
|
||||
* @example Use with default configuration (2020-12, shortcircuit)
|
||||
* ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_default"
|
||||
* const validator = new CfWorkerJsonSchemaValidator();
|
||||
* ```
|
||||
*
|
||||
* @example Use with custom configuration
|
||||
* ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_customConfig"
|
||||
* const validator = new CfWorkerJsonSchemaValidator({
|
||||
* draft: '2020-12',
|
||||
* shortcircuit: false // Report all errors
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class CfWorkerJsonSchemaValidator implements jsonSchemaValidator {
|
||||
private shortcircuit: boolean;
|
||||
private draft: CfWorkerSchemaDraft;
|
||||
|
||||
/**
|
||||
* Create a validator
|
||||
*
|
||||
* @param options - Configuration options
|
||||
* @param options.shortcircuit - If `true`, stop validation after first error (default: `true`)
|
||||
* @param options.draft - JSON Schema draft version to use (default: `'2020-12'`)
|
||||
*/
|
||||
constructor(options?: { shortcircuit?: boolean; draft?: CfWorkerSchemaDraft }) {
|
||||
this.shortcircuit = options?.shortcircuit ?? true;
|
||||
this.draft = options?.draft ?? '2020-12';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validator for the given JSON Schema
|
||||
*
|
||||
* Unlike AJV, this validator is not cached internally
|
||||
*
|
||||
* @param schema - Standard JSON Schema object
|
||||
* @returns A validator function that validates input data
|
||||
*/
|
||||
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
||||
// Cast to the cfworker Schema type - our JsonSchemaType is structurally compatible
|
||||
const validator = new Validator(schema as ConstructorParameters<typeof Validator>[0], this.draft, this.shortcircuit);
|
||||
|
||||
return (input: unknown): JsonSchemaValidatorResult<T> => {
|
||||
const result = validator.validate(input);
|
||||
|
||||
return result.valid
|
||||
? {
|
||||
valid: true,
|
||||
data: input as T,
|
||||
errorMessage: undefined
|
||||
}
|
||||
: {
|
||||
valid: false,
|
||||
data: undefined,
|
||||
errorMessage: result.errors.map(err => `${err.instanceLocation}: ${err.error}`).join('; ')
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
31
packages/core/src/validators/types.examples.ts
Normal file
31
packages/core/src/validators/types.examples.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Type-checked examples for `types.ts`.
|
||||
*
|
||||
* These examples are synced into JSDoc comments via the sync-snippets script.
|
||||
* Each function's region markers define the code snippet that appears in the docs.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from './types.js';
|
||||
|
||||
// Stub for hypothetical schema validation function
|
||||
declare function isValid(schema: JsonSchemaType, input: unknown): boolean;
|
||||
|
||||
/**
|
||||
* Example: Implementing the jsonSchemaValidator interface.
|
||||
*/
|
||||
function jsonSchemaValidator_implementation() {
|
||||
//#region jsonSchemaValidator_implementation
|
||||
class MyValidatorProvider implements jsonSchemaValidator {
|
||||
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
||||
// Compile/cache validator from schema
|
||||
return (input: unknown) =>
|
||||
isValid(schema, input)
|
||||
? { valid: true, data: input as T, errorMessage: undefined }
|
||||
: { valid: false, data: undefined, errorMessage: 'Error details' };
|
||||
}
|
||||
}
|
||||
//#endregion jsonSchemaValidator_implementation
|
||||
return MyValidatorProvider;
|
||||
}
|
||||
59
packages/core/src/validators/types.ts
Normal file
59
packages/core/src/validators/types.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Using the main export which points to draft-2020-12 by default
|
||||
import type { JSONSchema } from 'json-schema-typed';
|
||||
|
||||
/**
|
||||
* JSON Schema type definition (JSON Schema Draft 2020-12)
|
||||
*
|
||||
* This uses the object form of JSON Schema (excluding boolean schemas).
|
||||
* While `true` and `false` are valid JSON Schemas, this SDK uses the
|
||||
* object form for practical type safety.
|
||||
*
|
||||
* Re-exported from json-schema-typed for convenience.
|
||||
* @see https://json-schema.org/draft/2020-12/json-schema-core.html
|
||||
*/
|
||||
export type JsonSchemaType = JSONSchema.Interface;
|
||||
|
||||
/**
|
||||
* Result of a JSON Schema validation operation
|
||||
*/
|
||||
export type JsonSchemaValidatorResult<T> =
|
||||
| { valid: true; data: T; errorMessage: undefined }
|
||||
| { valid: false; data: undefined; errorMessage: string };
|
||||
|
||||
/**
|
||||
* A validator function that validates data against a JSON Schema
|
||||
*/
|
||||
export type JsonSchemaValidator<T> = (input: unknown) => JsonSchemaValidatorResult<T>;
|
||||
|
||||
/**
|
||||
* Provider interface for creating validators from JSON Schemas
|
||||
*
|
||||
* This is the main extension point for custom validator implementations.
|
||||
* Implementations should:
|
||||
* - Support JSON Schema Draft 2020-12 (or be compatible with it)
|
||||
* - Return validator functions that can be called multiple times
|
||||
* - Handle schema compilation/caching internally
|
||||
* - Provide clear error messages on validation failure
|
||||
*
|
||||
* @example
|
||||
* ```ts source="./types.examples.ts#jsonSchemaValidator_implementation"
|
||||
* class MyValidatorProvider implements jsonSchemaValidator {
|
||||
* getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
||||
* // Compile/cache validator from schema
|
||||
* return (input: unknown) =>
|
||||
* isValid(schema, input)
|
||||
* ? { valid: true, data: input as T, errorMessage: undefined }
|
||||
* : { valid: false, data: undefined, errorMessage: 'Error details' };
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface jsonSchemaValidator {
|
||||
/**
|
||||
* Create a validator for the given JSON Schema
|
||||
*
|
||||
* @param schema - Standard JSON Schema object
|
||||
* @returns A validator function that can be called multiple times
|
||||
*/
|
||||
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T>;
|
||||
}
|
||||
625
packages/core/test/validators/validators.test.ts
Normal file
625
packages/core/test/validators/validators.test.ts
Normal file
|
|
@ -0,0 +1,625 @@
|
|||
/**
|
||||
* Tests all validator providers with various JSON Schema 2020-12 features
|
||||
* Based on MCP specification for elicitation schemas:
|
||||
* https://modelcontextprotocol.io/specification/draft/client/elicitation.md
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { AjvJsonSchemaValidator } from '../../src/validators/ajvProvider.js';
|
||||
import { CfWorkerJsonSchemaValidator } from '../../src/validators/cfWorkerProvider.js';
|
||||
import type { JsonSchemaType } from '../../src/validators/types.js';
|
||||
|
||||
// Test with both AJV and CfWorker validators
|
||||
// AJV validator will use default configuration with format validation enabled
|
||||
const validators = [
|
||||
{ name: 'AJV', provider: new AjvJsonSchemaValidator() },
|
||||
{ name: 'CfWorker', provider: new CfWorkerJsonSchemaValidator() }
|
||||
];
|
||||
|
||||
describe('JSON Schema Validators', () => {
|
||||
describe.each(validators)('$name Validator', ({ provider }) => {
|
||||
describe('String schemas', () => {
|
||||
it('validates basic string', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
const validResult = validator('hello');
|
||||
expect(validResult.valid).toBe(true);
|
||||
expect(validResult.data).toBe('hello');
|
||||
|
||||
const invalidResult = validator(123);
|
||||
expect(invalidResult.valid).toBe(false);
|
||||
expect(invalidResult.errorMessage).toBeDefined();
|
||||
});
|
||||
|
||||
it('validates string with title and description', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string',
|
||||
title: 'Name',
|
||||
description: "User's full name"
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
const result = validator('John Doe');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.data).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('validates string with length constraints', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string',
|
||||
minLength: 3,
|
||||
maxLength: 10
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('abc').valid).toBe(true);
|
||||
expect(validator('abcdefghij').valid).toBe(true);
|
||||
expect(validator('ab').valid).toBe(false);
|
||||
expect(validator('abcdefghijk').valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates email format', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string',
|
||||
format: 'email'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('user@example.com').valid).toBe(true);
|
||||
expect(validator('invalid-email').valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates URI format', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string',
|
||||
format: 'uri'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('https://example.com').valid).toBe(true);
|
||||
expect(validator('not-a-uri').valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates date-time format', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('2025-10-17T12:00:00Z').valid).toBe(true);
|
||||
expect(validator('not-a-date').valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates string pattern', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'string',
|
||||
pattern: '^[A-Z]{3}$'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('ABC').valid).toBe(true);
|
||||
expect(validator('abc').valid).toBe(false);
|
||||
expect(validator('ABCD').valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Number schemas', () => {
|
||||
it('validates number type', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'number'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(42).valid).toBe(true);
|
||||
expect(validator(3.14).valid).toBe(true);
|
||||
expect(validator('42').valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates integer type', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'integer'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(42).valid).toBe(true);
|
||||
expect(validator(3.14).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates number range', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
maximum: 100
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(0).valid).toBe(true);
|
||||
expect(validator(50).valid).toBe(true);
|
||||
expect(validator(100).valid).toBe(true);
|
||||
expect(validator(-1).valid).toBe(false);
|
||||
expect(validator(101).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Boolean schemas', () => {
|
||||
it('validates boolean type', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'boolean'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(true).valid).toBe(true);
|
||||
expect(validator(false).valid).toBe(true);
|
||||
expect(validator('true').valid).toBe(false);
|
||||
expect(validator(1).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates boolean with default', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(true).valid).toBe(true);
|
||||
expect(validator(false).valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enum schemas', () => {
|
||||
it('validates enum values', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
enum: ['red', 'green', 'blue']
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('red').valid).toBe(true);
|
||||
expect(validator('green').valid).toBe(true);
|
||||
expect(validator('blue').valid).toBe(true);
|
||||
expect(validator('yellow').valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates enum with mixed types', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
enum: ['option1', 42, true, null]
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('option1').valid).toBe(true);
|
||||
expect(validator(42).valid).toBe(true);
|
||||
expect(validator(true).valid).toBe(true);
|
||||
expect(validator(null).valid).toBe(true);
|
||||
expect(validator('other').valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Object schemas', () => {
|
||||
it('validates simple object', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' }
|
||||
},
|
||||
required: ['name']
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator({ name: 'John', age: 30 }).valid).toBe(true);
|
||||
expect(validator({ name: 'John' }).valid).toBe(true);
|
||||
expect(validator({ age: 30 }).valid).toBe(false);
|
||||
expect(validator({}).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates nested objects', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string', format: 'email' }
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
required: ['user']
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
user: { name: 'John', email: 'john@example.com' }
|
||||
}).valid
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
user: { name: 'John' }
|
||||
}).valid
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
user: { email: 'john@example.com' }
|
||||
}).valid
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('validates object with additionalProperties: false', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' }
|
||||
},
|
||||
additionalProperties: false
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator({ name: 'John' }).valid).toBe(true);
|
||||
expect(validator({ name: 'John', extra: 'field' }).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Array schemas', () => {
|
||||
it('validates array of strings', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(['a', 'b', 'c']).valid).toBe(true);
|
||||
expect(validator([]).valid).toBe(true);
|
||||
expect(validator(['a', 1, 'c']).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates array length constraints', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
minItems: 1,
|
||||
maxItems: 3
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator([1]).valid).toBe(true);
|
||||
expect(validator([1, 2, 3]).valid).toBe(true);
|
||||
expect(validator([]).valid).toBe(false);
|
||||
expect(validator([1, 2, 3, 4]).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates array with unique items', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
uniqueItems: true
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator([1, 2, 3]).valid).toBe(true);
|
||||
expect(validator([1, 2, 2, 3]).valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON Schema 2020-12 features', () => {
|
||||
it('validates schema with $schema field', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
type: 'string'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('test').valid).toBe(true);
|
||||
});
|
||||
|
||||
it('validates schema with $id field', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
$id: 'https://example.com/schemas/test',
|
||||
type: 'number'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator(42).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('validates with allOf', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
allOf: [
|
||||
{ type: 'object', properties: { name: { type: 'string' } } },
|
||||
{ type: 'object', properties: { age: { type: 'number' } } }
|
||||
]
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator({ name: 'John', age: 30 }).valid).toBe(true);
|
||||
expect(validator({ name: 'John' }).valid).toBe(true);
|
||||
expect(validator({ name: 123 }).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates with anyOf', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
anyOf: [{ type: 'string' }, { type: 'number' }]
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('test').valid).toBe(true);
|
||||
expect(validator(42).valid).toBe(true);
|
||||
expect(validator(true).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates with oneOf', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
oneOf: [
|
||||
{ type: 'string', minLength: 5 },
|
||||
{ type: 'string', maxLength: 3 }
|
||||
]
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('ab').valid).toBe(true); // Matches second only
|
||||
expect(validator('hello').valid).toBe(true); // Matches first only
|
||||
expect(validator('abcd').valid).toBe(false); // Matches neither
|
||||
});
|
||||
|
||||
it('validates with not', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
not: { type: 'null' }
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('test').valid).toBe(true);
|
||||
expect(validator(42).valid).toBe(true);
|
||||
expect(validator(null).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('validates with const', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
const: 'specific-value'
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(validator('specific-value').valid).toBe(true);
|
||||
expect(validator('other-value').valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex real-world schemas', () => {
|
||||
it('validates user registration form', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: {
|
||||
type: 'string',
|
||||
minLength: 3,
|
||||
maxLength: 20,
|
||||
pattern: '^[a-zA-Z0-9_]+$'
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
format: 'email'
|
||||
},
|
||||
age: {
|
||||
type: 'integer',
|
||||
minimum: 18,
|
||||
maximum: 120
|
||||
},
|
||||
newsletter: {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: ['username', 'email']
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
username: 'john_doe',
|
||||
email: 'john@example.com',
|
||||
age: 25,
|
||||
newsletter: true
|
||||
}).valid
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
username: 'john_doe',
|
||||
email: 'john@example.com'
|
||||
}).valid
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
username: 'ab', // Too short
|
||||
email: 'john@example.com'
|
||||
}).valid
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
username: 'john_doe',
|
||||
email: 'invalid-email'
|
||||
}).valid
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('validates API response with nested structure', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['success', 'error', 'pending']
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
quantity: { type: 'integer', minimum: 1 }
|
||||
},
|
||||
required: ['name', 'quantity']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['id', 'items']
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
}
|
||||
},
|
||||
required: ['status', 'data']
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
status: 'success',
|
||||
data: {
|
||||
id: '123',
|
||||
items: [
|
||||
{ name: 'Item 1', quantity: 5 },
|
||||
{ name: 'Item 2', quantity: 3 }
|
||||
]
|
||||
},
|
||||
timestamp: '2025-10-17T12:00:00Z'
|
||||
}).valid
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
validator({
|
||||
status: 'invalid-status',
|
||||
data: { id: '123', items: [] }
|
||||
}).valid
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error messages', () => {
|
||||
it('provides helpful error message on validation failure', () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' }
|
||||
},
|
||||
required: ['name']
|
||||
};
|
||||
const validator = provider.getValidator(schema);
|
||||
|
||||
const result = validator({});
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errorMessage).toBeDefined();
|
||||
expect(result.errorMessage).toBeTruthy();
|
||||
expect(typeof result.errorMessage).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Missing dependencies', () => {
|
||||
describe('AJV not installed but CfWorker is', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock('ajv');
|
||||
vi.doUnmock('ajv-formats');
|
||||
});
|
||||
|
||||
it('should throw error when trying to import ajv-provider without ajv', async () => {
|
||||
// Mock ajv as not installed
|
||||
vi.doMock('ajv', () => {
|
||||
throw new Error("Cannot find module 'ajv'");
|
||||
});
|
||||
|
||||
vi.doMock('ajv-formats', () => {
|
||||
throw new Error("Cannot find module 'ajv-formats'");
|
||||
});
|
||||
|
||||
// Attempting to import ajv-provider should fail
|
||||
await expect(import('../../src/validators/ajvProvider.js')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should be able to import cfWorkerProvider when ajv is missing', async () => {
|
||||
// Mock ajv as not installed
|
||||
vi.doMock('ajv', () => {
|
||||
throw new Error("Cannot find module 'ajv'");
|
||||
});
|
||||
|
||||
vi.doMock('ajv-formats', () => {
|
||||
throw new Error("Cannot find module 'ajv-formats'");
|
||||
});
|
||||
|
||||
// But cfWorkerProvider should import successfully
|
||||
const cfworkerModule = await import('../../src/validators/cfWorkerProvider.js');
|
||||
expect(cfworkerModule.CfWorkerJsonSchemaValidator).toBeDefined();
|
||||
|
||||
// And should work correctly
|
||||
const validator = new cfworkerModule.CfWorkerJsonSchemaValidator();
|
||||
const schema: JsonSchemaType = { type: 'string' };
|
||||
const validatorFn = validator.getValidator(schema);
|
||||
expect(validatorFn('test').valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CfWorker not installed but AJV is', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock('@cfworker/json-schema');
|
||||
});
|
||||
|
||||
it('should throw error when trying to import cfWorkerProvider without @cfworker/json-schema', async () => {
|
||||
// Mock @cfworker/json-schema as not installed
|
||||
vi.doMock('@cfworker/json-schema', () => {
|
||||
throw new Error("Cannot find module '@cfworker/json-schema'");
|
||||
});
|
||||
|
||||
// Attempting to import cfWorkerProvider should fail
|
||||
await expect(import('../../src/validators/cfWorkerProvider.js')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should be able to import ajv-provider when @cfworker/json-schema is missing', async () => {
|
||||
// Mock @cfworker/json-schema as not installed
|
||||
vi.doMock('@cfworker/json-schema', () => {
|
||||
throw new Error("Cannot find module '@cfworker/json-schema'");
|
||||
});
|
||||
|
||||
// But ajv-provider should import successfully
|
||||
const ajvModule = await import('../../src/validators/ajvProvider.js');
|
||||
expect(ajvModule.AjvJsonSchemaValidator).toBeDefined();
|
||||
|
||||
// And should work correctly
|
||||
const validator = new ajvModule.AjvJsonSchemaValidator();
|
||||
const schema: JsonSchemaType = { type: 'string' };
|
||||
const validatorFn = validator.getValidator(schema);
|
||||
expect(validatorFn('test').valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should document that @cfworker/json-schema is required', () => {
|
||||
const cfworkerProviderPath = path.join(__dirname, '../../src/validators/cfWorkerProvider.ts');
|
||||
const content = readFileSync(cfworkerProviderPath, 'utf8');
|
||||
|
||||
expect(content).toContain('@cfworker/json-schema');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue