Highly ambitious ATProtocol AppView service and sdks
at main 547 lines 16 kB view raw
1/** 2 * AT Protocol Lexicon Validation Library 3 * 4 * Core implementation for comprehensive validation of AT Protocol lexicon documents and data records. 5 * Built on WebAssembly for high performance validation with full AT Protocol compliance. 6 */ 7 8import init, { 9 WasmLexiconValidator, 10 validate_string_format, 11 is_valid_nsid, 12 validate_lexicons_and_get_errors, 13} from "./wasm/slices_lexicon.js"; 14 15// ============================================================================ 16// Module Initialization 17// ============================================================================ 18 19/** Global WASM initialization state */ 20let wasmInitialized = false; 21 22/** 23 * Ensures the WebAssembly module is properly initialized before use. 24 * This is called automatically by all public methods. 25 * 26 * @internal 27 */ 28export async function ensureWasmInit(): Promise<void> { 29 if (!wasmInitialized) { 30 const wasmUrl = new URL("./wasm/slices_lexicon_bg.wasm", import.meta.url); 31 await init({ module_or_path: wasmUrl }); 32 wasmInitialized = true; 33 } 34} 35 36// ============================================================================ 37// Types and Interfaces 38// ============================================================================ 39 40/** 41 * Custom error class for lexicon validation failures. 42 * Extends the native Error class with validation-specific context. 43 */ 44export class ValidationError extends Error { 45 /** 46 * Creates a new ValidationError instance. 47 * 48 * @param message - Human-readable error description 49 */ 50 constructor(message: string) { 51 super(message); 52 this.name = "ValidationError"; 53 } 54} 55 56/** 57 * Represents a complete AT Protocol lexicon document. 58 * Contains schema definitions and metadata for a specific collection or service. 59 */ 60export interface LexiconDoc { 61 /** Unique namespace identifier (NSID) for this lexicon */ 62 id: string; 63 64 /** Human-readable description of the lexicon */ 65 description?: string; 66 67 /** Schema definitions mapping definition names to their schemas */ 68 defs: Record<string, LexiconDefinition>; 69 70 /** Lexicon format version (currently 1) */ 71 lexicon?: number; 72} 73 74/** 75 * Base lexicon definition interface 76 */ 77export interface LexiconDefinition { 78 type: string; 79 description?: string; 80 [key: string]: unknown; // Allow additional properties 81} 82 83/** 84 * String format types supported by AT Protocol 85 */ 86export type StringFormat = 87 | "datetime" 88 | "uri" 89 | "at-uri" 90 | "did" 91 | "handle" 92 | "at-identifier" 93 | "nsid" 94 | "cid" 95 | "language" 96 | "tid" 97 | "record-key"; 98 99/** 100 * Type alias for validation error results 101 */ 102export type ValidationErrors = Record<string, string[]> | null; 103 104/** 105 * Comprehensive validation result with error details and statistics. 106 */ 107export interface ValidationResult { 108 /** Whether all lexicons passed validation */ 109 valid: boolean; 110 111 /** Map of lexicon IDs to their error messages */ 112 errors: Record<string, string[]>; 113 114 /** Total count of all validation errors across all lexicons */ 115 totalErrors: number; 116 117 /** Number of lexicons that have validation errors */ 118 lexiconsWithErrors: number; 119} 120 121/** 122 * Detailed error context information for debugging validation failures. 123 */ 124export interface ErrorContext { 125 /** Current path in the schema being validated */ 126 path: string; 127 128 /** ID of the lexicon currently being validated */ 129 current_lexicon: string | null; 130 131 /** Whether a circular reference was detected */ 132 has_circular_reference: boolean; 133 134 /** Stack of references being resolved (for circular detection) */ 135 reference_stack: string[]; 136} 137 138// ============================================================================ 139// Core Validator Class 140// ============================================================================ 141 142/** 143 * High-performance AT Protocol lexicon validator with resource management. 144 * 145 * Provides comprehensive validation capabilities including: 146 * - Schema structure validation 147 * - Cross-reference resolution 148 * - Data format validation 149 * - Circular dependency detection 150 * 151 * Implements Disposable for automatic resource cleanup. 152 */ 153export class LexiconValidator implements Disposable { 154 private wasmValidator: WasmLexiconValidator; 155 156 /** 157 * Creates a new validator instance with the provided lexicon documents. 158 * 159 * Note: Prefer using the static `create()` method for async initialization. 160 * 161 * @param lexicons - Array of lexicon documents to validate 162 */ 163 constructor(lexicons: LexiconDoc[]) { 164 const lexiconsJson = JSON.stringify(lexicons); 165 this.wasmValidator = new WasmLexiconValidator(lexiconsJson); 166 } 167 168 /** 169 * Creates a new validator instance with proper async initialization. 170 * Ensures WASM module is loaded before creating the validator. 171 * 172 * @param lexicons - Array of lexicon documents to validate 173 * @returns Promise resolving to a new validator instance 174 * 175 * @example 176 * ```typescript 177 * const lexicons = [{ id: "com.example.post", defs: {...} }]; 178 * const validator = await LexiconValidator.create(lexicons); 179 * ``` 180 */ 181 static async create(lexicons: LexiconDoc[]): Promise<LexiconValidator> { 182 await ensureWasmInit(); 183 return new LexiconValidator(lexicons); 184 } 185 186 /** 187 * Validates a data record against its collection's lexicon schema. 188 * 189 * Performs comprehensive validation including: 190 * - Type checking 191 * - Constraint validation 192 * - Format validation 193 * - Required field checking 194 * 195 * @param collection - NSID of the collection (lexicon) to validate against 196 * @param record - Data record to validate 197 * @throws {ValidationError} If validation fails 198 * 199 * @example 200 * ```typescript 201 * validator.validateRecord("com.example.post", { 202 * text: "Hello world!", 203 * createdAt: "2024-01-01T12:00:00Z" 204 * }); 205 * ``` 206 */ 207 validateRecord(collection: string, record: Record<string, unknown>): void { 208 try { 209 const recordJson = JSON.stringify(record); 210 this.wasmValidator.validate_record(collection, recordJson); 211 } catch (error) { 212 throw new ValidationError( 213 error instanceof Error ? error.message : String(error) 214 ); 215 } 216 } 217 218 /** 219 * Validates all lexicon documents and returns comprehensive error information. 220 * 221 * Uses the enhanced validation system that collects ALL errors for each lexicon, 222 * not just the first error encountered. 223 * 224 * @returns Null if validation passes, or error map if validation fails 225 * 226 * @example 227 * ```typescript 228 * const errors = validator.validateLexicons(); 229 * if (errors) { 230 * console.log(`Found errors in ${Object.keys(errors).length} lexicons`); 231 * } 232 * ``` 233 */ 234 validateLexicons(): Record<string, string[]> | null { 235 try { 236 const result = this.wasmValidator.validate_lexicons(); 237 const errorMap = JSON.parse(result); 238 239 // Return null for success case (no errors) 240 if (Object.keys(errorMap).length === 0) { 241 return null; 242 } 243 244 return errorMap; 245 } catch (error) { 246 throw new ValidationError( 247 error instanceof Error ? error.message : String(error) 248 ); 249 } 250 } 251 252 /** 253 * Checks for unresolved references across the lexicon set. 254 * 255 * Identifies references (like "$ref": "com.example.collection#definition") 256 * that point to non-existent lexicons or definitions. 257 * 258 * @returns Array of unresolved reference strings 259 * 260 * @example 261 * ```typescript 262 * const unresolved = validator.checkReferences(); 263 * if (unresolved.length > 0) { 264 * console.log("Unresolved references:", unresolved); 265 * } 266 * ``` 267 */ 268 checkReferences(): string[] { 269 try { 270 const result = this.wasmValidator.check_references(); 271 return JSON.parse(result); 272 } catch (error) { 273 throw new ValidationError( 274 error instanceof Error ? error.message : String(error) 275 ); 276 } 277 } 278 279 /** 280 * Retrieves detailed error context for debugging validation failures. 281 * 282 * Provides path information, current lexicon context, and circular 283 * reference detection state for enhanced debugging. 284 * 285 * @param path - Schema path to get context for 286 * @returns Detailed error context information 287 * 288 * @example 289 * ```typescript 290 * const context = validator.getErrorContext("com.example.post#main"); 291 * console.log(`Validating at path: ${context.path}`); 292 * ``` 293 */ 294 getErrorContext(path: string): ErrorContext { 295 try { 296 const result = this.wasmValidator.get_error_context(path); 297 return JSON.parse(result); 298 } catch (error) { 299 throw new ValidationError( 300 error instanceof Error ? error.message : String(error) 301 ); 302 } 303 } 304 305 /** 306 * Manually releases WASM resources. 307 * 308 * Call this when done with the validator to free memory. 309 * Automatically called by dispose methods when using `using` syntax. 310 */ 311 free(): void { 312 this.wasmValidator.free(); 313 } 314 315 /** 316 * Synchronous dispose method for automatic resource cleanup. 317 * Called automatically when using `using` keyword. 318 * 319 * @example 320 * ```typescript 321 * using validator = await LexiconValidator.create(lexicons); 322 * // Automatically cleaned up at end of scope 323 * ``` 324 */ 325 [Symbol.dispose](): void { 326 this.free(); 327 } 328 329 /** 330 * Asynchronous dispose method for automatic resource cleanup. 331 * Called automatically when using `await using` keyword. 332 */ 333 async [Symbol.asyncDispose](): Promise<void> { 334 await Promise.resolve(this.free()); 335 } 336} 337 338// ============================================================================ 339// Standalone Validation Functions 340// ============================================================================ 341 342/** 343 * Validates lexicon documents using the enhanced validation system. 344 * 345 * This is the primary validation function that collects ALL validation errors 346 * for each lexicon, enabling comprehensive error reporting. 347 * 348 * @param lexicons - Array of lexicon documents to validate 349 * @returns Null if validation succeeds, or error map if validation fails 350 * 351 * @example 352 * ```typescript 353 * const errors = await validate([ 354 * { id: "com.example.post", defs: {...} }, 355 * { id: "com.example.user", defs: {...} } 356 * ]); 357 * 358 * if (errors) { 359 * for (const [lexiconId, errorList] of Object.entries(errors)) { 360 * console.log(`${lexiconId}: ${errorList.length} errors`); 361 * } 362 * } 363 * ``` 364 */ 365export async function validate( 366 lexicons: LexiconDoc[] 367): Promise<Record<string, string[]> | null> { 368 await ensureWasmInit(); 369 370 const lexiconsJson = JSON.stringify(lexicons); 371 const result = validate_lexicons_and_get_errors(lexiconsJson); 372 const errorMap = JSON.parse(result); 373 374 // Success case: no errors found 375 if (Object.keys(errorMap).length === 0) { 376 return null; 377 } 378 379 // Return complete error map with all errors for each lexicon 380 return errorMap; 381} 382 383/** 384 * Enhanced validation function with comprehensive result statistics. 385 * 386 * Provides detailed validation results including error counts and statistics 387 * for better reporting and debugging. 388 * 389 * @param lexicons - Array of lexicon documents to validate 390 * @returns Detailed validation result with statistics 391 * 392 * @example 393 * ```typescript 394 * const result = await validateWithDetails(lexicons); 395 * console.log(`Validation ${result.valid ? 'passed' : 'failed'}`); 396 * console.log(`Total errors: ${result.totalErrors}`); 397 * console.log(`Lexicons with errors: ${result.lexiconsWithErrors}`); 398 * ``` 399 */ 400export async function validateWithDetails( 401 lexicons: LexiconDoc[] 402): Promise<ValidationResult> { 403 const errors = await validate(lexicons); 404 405 if (errors === null) { 406 return { 407 valid: true, 408 errors: {}, 409 totalErrors: 0, 410 lexiconsWithErrors: 0, 411 }; 412 } 413 414 const totalErrors = Object.values(errors).reduce( 415 (sum, errorArray) => sum + errorArray.length, 416 0 417 ); 418 419 return { 420 valid: false, 421 errors, 422 totalErrors, 423 lexiconsWithErrors: Object.keys(errors).length, 424 }; 425} 426 427/** 428 * Validates a single data record against lexicon schemas. 429 * 430 * Convenience function that creates a temporary validator instance 431 * and automatically manages resources. 432 * 433 * @param lexicons - Array of lexicon documents containing the target schema 434 * @param collection - NSID of the collection to validate against 435 * @param record - Data record to validate 436 * @throws {ValidationError} If validation fails 437 * 438 * @example 439 * ```typescript 440 * await validateRecord(lexicons, "com.example.post", { 441 * text: "Hello world!", 442 * createdAt: "2024-01-01T12:00:00Z" 443 * }); 444 * ``` 445 */ 446export async function validateRecord( 447 lexicons: LexiconDoc[], 448 collection: string, 449 record: Record<string, unknown> 450): Promise<void> { 451 return await withValidator(lexicons, (validator) => { 452 validator.validateRecord(collection, record); 453 }); 454} 455 456// ============================================================================ 457// Format and Utility Functions 458// ============================================================================ 459 460/** 461 * Validates a string value against a specific AT Protocol format. 462 * 463 * Supports all AT Protocol string formats including: 464 * - datetime (RFC 3339) 465 * - uri, at-uri 466 * - did, handle 467 * - cid, tid 468 * - language (BCP 47) 469 * 470 * @param value - String value to validate 471 * @param format - AT Protocol format name 472 * @throws {ValidationError} If format validation fails 473 * 474 * @example 475 * ```typescript 476 * await validateStringFormat("2024-01-01T12:00:00Z", "datetime"); 477 * await validateStringFormat("did:plc:example123", "did"); 478 * ``` 479 */ 480export async function validateStringFormat( 481 value: string, 482 format: string 483): Promise<void> { 484 await ensureWasmInit(); 485 try { 486 validate_string_format(value, format); 487 } catch (error) { 488 throw new ValidationError( 489 error instanceof Error ? error.message : String(error) 490 ); 491 } 492} 493 494/** 495 * Checks if a string is a valid NSID (Namespaced Identifier). 496 * 497 * NSIDs are used throughout AT Protocol to identify lexicons, collections, 498 * and procedures. They follow reverse-domain-name format with at least 499 * 3 segments (e.g., "com.example.collection"). 500 * 501 * @param nsid - String to validate as NSID 502 * @returns True if valid NSID, false otherwise 503 * 504 * @example 505 * ```typescript 506 * console.log(await isValidNsid("com.example.post")); // true 507 * console.log(await isValidNsid("invalid-nsid")); // false 508 * ``` 509 */ 510export async function isValidNsid(nsid: string): Promise<boolean> { 511 await ensureWasmInit(); 512 return is_valid_nsid(nsid); 513} 514 515// ============================================================================ 516// Resource Management Utilities 517// ============================================================================ 518 519/** 520 * Utility function for automatic validator lifecycle management. 521 * 522 * Uses the callback pattern to ensure WASM resources are properly 523 * cleaned up, even if an exception occurs during validation. 524 * 525 * @param lexicons - Array of lexicon documents 526 * @param callback - Function to execute with the validator 527 * @returns Promise resolving to the callback's return value 528 * 529 * @example 530 * ```typescript 531 * const result = await withValidator(lexicons, (validator) => { 532 * validator.validateRecord("com.example.post", record); 533 * return "validation passed"; 534 * }); 535 * ``` 536 */ 537export async function withValidator<T>( 538 lexicons: LexiconDoc[], 539 callback: (validator: LexiconValidator) => T | Promise<T> 540): Promise<T> { 541 const validator = await LexiconValidator.create(lexicons); 542 try { 543 return await callback(validator); 544 } finally { 545 validator.free(); 546 } 547}