meld
Version:
Meld: A template language for LLM prompts
156 lines (142 loc) • 5.05 kB
text/typescript
import { DirectiveNode, PathDirectiveData } from 'meld-spec';
import { MeldDirectiveError, DirectiveLocation } from '@core/errors/MeldDirectiveError.js';
import { DirectiveErrorCode } from '@services/pipeline/DirectiveService/errors/DirectiveError.js';
import { ErrorSeverity } from '@core/errors/MeldError.js';
import { ResolutionContext } from '@services/resolution/ResolutionService/IResolutionService.js';
/**
* Converts AST SourceLocation to DirectiveLocation
*/
function convertLocation(location: any): DirectiveLocation {
if (!location) return { line: 0, column: 0 };
return {
line: location.line,
column: location.column
};
}
/**
* Validates path directives based on the latest meld-ast structure
* Uses AST-based validation instead of regex
*/
export async function validatePathDirective(node: DirectiveNode, context?: ResolutionContext): Promise<void> {
if (!node.directive) {
throw new MeldDirectiveError(
'Path directive is missing required fields',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Cast to PathDirectiveData to access typed properties
const directive = node.directive as PathDirectiveData;
// Fix for different field names: AST can use either 'id' or 'identifier'
const identifier = directive.identifier || (directive as any).id;
// Check for required fields
if (!identifier || typeof identifier !== 'string' || identifier.trim() === '') {
throw new MeldDirectiveError(
'Path directive requires a valid identifier',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Validate identifier format using character-by-character validation
// instead of regex
if (!isValidIdentifier(identifier)) {
throw new MeldDirectiveError(
'Path identifier must be a valid identifier (letters, numbers, underscore, starting with letter/underscore)',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Handle both direct string value and path object
let pathObject = directive.path;
let pathRaw: string | undefined;
if (!pathObject) {
// If path is missing, check for value property as fallback
if (directive.value) {
pathRaw = typeof directive.value === 'string'
? directive.value
: (directive.value as any).raw || '';
} else {
throw new MeldDirectiveError(
'Path directive requires a path value',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
} else if (typeof pathObject === 'string') {
// Handle direct string path
pathRaw = pathObject;
} else if (typeof pathObject === 'object') {
// Handle path object with raw property
if (!pathObject.raw || typeof pathObject.raw !== 'string' || pathObject.raw.trim() === '') {
throw new MeldDirectiveError(
'Path directive requires a non-empty path value',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
pathRaw = pathObject.raw;
} else {
throw new MeldDirectiveError(
'Path directive requires a valid path',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Ensure we have a non-empty path
if (!pathRaw || pathRaw.trim() === '') {
throw new MeldDirectiveError(
'Path directive requires a non-empty path value',
'path',
{
location: convertLocation(node.location?.start),
code: DirectiveErrorCode.VALIDATION_FAILED,
severity: ErrorSeverity.Fatal
}
);
}
// Path validation (absolute paths, path segments) is handled by ParserService
}
/**
* Helper function to validate identifier format without regex
*/
function isValidIdentifier(str: string): boolean {
if (!str || str.length === 0) return false;
// First character must be letter or underscore
const firstChar = str.charAt(0);
if (!(firstChar === '_' || (firstChar >= 'a' && firstChar <= 'z') || (firstChar >= 'A' && firstChar <= 'Z'))) {
return false;
}
// Rest of characters must be letters, numbers, or underscore
for (let i = 1; i < str.length; i++) {
const char = str.charAt(i);
if (!((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char === '_')) {
return false;
}
}
return true;
}