import { walk } from "@std/fs/walk"; import { extname } from "@std/path"; import { green, red, dim, cyan } from "@std/fmt/colors"; import { type LexiconDoc, validate } from "@slices/lexicon"; import { logger } from "./logger.ts"; // Type for raw lexicon content that may include unknown fields type RawLexicon = LexiconDoc & Record; export interface LexiconFile { path: string; content: unknown; valid: boolean; errors?: string[]; } export interface LexiconValidationResult { files: LexiconFile[]; totalFiles: number; validFiles: number; invalidFiles: number; } export async function findLexiconFiles(directory: string): Promise { const lexiconFiles: string[] = []; try { for await (const entry of walk(directory)) { if (entry.isFile && extname(entry.path) === ".json") { lexiconFiles.push(entry.path); } } } catch (error) { if (error instanceof Deno.errors.NotFound) { throw new Error(`Directory not found: ${directory}`); } throw error; } return lexiconFiles; } export async function readAndParseLexicon(filePath: string): Promise { try { const content = await Deno.readTextFile(filePath); return JSON.parse(content); } catch (error) { if (error instanceof Deno.errors.NotFound) { throw new Error(`File not found: ${filePath}`); } if (error instanceof SyntaxError) { throw new Error(`Invalid JSON in file: ${filePath} - ${error.message}`); } throw error; } } export async function validateLexicon(lexicon: unknown): Promise<{ valid: boolean; errors?: string[]; }> { try { // Basic structure validation if (!lexicon || typeof lexicon !== "object") { return { valid: false, errors: ["Lexicon must be an object"] }; } const lex = lexicon as Record; // Check required fields if (!lex.id || typeof lex.id !== "string") { return { valid: false, errors: ["Lexicon must have a valid 'id' field"] }; } // Check for either 'definitions' or 'defs' (convert if needed) const defs = lex.defs || lex.definitions; if (!defs || typeof defs !== "object") { return { valid: false, errors: ["Lexicon must have a 'defs' or 'definitions' object"], }; } // Use the new validate function const validationResult = await validate([lexicon as RawLexicon]); // validate returns null if validation succeeds, or error map if validation fails if (validationResult === null) { return { valid: true }; } else { // Extract errors for this specific lexicon const lexiconId = lex.id as string; const errors = validationResult[lexiconId] || [`Unknown validation error for lexicon: ${lexiconId}`]; return { valid: false, errors }; } } catch (error) { const err = error as Error; return { valid: false, errors: [`Validation error: ${err.message}`] }; } } export async function validateLexiconFiles( filePaths: string[], showProgress = true ): Promise { const files: LexiconFile[] = []; const lexicons: RawLexicon[] = []; // Read and parse all files for (let i = 0; i < filePaths.length; i++) { const filePath = filePaths[i]; if (showProgress) { logger.progress("Reading lexicons", i + 1, filePaths.length); } try { const content = await readAndParseLexicon(filePath); // Basic structure validation if (!content || typeof content !== "object") { files.push({ path: filePath, content: null, valid: false, errors: ["Lexicon must be an object"], }); continue; } const lex = content as Record; // Check required fields if (!lex.id || typeof lex.id !== "string") { files.push({ path: filePath, content, valid: false, errors: ["Lexicon must have a valid 'id' field"], }); continue; } // Check for either 'definitions' or 'defs' const defs = lex.defs || lex.definitions; if (!defs || typeof defs !== "object") { files.push({ path: filePath, content, valid: false, errors: ["Lexicon must have a 'defs' or 'definitions' object"], }); continue; } // Store for validation lexicons.push(content as RawLexicon); files.push({ path: filePath, content, valid: true, errors: [], }); } catch (error) { const err = error as Error; files.push({ path: filePath, content: null, valid: false, errors: [err.message], }); } } // Validate all lexicons together using the new validation system if (lexicons.length > 0) { try { const validationErrors = await validate(lexicons); // If validation errors exist, map them back to files if (validationErrors) { for (const file of files) { if (file.valid && file.content) { const lexicon = file.content as RawLexicon; const errors = validationErrors[lexicon.id]; if (errors && errors.length > 0) { file.valid = false; file.errors = errors; } } } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Mark all valid files as invalid due to validation failure for (const file of files) { if (file.valid) { file.valid = false; file.errors = [`Validation failed: ${errorMessage}`]; } } } } const validFiles = files.filter((f) => f.valid).length; const invalidFiles = files.length - validFiles; return { files, totalFiles: filePaths.length, validFiles, invalidFiles, }; } function colorizeErrorPaths(errorMessage: string): string { // Highlight field paths in quotes with cyan color return errorMessage.replace( /'([^']+)'/g, (_match, p1) => cyan(`'${p1}'`) ); } function formatError(error: string, index: number): string { return ` ${red(`${index + 1}.`)} ${colorizeErrorPaths(error)}`; } export function printValidationSummary(result: LexiconValidationResult): void { logger.section("Validation Summary"); logger.result(`Total files: ${result.totalFiles}`); logger.result(`Valid: ${green(result.validFiles.toString())}`); logger.result(`Invalid: ${red(result.invalidFiles.toString())}`); if (result.invalidFiles > 0) { logger.section("Invalid Files"); for (const file of result.files) { if (!file.valid) { console.log(` ${dim(file.path)}`); if (file.errors) { file.errors.forEach((error, index) => { console.log(formatError(error, index)); }); } } } } }