meld
Version:
Meld: A template language for LLM prompts
165 lines (146 loc) • 6.49 kB
text/typescript
import type { DirectiveNode } from 'meld-spec';
import { validationLogger as logger } from '@core/utils/logger.js';
import { IValidationService } from './IValidationService.js';
import { MeldDirectiveError } from '@core/errors/MeldDirectiveError.js';
import { DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js';
import { ErrorSeverity } from '@core/errors/MeldError.js';
// Import default validators
import { validateTextDirective } from './validators/TextDirectiveValidator.js';
import { validateDataDirective } from './validators/DataDirectiveValidator.js';
import { validateImportDirective } from './validators/ImportDirectiveValidator.js';
import { validateEmbedDirective } from './validators/EmbedDirectiveValidator.js';
import { validatePathDirective } from './validators/PathDirectiveValidator.js';
import { validateDefineDirective } from './validators/DefineDirectiveValidator.js';
import { validateRunDirective } from './validators/RunDirectiveValidator.js';
/**
* Map of directive error codes to severity levels
*/
export const ValidationErrorSeverity: Record<DirectiveErrorCode, ErrorSeverity> = {
[DirectiveErrorCode.VALIDATION_FAILED]: ErrorSeverity.Recoverable,
[DirectiveErrorCode.RESOLUTION_FAILED]: ErrorSeverity.Recoverable,
[DirectiveErrorCode.EXECUTION_FAILED]: ErrorSeverity.Recoverable,
[DirectiveErrorCode.HANDLER_NOT_FOUND]: ErrorSeverity.Fatal,
[DirectiveErrorCode.FILE_NOT_FOUND]: ErrorSeverity.Recoverable,
[DirectiveErrorCode.CIRCULAR_REFERENCE]: ErrorSeverity.Fatal,
[DirectiveErrorCode.VARIABLE_NOT_FOUND]: ErrorSeverity.Recoverable,
[DirectiveErrorCode.STATE_ERROR]: ErrorSeverity.Fatal,
[DirectiveErrorCode.INVALID_CONTEXT]: ErrorSeverity.Fatal,
[DirectiveErrorCode.SECTION_NOT_FOUND]: ErrorSeverity.Recoverable
};
export class ValidationService implements IValidationService {
private validators = new Map<string, (node: DirectiveNode) => Promise<void>>();
constructor() {
// Register default validators
this.registerValidator('text', async (node) => validateTextDirective(node));
this.registerValidator('data', async (node) => validateDataDirective(node));
this.registerValidator('import', async (node) => validateImportDirective(node));
this.registerValidator('embed', async (node) => validateEmbedDirective(node));
this.registerValidator('path', async (node) => validatePathDirective(node));
this.registerValidator('define', async (node) => validateDefineDirective(node));
this.registerValidator('run', async (node) => validateRunDirective(node));
logger.debug('ValidationService initialized with default validators', {
validators: Array.from(this.validators.keys())
});
}
/**
* Validate a directive node against its schema and constraints
* @throws {MeldDirectiveError} If validation fails
*/
async validate(node: DirectiveNode): Promise<void> {
logger.debug('Validating directive', {
kind: node.directive.kind,
location: node.location
});
const validator = this.validators.get(node.directive.kind);
if (!validator) {
throw new MeldDirectiveError(
`Unknown directive kind: ${node.directive.kind}`,
node.directive.kind,
{
location: node.location?.start,
code: DirectiveErrorCode.HANDLER_NOT_FOUND,
severity: ValidationErrorSeverity[DirectiveErrorCode.HANDLER_NOT_FOUND]
}
);
}
try {
await validator(node);
} catch (error) {
// If it's already a MeldDirectiveError, just rethrow it
if (error instanceof MeldDirectiveError) {
throw error;
}
// Determine the error code and severity
let code = DirectiveErrorCode.VALIDATION_FAILED;
let severity = ValidationErrorSeverity[code];
// Check for specific error messages to classify them
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('required') || errorMessage.includes('missing')) {
// Missing required fields are recoverable
code = DirectiveErrorCode.VALIDATION_FAILED;
severity = ValidationErrorSeverity[code];
} else if (errorMessage.includes('invalid format') || errorMessage.includes('must be')) {
// Format validation errors are recoverable
code = DirectiveErrorCode.VALIDATION_FAILED;
severity = ValidationErrorSeverity[code];
} else if (errorMessage.includes('not found') || errorMessage.includes('does not exist')) {
// Not found errors are recoverable
code = DirectiveErrorCode.FILE_NOT_FOUND;
severity = ValidationErrorSeverity[code];
} else if (errorMessage.includes('circular')) {
// Circular reference errors are fatal
code = DirectiveErrorCode.CIRCULAR_REFERENCE;
severity = ValidationErrorSeverity[code];
}
// Otherwise, wrap it in a MeldDirectiveError
throw new MeldDirectiveError(
errorMessage,
node.directive.kind,
{
location: node.location?.start,
code,
cause: error instanceof Error ? error : undefined,
severity
}
);
}
logger.debug('Directive validation successful', {
kind: node.directive.kind
});
}
/**
* Register a validator for a directive kind
*/
registerValidator(kind: string, validator: (node: DirectiveNode) => Promise<void>): void {
if (!kind || typeof kind !== 'string') {
throw new Error('Validator kind must be a non-empty string');
}
if (typeof validator !== 'function') {
throw new Error('Validator must be a function');
}
if (this.validators.has(kind)) {
logger.warn(`Overriding existing validator for directive kind: ${kind}`);
}
this.validators.set(kind, validator);
logger.debug(`Registered validator for directive kind: ${kind}`);
}
/**
* Remove a validator for a directive kind
*/
removeValidator(kind: string): void {
this.validators.delete(kind);
logger.debug(`Removed validator for directive kind: ${kind}`);
}
/**
* Check if a validator exists for a directive kind
*/
hasValidator(kind: string): boolean {
return this.validators.has(kind);
}
/**
* Get all registered directive kinds
*/
getRegisteredDirectiveKinds(): string[] {
return Array.from(this.validators.keys());
}
}