/**
* Validates ZPT navigation parameters (zoom/pan/tilt)
*/
export default class ParameterValidator {
constructor() {
this.schemas = this.initializeSchemas();
}
/**
* Initialize parameter schemas based on ZPT specification
*/
initializeSchemas() {
return {
zoom: {
type: 'enum',
values: ['entity', 'unit', 'text', 'community', 'corpus'],
required: true
},
pan: {
type: 'object',
required: false,
properties: {
topic: {
type: 'string',
pattern: /^[a-zA-Z0-9\-_]+$/
},
entity: {
type: 'array',
items: { type: 'string' }
},
temporal: {
type: 'object',
properties: {
start: { type: 'date' },
end: { type: 'date' }
}
},
geographic: {
type: 'object',
properties: {
bbox: {
type: 'array',
minItems: 4,
maxItems: 4
},
center: {
type: 'object',
properties: {
lat: { type: 'number', min: -90, max: 90 },
lon: { type: 'number', min: -180, max: 180 }
}
}
}
}
}
},
tilt: {
type: 'enum',
values: ['embedding', 'keywords', 'graph', 'temporal'],
required: true
},
transform: {
type: 'object',
required: false,
properties: {
maxTokens: {
type: 'number',
default: 4000,
min: 100,
max: 128000
},
format: {
type: 'enum',
values: ['json', 'markdown', 'structured'],
default: 'structured'
},
tokenizer: {
type: 'string',
default: 'cl100k_base'
},
includeMetadata: {
type: 'boolean',
default: true
},
chunkStrategy: {
type: 'enum',
values: ['semantic', 'fixed', 'adaptive'],
default: 'semantic'
}
}
}
};
}
/**
* Validate complete ZPT parameter object
* @param {Object} params - Navigation parameters
* @returns {Object} Validation result
*/
validate(params) {
if (!params || typeof params !== 'object') {
return {
valid: false,
message: 'Parameters must be an object',
errors: []
};
}
const errors = [];
const warnings = [];
// Validate zoom parameter
const zoomResult = this.validateZoom(params.zoom);
if (!zoomResult.valid) {
errors.push({ field: 'zoom', ...zoomResult });
}
// Validate tilt parameter
const tiltResult = this.validateTilt(params.tilt);
if (!tiltResult.valid) {
errors.push({ field: 'tilt', ...tiltResult });
}
// Validate optional pan parameter
if (params.pan !== undefined) {
const panResult = this.validatePan(params.pan);
if (!panResult.valid) {
errors.push({ field: 'pan', ...panResult });
}
}
// Validate optional transform parameter
if (params.transform !== undefined) {
const transformResult = this.validateTransform(params.transform);
if (!transformResult.valid) {
errors.push({ field: 'transform', ...transformResult });
}
warnings.push(...(transformResult.warnings || []));
}
// Check for unknown parameters
const knownParams = ['zoom', 'pan', 'tilt', 'transform'];
const unknownParams = Object.keys(params).filter(key => !knownParams.includes(key));
if (unknownParams.length > 0) {
warnings.push({
field: 'unknown',
message: `Unknown parameters: ${unknownParams.join(', ')}`
});
}
return {
valid: errors.length === 0,
errors,
warnings,
message: errors.length > 0 ? `Validation failed: ${errors[0].message}` : null
};
}
/**
* Validate zoom parameter
*/
validateZoom(zoom) {
const schema = this.schemas.zoom;
if (zoom === undefined || zoom === null) {
return {
valid: false,
message: 'Zoom parameter is required'
};
}
if (!schema.values.includes(zoom)) {
return {
valid: false,
message: `Invalid zoom level. Must be one of: ${schema.values.join(', ')}`
};
}
return { valid: true };
}
/**
* Validate tilt parameter
*/
validateTilt(tilt) {
const schema = this.schemas.tilt;
if (tilt === undefined || tilt === null) {
return {
valid: false,
message: 'Tilt parameter is required'
};
}
if (!schema.values.includes(tilt)) {
return {
valid: false,
message: `Invalid tilt value. Must be one of: ${schema.values.join(', ')}`
};
}
return { valid: true };
}
/**
* Validate pan parameter
*/
validatePan(pan) {
if (!pan || typeof pan !== 'object') {
return {
valid: false,
message: 'Pan parameter must be an object'
};
}
const errors = [];
// Validate topic filter
if (pan.topic !== undefined) {
if (typeof pan.topic !== 'string' || pan.topic.length === 0) {
errors.push('Topic filter must be a non-empty string');
} else if (!this.schemas.pan.properties.topic.pattern.test(pan.topic)) {
errors.push('Topic filter contains invalid characters');
}
}
// Validate entity filter
if (pan.entity !== undefined) {
if (!Array.isArray(pan.entity)) {
errors.push('Entity filter must be an array');
} else if (pan.entity.some(e => typeof e !== 'string')) {
errors.push('All entity filter values must be strings');
}
}
// Validate temporal filter
if (pan.temporal !== undefined) {
const temporalResult = this.validateTemporal(pan.temporal);
if (!temporalResult.valid) {
errors.push(temporalResult.message);
}
}
// Validate geographic filter
if (pan.geographic !== undefined) {
const geoResult = this.validateGeographic(pan.geographic);
if (!geoResult.valid) {
errors.push(geoResult.message);
}
}
return {
valid: errors.length === 0,
message: errors.length > 0 ? errors[0] : null,
errors
};
}
/**
* Validate temporal filter
*/
validateTemporal(temporal) {
if (!temporal || typeof temporal !== 'object') {
return {
valid: false,
message: 'Temporal filter must be an object'
};
}
const { start, end } = temporal;
if (start) {
const startDate = new Date(start);
if (isNaN(startDate.getTime())) {
return {
valid: false,
message: 'Invalid start date format'
};
}
}
if (end) {
const endDate = new Date(end);
if (isNaN(endDate.getTime())) {
return {
valid: false,
message: 'Invalid end date format'
};
}
}
if (start && end) {
const startDate = new Date(start);
const endDate = new Date(end);
if (startDate > endDate) {
return {
valid: false,
message: 'Start date must be before end date'
};
}
}
return { valid: true };
}
/**
* Validate geographic filter
*/
validateGeographic(geographic) {
if (!geographic || typeof geographic !== 'object') {
return {
valid: false,
message: 'Geographic filter must be an object'
};
}
if (geographic.bbox) {
if (!Array.isArray(geographic.bbox) || geographic.bbox.length !== 4) {
return {
valid: false,
message: 'Bounding box must be an array of 4 numbers [minLon, minLat, maxLon, maxLat]'
};
}
const [minLon, minLat, maxLon, maxLat] = geographic.bbox;
if (![minLon, minLat, maxLon, maxLat].every(n => typeof n === 'number')) {
return {
valid: false,
message: 'All bounding box values must be numbers'
};
}
if (minLon >= maxLon || minLat >= maxLat) {
return {
valid: false,
message: 'Invalid bounding box coordinates'
};
}
}
if (geographic.center) {
const { lat, lon } = geographic.center;
if (typeof lat !== 'number' || typeof lon !== 'number') {
return {
valid: false,
message: 'Center coordinates must be numbers'
};
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
return {
valid: false,
message: 'Invalid center coordinates'
};
}
}
return { valid: true };
}
/**
* Validate transform parameter
*/
validateTransform(transform) {
if (!transform || typeof transform !== 'object') {
return {
valid: false,
message: 'Transform parameter must be an object'
};
}
const errors = [];
const warnings = [];
const schema = this.schemas.transform.properties;
// Validate maxTokens
if (transform.maxTokens !== undefined) {
if (typeof transform.maxTokens !== 'number' || transform.maxTokens <= 0) {
errors.push('maxTokens must be a positive number');
} else if (transform.maxTokens < schema.maxTokens.min) {
errors.push(`maxTokens must be at least ${schema.maxTokens.min}`);
} else if (transform.maxTokens > schema.maxTokens.max) {
warnings.push(`maxTokens exceeds recommended maximum of ${schema.maxTokens.max}`);
}
}
// Validate format
if (transform.format !== undefined) {
if (!schema.format.values.includes(transform.format)) {
errors.push(`Invalid format. Must be one of: ${schema.format.values.join(', ')}`);
}
}
// Validate tokenizer
if (transform.tokenizer !== undefined) {
if (typeof transform.tokenizer !== 'string') {
errors.push('Tokenizer must be a string');
}
}
// Validate includeMetadata
if (transform.includeMetadata !== undefined) {
if (typeof transform.includeMetadata !== 'boolean') {
errors.push('includeMetadata must be a boolean');
}
}
// Validate chunkStrategy
if (transform.chunkStrategy !== undefined) {
if (!schema.chunkStrategy.values.includes(transform.chunkStrategy)) {
errors.push(`Invalid chunkStrategy. Must be one of: ${schema.chunkStrategy.values.join(', ')}`);
}
}
return {
valid: errors.length === 0,
message: errors.length > 0 ? errors[0] : null,
errors,
warnings
};
}
/**
* Get parameter schema for documentation
*/
getSchema() {
return this.schemas;
}
/**
* Get default values for optional parameters
*/
getDefaults() {
return {
transform: {
maxTokens: this.schemas.transform.properties.maxTokens.default,
format: this.schemas.transform.properties.format.default,
tokenizer: this.schemas.transform.properties.tokenizer.default,
includeMetadata: this.schemas.transform.properties.includeMetadata.default,
chunkStrategy: this.schemas.transform.properties.chunkStrategy.default
}
};
}
}