Highly ambitious ATProtocol AppView service and sdks
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}