@openfloor/protocol
Version:
Open Floor Protocol implementation for JavaScript/TypeScript - enables interoperable multi-agent conversations
276 lines • 9.57 kB
JavaScript
/**
* @fileoverview JSON Schema validation for Open Floor Protocol
* Provides validation against the official Open Floor schemas
* @author Open Voice Interoperability Initiative
* @version 0.0.1
* @license Apache-2.0
*/
import conversationEnvelopeSchema from '../schemas/conversation-envelope/1.0.0/conversation-envelope-schema.json';
import dialogEventSchema from '../schemas/dialog-event/1.0.2/dialog-event-schema.json';
import assistantManifestSchema from '../schemas/assistant-manifest/1.0.0/assistant-manifest-schema.json';
/**
* Simple JSON Schema validator implementation
* Provides basic validation without external dependencies
*/
class SimpleValidator {
/**
* Validate data against a JSON schema
* @param data - Data to validate
* @param schema - JSON schema to validate against
* @returns Validation result
*/
static validate(data, schema) {
const errors = [];
try {
this._validateObject(data, schema, '', errors);
}
catch (error) {
errors.push(`Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Recursively validate an object against a schema
*/
static _validateObject(data, schema, path, errors) {
// Type validation
if (schema.type) {
if (schema.type === 'array') {
if (!Array.isArray(data)) {
errors.push(`${path || 'root'}: expected array, got ${typeof data}`);
return;
}
}
else if (typeof data !== schema.type) {
if (!(schema.type === 'object' && data && typeof data === 'object')) {
errors.push(`${path || 'root'}: expected ${schema.type}, got ${typeof data}`);
return;
}
}
}
// Required properties validation
if (schema.required && Array.isArray(schema.required) && data && typeof data === 'object') {
const obj = data;
for (const requiredProp of schema.required) {
if (!(requiredProp in obj) || obj[requiredProp] === undefined) {
errors.push(`${path || 'root'}: missing required property '${requiredProp}'`);
}
}
}
// Properties validation
if (schema.properties && data && typeof data === 'object') {
const obj = data;
for (const [propName, propSchema] of Object.entries(schema.properties)) {
if (propName in obj) {
const propPath = path ? `${path}.${propName}` : propName;
this._validateObject(obj[propName], propSchema, propPath, errors);
}
}
}
// Pattern properties validation
if (schema.patternProperties && data && typeof data === 'object') {
const obj = data;
for (const [pattern, propSchema] of Object.entries(schema.patternProperties)) {
const regex = new RegExp(pattern);
for (const [propName, propValue] of Object.entries(obj)) {
if (regex.test(propName)) {
const propPath = path ? `${path}.${propName}` : propName;
this._validateObject(propValue, propSchema, propPath, errors);
}
}
}
}
// Array validation
if (schema.items && Array.isArray(data)) {
data.forEach((item, index) => {
const itemPath = `${path}[${index}]`;
this._validateObject(item, schema.items, itemPath, errors);
});
}
// Enum validation
if (schema.enum && Array.isArray(schema.enum)) {
if (!schema.enum.includes(data)) {
errors.push(`${path || 'root'}: value must be one of [${schema.enum.join(', ')}], got ${JSON.stringify(data)}`);
}
}
// Number range validation
if (typeof data === 'number') {
if (typeof schema.minimum === 'number' && data < schema.minimum) {
errors.push(`${path || 'root'}: value ${data} is below minimum ${schema.minimum}`);
}
if (typeof schema.maximum === 'number' && data > schema.maximum) {
errors.push(`${path || 'root'}: value ${data} is above maximum ${schema.maximum}`);
}
}
// String pattern validation
if (typeof data === 'string' && schema.pattern) {
const regex = new RegExp(schema.pattern);
if (!regex.test(data)) {
errors.push(`${path || 'root'}: string does not match pattern ${schema.pattern}`);
}
}
// AnyOf validation
if (schema.anyOf && Array.isArray(schema.anyOf)) {
const anyOfErrors = [];
let valid = false;
for (const subSchema of schema.anyOf) {
const subErrors = [];
this._validateObject(data, subSchema, path, subErrors);
if (subErrors.length === 0) {
valid = true;
break;
}
anyOfErrors.push(...subErrors);
}
if (!valid) {
errors.push(`${path || 'root'}: value does not match any of the allowed schemas`);
}
}
}
}
/**
* Validates a dialog event object against the Dialog Event schema
* @param data - Dialog event data to validate
* @returns Validation result
*
* @example
* ```typescript
* const dialogEvent = {
* id: 'de:123',
* speakerUri: 'tag:example.com,2025:user1',
* span: { startTime: '2025-01-01T00:00:00Z' },
* features: {
* text: {
* mimeType: 'text/plain',
* tokens: [{ value: 'Hello' }]
* }
* }
* };
*
* const result = validateDialogEvent(dialogEvent);
* console.log(result.valid); // true or false
* ```
*/
export function validateDialogEvent(data) {
return SimpleValidator.validate(data, dialogEventSchema);
}
/**
* Validates an assistant manifest object against the Assistant Manifest schema
* @param data - Manifest data to validate
* @returns Validation result
*
* @example
* ```typescript
* const manifest = {
* identification: {
* speakerUri: 'tag:example.com,2025:bot1',
* serviceUrl: 'https://example.com/bot',
* organization: 'Example Corp',
* conversationalName: 'Bot',
* synopsis: 'A helpful assistant'
* },
* capabilities: []
* };
*
* const result = validateManifest(manifest);
* console.log(result.valid); // true or false
* ```
*/
export function validateManifest(data) {
return SimpleValidator.validate(data, assistantManifestSchema);
}
/**
* Validates a conversation envelope object against the Conversation Envelope schema
* @param data - Envelope data to validate
* @returns Validation result
*
* @example
* ```typescript
* const envelope = {
* openFloor: {
* schema: { version: '1.0.0' },
* conversation: { id: 'conv:123' },
* sender: { speakerUri: 'tag:example.com,2025:agent1' },
* events: []
* }
* };
*
* const result = validateEnvelope(envelope);
* console.log(result.valid); // true or false
* ```
*/
export function validateEnvelope(data) {
return SimpleValidator.validate(data, conversationEnvelopeSchema);
}
/**
* Enhanced validator that uses external AJV library if available
* Falls back to simple validator if AJV is not available
*/
export class EnhancedValidator {
static _ajv = null;
static _initialized = false;
/**
* Initialize the validator (attempts to load AJV)
*/
static _initialize() {
if (this._initialized)
return;
try {
const Ajv = require('ajv');
this._ajv = new Ajv({
allErrors: true,
verbose: true,
strict: false
});
}
catch {
this._ajv = null;
}
this._initialized = true;
}
/**
* Validate data against schema using AJV if available, otherwise simple validator
* @param data - Data to validate
* @param schema - JSON schema
* @returns Validation result
*/
static validate(data, schema) {
this._initialize();
if (this._ajv) {
try {
const validate = this._ajv.compile(schema);
const valid = validate(data);
return {
valid,
errors: valid ? [] : (validate.errors || []).map((err) => `${err.instancePath || 'root'}: ${err.message}`)
};
}
catch (error) {
return SimpleValidator.validate(data, schema);
}
}
return SimpleValidator.validate(data, schema);
}
}
/**
* Validates dialog event using enhanced validator
*/
export function validateDialogEventEnhanced(data) {
return EnhancedValidator.validate(data, dialogEventSchema);
}
/**
* Validates manifest using enhanced validator
*/
export function validateManifestEnhanced(data) {
return EnhancedValidator.validate(data, assistantManifestSchema);
}
/**
* Validates envelope using enhanced validator
*/
export function validateEnvelopeEnhanced(data) {
return EnhancedValidator.validate(data, conversationEnvelopeSchema);
}
//# sourceMappingURL=validation.js.map