Highly ambitious ATProtocol AppView service and sdks
at main 248 lines 6.9 kB view raw
1import { walk } from "@std/fs/walk"; 2import { extname } from "@std/path"; 3import { green, red, dim, cyan } from "@std/fmt/colors"; 4import { type LexiconDoc, validate } from "@slices/lexicon"; 5import { logger } from "./logger.ts"; 6 7// Type for raw lexicon content that may include unknown fields 8type RawLexicon = LexiconDoc & Record<string, unknown>; 9 10export interface LexiconFile { 11 path: string; 12 content: unknown; 13 valid: boolean; 14 errors?: string[]; 15} 16 17export interface LexiconValidationResult { 18 files: LexiconFile[]; 19 totalFiles: number; 20 validFiles: number; 21 invalidFiles: number; 22} 23 24export async function findLexiconFiles(directory: string): Promise<string[]> { 25 const lexiconFiles: string[] = []; 26 27 try { 28 for await (const entry of walk(directory)) { 29 if (entry.isFile && extname(entry.path) === ".json") { 30 lexiconFiles.push(entry.path); 31 } 32 } 33 } catch (error) { 34 if (error instanceof Deno.errors.NotFound) { 35 throw new Error(`Directory not found: ${directory}`); 36 } 37 throw error; 38 } 39 40 return lexiconFiles; 41} 42 43export async function readAndParseLexicon(filePath: string): Promise<unknown> { 44 try { 45 const content = await Deno.readTextFile(filePath); 46 return JSON.parse(content); 47 } catch (error) { 48 if (error instanceof Deno.errors.NotFound) { 49 throw new Error(`File not found: ${filePath}`); 50 } 51 if (error instanceof SyntaxError) { 52 throw new Error(`Invalid JSON in file: ${filePath} - ${error.message}`); 53 } 54 throw error; 55 } 56} 57 58export async function validateLexicon(lexicon: unknown): Promise<{ 59 valid: boolean; 60 errors?: string[]; 61}> { 62 try { 63 // Basic structure validation 64 if (!lexicon || typeof lexicon !== "object") { 65 return { valid: false, errors: ["Lexicon must be an object"] }; 66 } 67 68 const lex = lexicon as Record<string, unknown>; 69 70 // Check required fields 71 if (!lex.id || typeof lex.id !== "string") { 72 return { valid: false, errors: ["Lexicon must have a valid 'id' field"] }; 73 } 74 75 // Check for either 'definitions' or 'defs' (convert if needed) 76 const defs = lex.defs || lex.definitions; 77 if (!defs || typeof defs !== "object") { 78 return { 79 valid: false, 80 errors: ["Lexicon must have a 'defs' or 'definitions' object"], 81 }; 82 } 83 84 // Use the new validate function 85 const validationResult = await validate([lexicon as RawLexicon]); 86 87 // validate returns null if validation succeeds, or error map if validation fails 88 if (validationResult === null) { 89 return { valid: true }; 90 } else { 91 // Extract errors for this specific lexicon 92 const lexiconId = lex.id as string; 93 const errors = validationResult[lexiconId] || [`Unknown validation error for lexicon: ${lexiconId}`]; 94 return { valid: false, errors }; 95 } 96 } catch (error) { 97 const err = error as Error; 98 return { valid: false, errors: [`Validation error: ${err.message}`] }; 99 } 100} 101 102export async function validateLexiconFiles( 103 filePaths: string[], 104 showProgress = true 105): Promise<LexiconValidationResult> { 106 const files: LexiconFile[] = []; 107 const lexicons: RawLexicon[] = []; 108 109 // Read and parse all files 110 for (let i = 0; i < filePaths.length; i++) { 111 const filePath = filePaths[i]; 112 113 if (showProgress) { 114 logger.progress("Reading lexicons", i + 1, filePaths.length); 115 } 116 117 try { 118 const content = await readAndParseLexicon(filePath); 119 120 // Basic structure validation 121 if (!content || typeof content !== "object") { 122 files.push({ 123 path: filePath, 124 content: null, 125 valid: false, 126 errors: ["Lexicon must be an object"], 127 }); 128 continue; 129 } 130 131 const lex = content as Record<string, unknown>; 132 133 // Check required fields 134 if (!lex.id || typeof lex.id !== "string") { 135 files.push({ 136 path: filePath, 137 content, 138 valid: false, 139 errors: ["Lexicon must have a valid 'id' field"], 140 }); 141 continue; 142 } 143 144 // Check for either 'definitions' or 'defs' 145 const defs = lex.defs || lex.definitions; 146 if (!defs || typeof defs !== "object") { 147 files.push({ 148 path: filePath, 149 content, 150 valid: false, 151 errors: ["Lexicon must have a 'defs' or 'definitions' object"], 152 }); 153 continue; 154 } 155 156 // Store for validation 157 lexicons.push(content as RawLexicon); 158 files.push({ 159 path: filePath, 160 content, 161 valid: true, 162 errors: [], 163 }); 164 } catch (error) { 165 const err = error as Error; 166 files.push({ 167 path: filePath, 168 content: null, 169 valid: false, 170 errors: [err.message], 171 }); 172 } 173 } 174 175 // Validate all lexicons together using the new validation system 176 if (lexicons.length > 0) { 177 try { 178 const validationErrors = await validate(lexicons); 179 180 // If validation errors exist, map them back to files 181 if (validationErrors) { 182 for (const file of files) { 183 if (file.valid && file.content) { 184 const lexicon = file.content as RawLexicon; 185 const errors = validationErrors[lexicon.id]; 186 if (errors && errors.length > 0) { 187 file.valid = false; 188 file.errors = errors; 189 } 190 } 191 } 192 } 193 } catch (error) { 194 const errorMessage = 195 error instanceof Error ? error.message : String(error); 196 // Mark all valid files as invalid due to validation failure 197 for (const file of files) { 198 if (file.valid) { 199 file.valid = false; 200 file.errors = [`Validation failed: ${errorMessage}`]; 201 } 202 } 203 } 204 } 205 206 const validFiles = files.filter((f) => f.valid).length; 207 const invalidFiles = files.length - validFiles; 208 209 return { 210 files, 211 totalFiles: filePaths.length, 212 validFiles, 213 invalidFiles, 214 }; 215} 216 217function colorizeErrorPaths(errorMessage: string): string { 218 // Highlight field paths in quotes with cyan color 219 return errorMessage.replace( 220 /'([^']+)'/g, 221 (_match, p1) => cyan(`'${p1}'`) 222 ); 223} 224 225function formatError(error: string, index: number): string { 226 return ` ${red(`${index + 1}.`)} ${colorizeErrorPaths(error)}`; 227} 228 229export function printValidationSummary(result: LexiconValidationResult): void { 230 logger.section("Validation Summary"); 231 logger.result(`Total files: ${result.totalFiles}`); 232 logger.result(`Valid: ${green(result.validFiles.toString())}`); 233 logger.result(`Invalid: ${red(result.invalidFiles.toString())}`); 234 235 if (result.invalidFiles > 0) { 236 logger.section("Invalid Files"); 237 for (const file of result.files) { 238 if (!file.valid) { 239 console.log(` ${dim(file.path)}`); 240 if (file.errors) { 241 file.errors.forEach((error, index) => { 242 console.log(formatError(error, index)); 243 }); 244 } 245 } 246 } 247 } 248}