mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-05-03 03:52:31 +02:00
217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
import Ajv, { ErrorObject } from 'ajv';
|
|
import { ContentType } from '../models';
|
|
import { ContentTypeSchemaGenerator, JSONSchema } from './ContentTypeSchemaGenerator';
|
|
|
|
/**
|
|
* Validation error with location information
|
|
*/
|
|
export interface ValidationError {
|
|
field: string;
|
|
message: string;
|
|
keyword?: string;
|
|
params?: Record<string, any>;
|
|
}
|
|
|
|
/**
|
|
* Validates front matter data against content type schemas
|
|
*
|
|
* This validator uses JSON Schema validation (via AJV) to ensure that front matter
|
|
* in markdown files conforms to the structure defined in content types.
|
|
*
|
|
* Features:
|
|
* - Automatic schema generation from content type definitions
|
|
* - Type validation (string, number, boolean, datetime, arrays, etc.)
|
|
* - Required field validation
|
|
* - Enum/choice validation
|
|
* - Number range validation (min/max)
|
|
* - Nested object validation
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* const validator = new FrontMatterValidator();
|
|
* const errors = validator.validate(frontMatterData, contentType);
|
|
* if (errors.length > 0) {
|
|
* // Handle validation errors
|
|
* }
|
|
* ```
|
|
*/
|
|
export class FrontMatterValidator {
|
|
private ajv: Ajv;
|
|
private schemaCache: Map<string, JSONSchema>;
|
|
|
|
constructor() {
|
|
this.ajv = new Ajv({
|
|
allErrors: true,
|
|
verbose: true,
|
|
strict: false,
|
|
allowUnionTypes: true
|
|
});
|
|
this.schemaCache = new Map();
|
|
}
|
|
|
|
/**
|
|
* Validate front matter data against a content type
|
|
* @param data The front matter data to validate
|
|
* @param contentType The content type to validate against
|
|
* @returns Array of validation errors (empty if valid)
|
|
*/
|
|
public async validate(data: any, contentType: ContentType): Promise<ValidationError[]> {
|
|
if (!contentType || !contentType.fields || contentType.fields.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Get or generate schema
|
|
const schema = await this.getSchema(contentType);
|
|
if (!schema) {
|
|
return [];
|
|
}
|
|
|
|
// Compile and validate
|
|
const validate = this.ajv.compile(schema);
|
|
const valid = validate(data);
|
|
|
|
if (valid) {
|
|
return [];
|
|
}
|
|
|
|
// Convert AJV errors to our format
|
|
return this.convertAjvErrors(validate.errors || []);
|
|
}
|
|
|
|
/**
|
|
* Get or generate schema for a content type
|
|
* @param contentType The content type
|
|
* @returns JSON Schema
|
|
*/
|
|
private async getSchema(contentType: ContentType): Promise<JSONSchema | null> {
|
|
// Check cache first
|
|
const cacheKey = contentType.name;
|
|
if (this.schemaCache.has(cacheKey)) {
|
|
return this.schemaCache.get(cacheKey) || null;
|
|
}
|
|
|
|
// Generate new schema
|
|
const schema = await ContentTypeSchemaGenerator.generateSchema(contentType);
|
|
this.schemaCache.set(cacheKey, schema);
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Clear the schema cache
|
|
*/
|
|
public clearCache(): void {
|
|
this.schemaCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Convert AJV errors to validation errors
|
|
* @param ajvErrors AJV error objects
|
|
* @returns Array of validation errors
|
|
*/
|
|
private convertAjvErrors(ajvErrors: ErrorObject[]): ValidationError[] {
|
|
const errors: ValidationError[] = [];
|
|
|
|
for (const error of ajvErrors) {
|
|
const field = this.extractFieldName(error.instancePath);
|
|
const message = this.formatErrorMessage(error, field);
|
|
|
|
errors.push({
|
|
field,
|
|
message,
|
|
keyword: error.keyword,
|
|
params: error.params
|
|
});
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Extract field name from instance path
|
|
* @param instancePath The JSON pointer path
|
|
* @returns Field name
|
|
*/
|
|
private extractFieldName(instancePath: string): string {
|
|
if (!instancePath || instancePath === '') {
|
|
return 'root';
|
|
}
|
|
|
|
// Remove leading slash and convert to dot notation
|
|
return instancePath
|
|
.replace(/^\//, '')
|
|
.replace(/\//g, '.')
|
|
.replace(/~1/g, '/')
|
|
.replace(/~0/g, '~');
|
|
}
|
|
|
|
/**
|
|
* Format error message for display
|
|
* @param error AJV error object
|
|
* @param field Field name
|
|
* @returns Formatted error message
|
|
*/
|
|
private formatErrorMessage(error: ErrorObject, field: string): string {
|
|
const displayField = field === 'root' ? 'The document' : `Field '${field}'`;
|
|
|
|
switch (error.keyword) {
|
|
case 'required': {
|
|
const missingProperty = error.params?.missingProperty;
|
|
return `Missing required field '${missingProperty}'`;
|
|
}
|
|
|
|
case 'type': {
|
|
const expectedType = error.params?.type;
|
|
return `${displayField} must be of type ${expectedType}`;
|
|
}
|
|
|
|
case 'enum': {
|
|
const allowedValues = error.params?.allowedValues;
|
|
if (allowedValues && Array.isArray(allowedValues)) {
|
|
return `${displayField} must be one of: ${allowedValues.join(', ')}`;
|
|
}
|
|
return `${displayField} has an invalid value`;
|
|
}
|
|
|
|
case 'format': {
|
|
const format = error.params?.format;
|
|
return `${displayField} must be in ${format} format`;
|
|
}
|
|
|
|
case 'minimum': {
|
|
const minimum = error.params?.limit;
|
|
return `${displayField} must be greater than or equal to ${minimum}`;
|
|
}
|
|
|
|
case 'maximum': {
|
|
const maximum = error.params?.limit;
|
|
return `${displayField} must be less than or equal to ${maximum}`;
|
|
}
|
|
|
|
case 'minItems': {
|
|
const minItems = error.params?.limit;
|
|
return `${displayField} must have at least ${minItems} items`;
|
|
}
|
|
|
|
case 'maxItems': {
|
|
const maxItems = error.params?.limit;
|
|
return `${displayField} must have at most ${maxItems} items`;
|
|
}
|
|
|
|
case 'additionalProperties': {
|
|
const additionalProperty = error.params?.additionalProperty;
|
|
return `Unexpected field '${additionalProperty}' is not allowed`;
|
|
}
|
|
|
|
case 'oneOf':
|
|
return `${displayField} must match exactly one of the allowed schemas`;
|
|
|
|
case 'anyOf':
|
|
return `${displayField} must match at least one of the allowed schemas`;
|
|
|
|
default:
|
|
return error.message || `${displayField} is invalid`;
|
|
}
|
|
}
|
|
}
|