/** * AT Protocol Lexicon Validation Library * * Core implementation for comprehensive validation of AT Protocol lexicon documents and data records. * Built on WebAssembly for high performance validation with full AT Protocol compliance. */ import init, { WasmLexiconValidator, validate_string_format, is_valid_nsid, validate_lexicons_and_get_errors, } from "./wasm/slices_lexicon.js"; // ============================================================================ // Module Initialization // ============================================================================ /** Global WASM initialization state */ let wasmInitialized = false; /** * Ensures the WebAssembly module is properly initialized before use. * This is called automatically by all public methods. * * @internal */ export async function ensureWasmInit(): Promise { if (!wasmInitialized) { const wasmUrl = new URL("./wasm/slices_lexicon_bg.wasm", import.meta.url); await init({ module_or_path: wasmUrl }); wasmInitialized = true; } } // ============================================================================ // Types and Interfaces // ============================================================================ /** * Custom error class for lexicon validation failures. * Extends the native Error class with validation-specific context. */ export class ValidationError extends Error { /** * Creates a new ValidationError instance. * * @param message - Human-readable error description */ constructor(message: string) { super(message); this.name = "ValidationError"; } } /** * Represents a complete AT Protocol lexicon document. * Contains schema definitions and metadata for a specific collection or service. */ export interface LexiconDoc { /** Unique namespace identifier (NSID) for this lexicon */ id: string; /** Human-readable description of the lexicon */ description?: string; /** Schema definitions mapping definition names to their schemas */ defs: Record; /** Lexicon format version (currently 1) */ lexicon?: number; } /** * Base lexicon definition interface */ export interface LexiconDefinition { type: string; description?: string; [key: string]: unknown; // Allow additional properties } /** * String format types supported by AT Protocol */ export type StringFormat = | "datetime" | "uri" | "at-uri" | "did" | "handle" | "at-identifier" | "nsid" | "cid" | "language" | "tid" | "record-key"; /** * Type alias for validation error results */ export type ValidationErrors = Record | null; /** * Comprehensive validation result with error details and statistics. */ export interface ValidationResult { /** Whether all lexicons passed validation */ valid: boolean; /** Map of lexicon IDs to their error messages */ errors: Record; /** Total count of all validation errors across all lexicons */ totalErrors: number; /** Number of lexicons that have validation errors */ lexiconsWithErrors: number; } /** * Detailed error context information for debugging validation failures. */ export interface ErrorContext { /** Current path in the schema being validated */ path: string; /** ID of the lexicon currently being validated */ current_lexicon: string | null; /** Whether a circular reference was detected */ has_circular_reference: boolean; /** Stack of references being resolved (for circular detection) */ reference_stack: string[]; } // ============================================================================ // Core Validator Class // ============================================================================ /** * High-performance AT Protocol lexicon validator with resource management. * * Provides comprehensive validation capabilities including: * - Schema structure validation * - Cross-reference resolution * - Data format validation * - Circular dependency detection * * Implements Disposable for automatic resource cleanup. */ export class LexiconValidator implements Disposable { private wasmValidator: WasmLexiconValidator; /** * Creates a new validator instance with the provided lexicon documents. * * Note: Prefer using the static `create()` method for async initialization. * * @param lexicons - Array of lexicon documents to validate */ constructor(lexicons: LexiconDoc[]) { const lexiconsJson = JSON.stringify(lexicons); this.wasmValidator = new WasmLexiconValidator(lexiconsJson); } /** * Creates a new validator instance with proper async initialization. * Ensures WASM module is loaded before creating the validator. * * @param lexicons - Array of lexicon documents to validate * @returns Promise resolving to a new validator instance * * @example * ```typescript * const lexicons = [{ id: "com.example.post", defs: {...} }]; * const validator = await LexiconValidator.create(lexicons); * ``` */ static async create(lexicons: LexiconDoc[]): Promise { await ensureWasmInit(); return new LexiconValidator(lexicons); } /** * Validates a data record against its collection's lexicon schema. * * Performs comprehensive validation including: * - Type checking * - Constraint validation * - Format validation * - Required field checking * * @param collection - NSID of the collection (lexicon) to validate against * @param record - Data record to validate * @throws {ValidationError} If validation fails * * @example * ```typescript * validator.validateRecord("com.example.post", { * text: "Hello world!", * createdAt: "2024-01-01T12:00:00Z" * }); * ``` */ validateRecord(collection: string, record: Record): void { try { const recordJson = JSON.stringify(record); this.wasmValidator.validate_record(collection, recordJson); } catch (error) { throw new ValidationError( error instanceof Error ? error.message : String(error) ); } } /** * Validates all lexicon documents and returns comprehensive error information. * * Uses the enhanced validation system that collects ALL errors for each lexicon, * not just the first error encountered. * * @returns Null if validation passes, or error map if validation fails * * @example * ```typescript * const errors = validator.validateLexicons(); * if (errors) { * console.log(`Found errors in ${Object.keys(errors).length} lexicons`); * } * ``` */ validateLexicons(): Record | null { try { const result = this.wasmValidator.validate_lexicons(); const errorMap = JSON.parse(result); // Return null for success case (no errors) if (Object.keys(errorMap).length === 0) { return null; } return errorMap; } catch (error) { throw new ValidationError( error instanceof Error ? error.message : String(error) ); } } /** * Checks for unresolved references across the lexicon set. * * Identifies references (like "$ref": "com.example.collection#definition") * that point to non-existent lexicons or definitions. * * @returns Array of unresolved reference strings * * @example * ```typescript * const unresolved = validator.checkReferences(); * if (unresolved.length > 0) { * console.log("Unresolved references:", unresolved); * } * ``` */ checkReferences(): string[] { try { const result = this.wasmValidator.check_references(); return JSON.parse(result); } catch (error) { throw new ValidationError( error instanceof Error ? error.message : String(error) ); } } /** * Retrieves detailed error context for debugging validation failures. * * Provides path information, current lexicon context, and circular * reference detection state for enhanced debugging. * * @param path - Schema path to get context for * @returns Detailed error context information * * @example * ```typescript * const context = validator.getErrorContext("com.example.post#main"); * console.log(`Validating at path: ${context.path}`); * ``` */ getErrorContext(path: string): ErrorContext { try { const result = this.wasmValidator.get_error_context(path); return JSON.parse(result); } catch (error) { throw new ValidationError( error instanceof Error ? error.message : String(error) ); } } /** * Manually releases WASM resources. * * Call this when done with the validator to free memory. * Automatically called by dispose methods when using `using` syntax. */ free(): void { this.wasmValidator.free(); } /** * Synchronous dispose method for automatic resource cleanup. * Called automatically when using `using` keyword. * * @example * ```typescript * using validator = await LexiconValidator.create(lexicons); * // Automatically cleaned up at end of scope * ``` */ [Symbol.dispose](): void { this.free(); } /** * Asynchronous dispose method for automatic resource cleanup. * Called automatically when using `await using` keyword. */ async [Symbol.asyncDispose](): Promise { await Promise.resolve(this.free()); } } // ============================================================================ // Standalone Validation Functions // ============================================================================ /** * Validates lexicon documents using the enhanced validation system. * * This is the primary validation function that collects ALL validation errors * for each lexicon, enabling comprehensive error reporting. * * @param lexicons - Array of lexicon documents to validate * @returns Null if validation succeeds, or error map if validation fails * * @example * ```typescript * const errors = await validate([ * { id: "com.example.post", defs: {...} }, * { id: "com.example.user", defs: {...} } * ]); * * if (errors) { * for (const [lexiconId, errorList] of Object.entries(errors)) { * console.log(`${lexiconId}: ${errorList.length} errors`); * } * } * ``` */ export async function validate( lexicons: LexiconDoc[] ): Promise | null> { await ensureWasmInit(); const lexiconsJson = JSON.stringify(lexicons); const result = validate_lexicons_and_get_errors(lexiconsJson); const errorMap = JSON.parse(result); // Success case: no errors found if (Object.keys(errorMap).length === 0) { return null; } // Return complete error map with all errors for each lexicon return errorMap; } /** * Enhanced validation function with comprehensive result statistics. * * Provides detailed validation results including error counts and statistics * for better reporting and debugging. * * @param lexicons - Array of lexicon documents to validate * @returns Detailed validation result with statistics * * @example * ```typescript * const result = await validateWithDetails(lexicons); * console.log(`Validation ${result.valid ? 'passed' : 'failed'}`); * console.log(`Total errors: ${result.totalErrors}`); * console.log(`Lexicons with errors: ${result.lexiconsWithErrors}`); * ``` */ export async function validateWithDetails( lexicons: LexiconDoc[] ): Promise { const errors = await validate(lexicons); if (errors === null) { return { valid: true, errors: {}, totalErrors: 0, lexiconsWithErrors: 0, }; } const totalErrors = Object.values(errors).reduce( (sum, errorArray) => sum + errorArray.length, 0 ); return { valid: false, errors, totalErrors, lexiconsWithErrors: Object.keys(errors).length, }; } /** * Validates a single data record against lexicon schemas. * * Convenience function that creates a temporary validator instance * and automatically manages resources. * * @param lexicons - Array of lexicon documents containing the target schema * @param collection - NSID of the collection to validate against * @param record - Data record to validate * @throws {ValidationError} If validation fails * * @example * ```typescript * await validateRecord(lexicons, "com.example.post", { * text: "Hello world!", * createdAt: "2024-01-01T12:00:00Z" * }); * ``` */ export async function validateRecord( lexicons: LexiconDoc[], collection: string, record: Record ): Promise { return await withValidator(lexicons, (validator) => { validator.validateRecord(collection, record); }); } // ============================================================================ // Format and Utility Functions // ============================================================================ /** * Validates a string value against a specific AT Protocol format. * * Supports all AT Protocol string formats including: * - datetime (RFC 3339) * - uri, at-uri * - did, handle * - cid, tid * - language (BCP 47) * * @param value - String value to validate * @param format - AT Protocol format name * @throws {ValidationError} If format validation fails * * @example * ```typescript * await validateStringFormat("2024-01-01T12:00:00Z", "datetime"); * await validateStringFormat("did:plc:example123", "did"); * ``` */ export async function validateStringFormat( value: string, format: string ): Promise { await ensureWasmInit(); try { validate_string_format(value, format); } catch (error) { throw new ValidationError( error instanceof Error ? error.message : String(error) ); } } /** * Checks if a string is a valid NSID (Namespaced Identifier). * * NSIDs are used throughout AT Protocol to identify lexicons, collections, * and procedures. They follow reverse-domain-name format with at least * 3 segments (e.g., "com.example.collection"). * * @param nsid - String to validate as NSID * @returns True if valid NSID, false otherwise * * @example * ```typescript * console.log(await isValidNsid("com.example.post")); // true * console.log(await isValidNsid("invalid-nsid")); // false * ``` */ export async function isValidNsid(nsid: string): Promise { await ensureWasmInit(); return is_valid_nsid(nsid); } // ============================================================================ // Resource Management Utilities // ============================================================================ /** * Utility function for automatic validator lifecycle management. * * Uses the callback pattern to ensure WASM resources are properly * cleaned up, even if an exception occurs during validation. * * @param lexicons - Array of lexicon documents * @param callback - Function to execute with the validator * @returns Promise resolving to the callback's return value * * @example * ```typescript * const result = await withValidator(lexicons, (validator) => { * validator.validateRecord("com.example.post", record); * return "validation passed"; * }); * ``` */ export async function withValidator( lexicons: LexiconDoc[], callback: (validator: LexiconValidator) => T | Promise ): Promise { const validator = await LexiconValidator.create(lexicons); try { return await callback(validator); } finally { validator.free(); } }