Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

enhance lexicon structure and constraints validation

+2208 -303
+34
deno.lock
··· 648 648 "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" 649 649 } 650 650 }, 651 + "remote": { 652 + "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", 653 + "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", 654 + "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", 655 + "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", 656 + "https://deno.land/std@0.208.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", 657 + "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", 658 + "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", 659 + "https://deno.land/std@0.208.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", 660 + "https://deno.land/std@0.208.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", 661 + "https://deno.land/std@0.208.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", 662 + "https://deno.land/std@0.208.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", 663 + "https://deno.land/std@0.208.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", 664 + "https://deno.land/std@0.208.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", 665 + "https://deno.land/std@0.208.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", 666 + "https://deno.land/std@0.208.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", 667 + "https://deno.land/std@0.208.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", 668 + "https://deno.land/std@0.208.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", 669 + "https://deno.land/std@0.208.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", 670 + "https://deno.land/std@0.208.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", 671 + "https://deno.land/std@0.208.0/assert/assert_not_strict_equals.ts": "4cdef83df17488df555c8aac1f7f5ec2b84ad161b6d0645ccdbcc17654e80c99", 672 + "https://deno.land/std@0.208.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", 673 + "https://deno.land/std@0.208.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", 674 + "https://deno.land/std@0.208.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", 675 + "https://deno.land/std@0.208.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", 676 + "https://deno.land/std@0.208.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", 677 + "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", 678 + "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", 679 + "https://deno.land/std@0.208.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", 680 + "https://deno.land/std@0.208.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", 681 + "https://deno.land/std@0.208.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", 682 + "https://deno.land/std@0.208.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", 683 + "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2" 684 + }, 651 685 "workspace": { 652 686 "dependencies": [ 653 687 "jsr:@std/cli@^1.0.22",
+12 -12
packages/cli/src/commands/lexicon/import.ts
··· 222 222 const validateOnly = args["validate-only"] as boolean; 223 223 const dryRun = args["dry-run"] as boolean; 224 224 225 - logger.step("🔍 Finding lexicon files..."); 226 - logger.info(`📁 Scanning directory: ${lexiconPath}`); 225 + logger.step("Finding lexicon files..."); 226 + logger.info(`Scanning directory: ${lexiconPath}`); 227 227 228 228 const lexiconFiles = await findLexiconFiles(lexiconPath); 229 229 ··· 232 232 return; 233 233 } 234 234 235 - logger.info(`📄 Found ${lexiconFiles.length} JSON files`); 235 + logger.info(`Found ${lexiconFiles.length} JSON files`); 236 236 237 237 // Validate all lexicon files 238 - logger.step("🔬 Validating lexicon files..."); 238 + logger.step("Validating lexicon files..."); 239 239 const validationResult = await validateLexiconFiles(lexiconFiles); 240 240 241 241 printValidationSummary(validationResult); 242 242 243 243 if (validationResult.invalidFiles > 0) { 244 - logger.error(`❌ ${validationResult.invalidFiles} invalid files found`); 244 + logger.error(`${validationResult.invalidFiles} invalid files found`); 245 245 if (!validateOnly) { 246 246 logger.error("Please fix validation errors before importing"); 247 247 Deno.exit(1); ··· 249 249 } 250 250 251 251 if (validateOnly) { 252 - logger.success("✅ Validation complete"); 252 + logger.success("Validation complete"); 253 253 return; 254 254 } 255 255 256 256 if (validationResult.validFiles === 0) { 257 - logger.error("❌ No valid lexicon files to import"); 257 + logger.error("No valid lexicon files to import"); 258 258 Deno.exit(1); 259 259 } 260 260 ··· 263 263 await config.load(); 264 264 265 265 if (!config.isAuthenticated()) { 266 - logger.error("❌ Not authenticated. Run 'slices login' first."); 266 + logger.error("Not authenticated. Run 'slices login' first."); 267 267 Deno.exit(1); 268 268 } 269 269 270 270 // Initialize authenticated client 271 - logger.step("🔐 Initializing authenticated client..."); 271 + logger.step("Initializing authenticated client..."); 272 272 const client = await createAuthenticatedClient(sliceUri, apiUrl); 273 273 274 274 if (dryRun) { 275 - logger.info("🔬 DRY RUN - No actual uploads will be performed"); 275 + logger.info("DRY RUN - No actual uploads will be performed"); 276 276 } else { 277 - logger.step(`📤 Uploading ${validationResult.validFiles} valid lexicons to ${sliceUri}...`); 277 + logger.step(`Uploading ${validationResult.validFiles} valid lexicons to ${sliceUri}...`); 278 278 } 279 279 280 280 // Upload lexicons ··· 288 288 printImportSummary(importStats); 289 289 290 290 if (importStats.failed > 0) { 291 - logger.error(`❌ ${importStats.failed} uploads failed`); 291 + logger.error(`${importStats.failed} uploads failed`); 292 292 Deno.exit(1); 293 293 } 294 294
+98 -48
packages/cli/src/utils/lexicon.ts
··· 3 3 import { LexiconValidator, type LexiconDoc } from "@slices/lexicon"; 4 4 import { logger } from "./logger.ts"; 5 5 6 + // Type for raw lexicon content that may include unknown fields 7 + type RawLexicon = LexiconDoc & Record<string, unknown>; 8 + 6 9 export interface LexiconFile { 7 10 path: string; 8 11 content: unknown; ··· 77 80 }; 78 81 } 79 82 80 - // Convert to LexiconDoc format expected by @slices/lexicon 81 - const lexiconDoc: LexiconDoc = { 82 - id: lex.id as string, 83 - defs: defs as Record<string, unknown>, 84 - lexicon: (lex.lexicon as number) || 1, 85 - }; 86 - 87 83 // Use @slices/lexicon for validation 84 + // Pass the original lexicon to preserve all fields for validation 88 85 try { 89 - const validator = await LexiconValidator.create([lexiconDoc]); 86 + const validator = await LexiconValidator.create([lexicon as RawLexicon]); 90 87 try { 91 88 validator.validateLexiconSetCompleteness(); 92 89 validator.free(); // Clean up resources 93 90 return { valid: true }; 94 91 } catch (validationError) { 95 92 validator.free(); // Clean up resources 96 - const err = validationError as Error; 97 - return { valid: false, errors: [err.message] }; 93 + const errorMessage = validationError instanceof Error ? validationError.message : String(validationError); 94 + return { valid: false, errors: [errorMessage] }; 98 95 } 99 96 } catch (validationError) { 100 - // If @slices/lexicon validation fails, fall back to basic validation 101 - const err = validationError as Error; 102 - logger.debug("Lexicon validation library error:", err.message); 103 - return { valid: true }; // Allow basic structure if advanced validation fails 97 + // Lexicon validation failed - report the actual error 98 + const errorMessage = validationError instanceof Error ? validationError.message : String(validationError); 99 + return { valid: false, errors: [errorMessage] }; 104 100 } 105 101 } catch (error) { 106 102 const err = error as Error; ··· 113 109 showProgress = true 114 110 ): Promise<LexiconValidationResult> { 115 111 const files: LexiconFile[] = []; 116 - const lexiconDocs: LexiconDoc[] = []; 112 + const validLexicons: RawLexicon[] = []; // Store valid original lexicon objects 117 113 118 114 // First pass: read and parse all files 119 115 for (let i = 0; i < filePaths.length; i++) { ··· 162 158 continue; 163 159 } 164 160 165 - // Convert to LexiconDoc format 166 - const lexiconDoc: LexiconDoc = { 167 - id: lex.id as string, 168 - defs: defs as Record<string, unknown>, 169 - lexicon: (lex.lexicon as number) || 1, 170 - }; 161 + // Validate individual lexicon first 162 + // Pass the original content to preserve all fields for validation 163 + try { 164 + const individualValidator = await LexiconValidator.create([content as RawLexicon]); 165 + individualValidator.free(); // Clean up immediately 171 166 172 - lexiconDocs.push(lexiconDoc); 173 - files.push({ 174 - path: filePath, 175 - content, 176 - valid: true, // Will be updated after set validation 177 - errors: [], 178 - }); 167 + validLexicons.push(content as RawLexicon); // Store valid original content 168 + files.push({ 169 + path: filePath, 170 + content, 171 + valid: true, 172 + errors: [], 173 + }); 174 + } catch (validationError) { 175 + // Individual lexicon validation failed 176 + const errorMessage = validationError instanceof Error ? validationError.message : String(validationError); 177 + files.push({ 178 + path: filePath, 179 + content, 180 + valid: false, 181 + errors: [errorMessage], 182 + }); 183 + } 179 184 } catch (error) { 180 185 const err = error as Error; 181 186 files.push({ ··· 187 192 } 188 193 } 189 194 190 - // Second pass: validate the complete set together 191 - if (lexiconDocs.length > 0) { 195 + // Second pass: validate the complete set together (only for individually valid files) 196 + if (validLexicons.length > 0) { 192 197 try { 193 - const validator = await LexiconValidator.create(lexiconDocs); 198 + const validator = await LexiconValidator.create(validLexicons); 194 199 try { 195 200 validator.validateLexiconSetCompleteness(); 196 201 validator.free(); 197 202 // All lexicons in the set are valid 198 203 } catch (validationError) { 199 204 validator.free(); 200 - const err = validationError as Error; 205 + const errorMessage = validationError instanceof Error ? validationError.message : String(validationError); 201 206 202 - // Mark all files as invalid due to set validation failure 207 + // Mark all individually valid files as invalid due to set validation failure 203 208 for (const file of files) { 204 209 if (file.valid) { 205 210 file.valid = false; 206 - file.errors = [err.message]; 211 + file.errors = [`Set validation failed: ${errorMessage}`]; 207 212 } 208 213 } 209 214 } 210 215 } catch (validationError) { 211 - // If validator creation fails, use basic validation results 212 - const err = validationError as Error; 213 - logger.debug("Lexicon validation library error:", err.message); 214 - // Keep the basic validation results (already set above) 216 + // This shouldn't happen since we already validated individual lexicons 217 + // But if it does, it's likely a set-level issue 218 + const errorMessage = validationError instanceof Error ? validationError.message : String(validationError); 219 + 220 + // Mark only individually valid files as invalid due to set creation failure 221 + for (const file of files) { 222 + if (file.valid) { 223 + file.valid = false; 224 + file.errors = [`Set creation failed: ${errorMessage}`]; 225 + } 226 + } 215 227 } 216 228 } 217 229 ··· 226 238 }; 227 239 } 228 240 241 + // ANSI color codes 242 + const colors = { 243 + reset: '\x1b[0m', 244 + red: '\x1b[31m', 245 + green: '\x1b[32m', 246 + yellow: '\x1b[33m', 247 + blue: '\x1b[34m', 248 + magenta: '\x1b[35m', 249 + cyan: '\x1b[36m', 250 + dim: '\x1b[2m', 251 + }; 252 + 253 + function colorizeErrorPaths(errorMessage: string): string { 254 + // Highlight field paths in quotes with cyan color 255 + return errorMessage.replace(/'([^']+)'/g, `${colors.cyan}'$1'${colors.reset}`); 256 + } 257 + 258 + function formatError(error: string, index: number): string { 259 + // Handle "Multiple validation errors:" by extracting individual errors 260 + if (error.includes("Multiple validation errors:")) { 261 + const lines = error.split('\n'); 262 + const errors: string[] = []; 263 + 264 + for (const line of lines) { 265 + const trimmed = line.trim(); 266 + if (trimmed.startsWith('- ')) { 267 + errors.push(trimmed.substring(2)); // Remove "- " prefix 268 + } 269 + } 270 + 271 + return errors.map((err, i) => 272 + ` ${colors.red}${index + 1}.${i + 1}${colors.reset} ${colorizeErrorPaths(err)}` 273 + ).join('\n'); 274 + } else { 275 + return ` ${colors.red}${index + 1}.${colors.reset} ${colorizeErrorPaths(error)}`; 276 + } 277 + } 278 + 229 279 export function printValidationSummary(result: LexiconValidationResult): void { 230 - console.log("\n📊 Validation Summary"); 231 - console.log("━━━━━━━━━━━━━━━━━━━━━"); 232 - console.log(`📁 Total files: ${result.totalFiles}`); 233 - console.log(`✅ Valid files: ${result.validFiles}`); 234 - console.log(`❌ Invalid files: ${result.invalidFiles}`); 280 + console.log("\nValidation Summary"); 281 + console.log("─".repeat(50)); 282 + console.log(`Total files: ${result.totalFiles}`); 283 + console.log(`${colors.green}Valid: ${result.validFiles}${colors.reset}`); 284 + console.log(`${colors.red}Invalid: ${result.invalidFiles}${colors.reset}`); 235 285 236 286 if (result.invalidFiles > 0) { 237 - console.log("\n❌ Invalid Files:"); 287 + console.log(`\n${colors.red}Invalid Files:${colors.reset}`); 238 288 for (const file of result.files) { 239 289 if (!file.valid) { 240 - console.log(` ${file.path}`); 290 + console.log(` ${colors.dim}${file.path}${colors.reset}`); 241 291 if (file.errors) { 242 - for (const error of file.errors) { 243 - console.log(` - ${error}`); 244 - } 292 + file.errors.forEach((error, index) => { 293 + console.log(formatError(error, index)); 294 + }); 245 295 } 246 296 } 247 297 }
+8 -1
packages/lexicon-rs/Cargo.lock
··· 267 267 268 268 [[package]] 269 269 name = "slices-lexicon" 270 - version = "0.1.3" 270 + version = "0.1.4" 271 271 dependencies = [ 272 272 "chrono", 273 273 "console_error_panic_hook", ··· 276 276 "serde", 277 277 "serde_json", 278 278 "thiserror", 279 + "unicode-segmentation", 279 280 "wasm-bindgen", 280 281 "web-sys", 281 282 ] ··· 316 317 version = "1.0.19" 317 318 source = "registry+https://github.com/rust-lang/crates.io-index" 318 319 checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 320 + 321 + [[package]] 322 + name = "unicode-segmentation" 323 + version = "1.12.0" 324 + source = "registry+https://github.com/rust-lang/crates.io-index" 325 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 319 326 320 327 [[package]] 321 328 name = "wasm-bindgen"
+4 -1
packages/lexicon-rs/Cargo.toml
··· 1 1 [package] 2 2 name = "slices-lexicon" 3 - version = "0.1.3" 3 + version = "0.1.4" 4 4 edition = "2021" 5 5 description = "AT Protocol lexicon validation library for Slices" 6 6 license = "MIT" ··· 25 25 26 26 # Regex for string format validation 27 27 regex = "1.0" 28 + 29 + # Unicode segmentation for grapheme clusters 30 + unicode-segmentation = "1.0" 28 31 29 32 # WASM bindings (optional) 30 33 wasm-bindgen = { version = "0.2", optional = true }
+20
packages/lexicon-rs/LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Slices Network 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy of 6 + this software and associated documentation files (the "Software"), to deal in 7 + the Software without restriction, including without limitation the rights to 8 + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 + the Software, and to permit persons to whom the Software is furnished to do so, 10 + subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
packages/lexicon-rs/src/errors.rs
··· 41 41 42 42 #[error("Union validation failed at {path}: no matching variant")] 43 43 UnionValidationFailed { path: String }, 44 + 45 + #[error("{}", 46 + if .errors.len() == 1 { 47 + .errors[0].clone() 48 + } else { 49 + format!("Multiple validation errors:\n{}", .errors.iter().map(|e| format!(" - {}", e)).collect::<Vec<_>>().join("\n")) 50 + } 51 + )] 52 + MultipleErrors { errors: Vec<String> }, 44 53 }
+14 -12
packages/lexicon-rs/src/lib.rs
··· 9 9 // WASM-specific code - only compiled when the wasm feature is enabled 10 10 #[cfg(feature = "wasm")] 11 11 mod wasm_bindings { 12 - use wasm_bindgen::prelude::*; 13 - use serde_json::Value; 14 12 use super::*; 13 + use serde_json::Value; 14 + use wasm_bindgen::prelude::*; 15 15 16 16 // When the `console_error_panic_hook` feature is enabled, we can call the 17 17 // `set_panic_hook` function at least once during initialization, and then ··· 40 40 let lexicons: Vec<Value> = serde_json::from_str(lexicons_json) 41 41 .map_err(|e| JsValue::from_str(&format!("Failed to parse lexicons JSON: {}", e)))?; 42 42 43 - let validator = LexiconValidator::new(lexicons) 44 - .map_err(|e| JsValue::from_str(&format!("Failed to create validator: {}", e)))?; 43 + let validator = 44 + LexiconValidator::new(lexicons).map_err(|e| JsValue::from_str(&e.to_string()))?; 45 45 46 46 Ok(WasmLexiconValidator { inner: validator }) 47 47 } ··· 55 55 56 56 self.inner 57 57 .validate_record(collection, &record) 58 - .map_err(|e| JsValue::from_str(&format!("Validation failed: {}", e))) 58 + .map_err(|e| JsValue::from_str(&e.to_string())) 59 59 } 60 60 61 61 /// Validate that all cross-lexicon references can be resolved ··· 63 63 pub fn validate_lexicon_set_completeness(&self) -> Result<(), JsValue> { 64 64 self.inner 65 65 .validate_lexicon_set_completeness() 66 - .map_err(|e| JsValue::from_str(&format!("Lexicon set validation failed: {}", e))) 66 + .map_err(|e| JsValue::from_str(&e.to_string())) 67 67 } 68 68 } 69 69 ··· 83 83 .map_err(|e| JsValue::from_str(&format!("String format validation failed: {}", e))) 84 84 } 85 85 86 - // Utility function to check if a string is a valid lexicon ID 86 + // Utility function to check if a string is a valid lexicon ID (uses shared implementation) 87 87 #[wasm_bindgen] 88 88 pub fn is_valid_nsid(nsid: &str) -> bool { 89 - use regex::Regex; 90 - let nsid_regex = Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$").unwrap(); 91 - nsid_regex.is_match(nsid) 89 + crate::is_valid_nsid(nsid) 92 90 } 93 91 } 94 92 ··· 99 97 // Non-WASM utility functions that are always available 100 98 pub fn is_valid_nsid(nsid: &str) -> bool { 101 99 use regex::Regex; 102 - let nsid_regex = Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$").unwrap(); 100 + // NSID must have at least one dot (require reverse-domain-name format) 101 + let nsid_regex = Regex::new( 102 + r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$", 103 + ) 104 + .unwrap(); 103 105 nsid_regex.is_match(nsid) 104 106 } 105 107 106 108 #[cfg(test)] 107 - mod tests; 109 + mod tests;
+573
packages/lexicon-rs/src/tests.rs
··· 294 294 }), 295 295 json!({ 296 296 "lexicon": 1, 297 + "id": "com.example.stringGraphemes", 298 + "defs": { 299 + "main": { 300 + "type": "record", 301 + "record": { 302 + "type": "object", 303 + "properties": { 304 + "emoji": { 305 + "type": "string", 306 + "minGraphemes": 1, 307 + "maxGraphemes": 3 308 + } 309 + } 310 + } 311 + } 312 + } 313 + }), 314 + json!({ 315 + "lexicon": 1, 316 + "id": "com.example.blobConstraints", 317 + "defs": { 318 + "main": { 319 + "type": "record", 320 + "record": { 321 + "type": "object", 322 + "properties": { 323 + "image": { 324 + "type": "blob", 325 + "accept": ["image/png", "image/jpeg"], 326 + "maxSize": 1000000 327 + } 328 + } 329 + } 330 + } 331 + } 332 + }), 333 + json!({ 334 + "lexicon": 1, 297 335 "id": "com.example.nsid", 298 336 "defs": { 299 337 "main": { ··· 1010 1048 // Test with complete lexicon set (should pass) 1011 1049 let complete_lexicons = vec![ 1012 1050 serde_json::json!({ 1051 + "lexicon": 1, 1013 1052 "id": "com.example.base", 1014 1053 "defs": { 1015 1054 "user": { ··· 1021 1060 } 1022 1061 }), 1023 1062 serde_json::json!({ 1063 + "lexicon": 1, 1024 1064 "id": "com.example.posts", 1025 1065 "defs": { 1026 1066 "main": { ··· 1046 1086 1047 1087 // Test with missing lexicon reference (should fail) 1048 1088 let incomplete_lexicons = vec![serde_json::json!({ 1089 + "lexicon": 1, 1049 1090 "id": "com.example.posts", 1050 1091 "defs": { 1051 1092 "main": { ··· 1176 1217 // Note: Token validation happens at the union level, not at the token definition level 1177 1218 // The union validation should catch invalid references, but for now tokens just accept anything 1178 1219 // This is consistent with how tokens work - they're type discriminators, not value validators 1220 + } 1221 + 1222 + #[test] 1223 + fn test_invalid_types_validation() { 1224 + // Test the invalid lexicon from the user's example 1225 + let invalid_lexicon = json!({ 1226 + "lexicon": 1, 1227 + "id": "com.recordcollector.album", 1228 + "defs": { 1229 + "main": { 1230 + "type": "hey", // Invalid type 1231 + "description": "A vinyl album record", 1232 + "record": { 1233 + "type": "object", 1234 + "properties": { 1235 + "title": { 1236 + "type": "whoa", // Invalid type 1237 + "description": "Album title" 1238 + }, 1239 + "artist": { 1240 + "type": "string", 1241 + "description": "Artist or band name" 1242 + } 1243 + } 1244 + } 1245 + } 1246 + } 1247 + }); 1248 + 1249 + // Validation should fail during lexicon loading due to invalid types 1250 + let result = LexiconValidator::new(vec![invalid_lexicon]); 1251 + assert!(result.is_err(), "Validator should reject lexicon with invalid types"); 1252 + 1253 + let error_msg = result.unwrap_err().to_string(); 1254 + assert!( 1255 + error_msg.contains("unknown type 'hey'") || error_msg.contains("unknown type 'whoa'"), 1256 + "Error should mention the invalid types: {}", 1257 + error_msg 1258 + ); 1259 + 1260 + // Test another invalid lexicon with invalid property type 1261 + let invalid_property_type = json!({ 1262 + "lexicon": 1, 1263 + "id": "com.example.invalid", 1264 + "defs": { 1265 + "main": { 1266 + "type": "record", 1267 + "record": { 1268 + "type": "object", 1269 + "properties": { 1270 + "badField": { 1271 + "type": "invalidType", // Invalid type 1272 + "description": "A field with invalid type" 1273 + } 1274 + } 1275 + } 1276 + } 1277 + } 1278 + }); 1279 + 1280 + let result2 = LexiconValidator::new(vec![invalid_property_type]); 1281 + assert!(result2.is_err(), "Validator should reject lexicon with invalid property types"); 1282 + 1283 + let error_msg2 = result2.unwrap_err().to_string(); 1284 + assert!( 1285 + error_msg2.contains("unknown type 'invalidType'"), 1286 + "Error should mention the invalid property type: {}", 1287 + error_msg2 1288 + ); 1289 + } 1290 + 1291 + #[test] 1292 + fn test_lexicon_document_structure_validation() { 1293 + // Test missing lexicon version field 1294 + let missing_version = json!({ 1295 + "id": "com.example.test", 1296 + "defs": { 1297 + "main": { 1298 + "type": "string" 1299 + } 1300 + } 1301 + }); 1302 + 1303 + let result = LexiconValidator::new(vec![missing_version]); 1304 + assert!(result.is_err(), "Should reject lexicon without version field"); 1305 + assert!(result.unwrap_err().to_string().contains("Missing or invalid lexicon version")); 1306 + 1307 + // Test invalid lexicon version 1308 + let invalid_version = json!({ 1309 + "lexicon": 2, 1310 + "id": "com.example.test", 1311 + "defs": { 1312 + "main": { 1313 + "type": "string" 1314 + } 1315 + } 1316 + }); 1317 + 1318 + let result = LexiconValidator::new(vec![invalid_version]); 1319 + assert!(result.is_err(), "Should reject unsupported lexicon version"); 1320 + assert!(result.unwrap_err().to_string().contains("Unsupported lexicon version: 2")); 1321 + 1322 + // Test invalid lexicon ID format 1323 + let invalid_id = json!({ 1324 + "lexicon": 1, 1325 + "id": "123invalid.nsid", // Starts with number, invalid NSID 1326 + "defs": { 1327 + "main": { 1328 + "type": "string" 1329 + } 1330 + } 1331 + }); 1332 + 1333 + let result = LexiconValidator::new(vec![invalid_id]); 1334 + assert!(result.is_err(), "Should reject invalid NSID format"); 1335 + assert!(result.unwrap_err().to_string().contains("Invalid lexicon ID format")); 1336 + 1337 + // Test valid lexicon document 1338 + let valid_lexicon = json!({ 1339 + "lexicon": 1, 1340 + "id": "com.example.test", 1341 + "defs": { 1342 + "main": { 1343 + "type": "string" 1344 + } 1345 + } 1346 + }); 1347 + 1348 + let result = LexiconValidator::new(vec![valid_lexicon]); 1349 + assert!(result.is_ok(), "Should accept valid lexicon document"); 1350 + } 1351 + 1352 + #[test] 1353 + fn test_strict_field_validation() { 1354 + // Test rejection of unknown top-level fields 1355 + let unknown_fields = json!({ 1356 + "lexicon": 1, 1357 + "id": "com.example.test", 1358 + "defs": { 1359 + "main": { 1360 + "type": "string" 1361 + } 1362 + }, 1363 + "maaain": "some random value", 1364 + "randomStuff": 42 1365 + }); 1366 + 1367 + let result = LexiconValidator::new(vec![unknown_fields]); 1368 + assert!(result.is_err(), "Should reject lexicon with unknown fields"); 1369 + let error_msg = result.unwrap_err().to_string(); 1370 + assert!( 1371 + error_msg.contains("Unrecognized key(s) in lexicon document") && 1372 + (error_msg.contains("maaain") || error_msg.contains("randomStuff")), 1373 + "Error should mention the unknown fields: {}", 1374 + error_msg 1375 + ); 1376 + 1377 + // Test that optional fields are allowed 1378 + let with_optional_fields = json!({ 1379 + "lexicon": 1, 1380 + "id": "com.example.test", 1381 + "revision": 123, 1382 + "description": "A test lexicon", 1383 + "defs": { 1384 + "main": { 1385 + "type": "string" 1386 + } 1387 + } 1388 + }); 1389 + 1390 + let result = LexiconValidator::new(vec![with_optional_fields]); 1391 + assert!(result.is_ok(), "Should accept lexicon with optional fields"); 1392 + } 1393 + 1394 + #[test] 1395 + fn test_nsid_validation_directly() { 1396 + // Test that our NSID validation now requires dots 1397 + assert!(crate::is_valid_nsid("com.example.test"), "Should accept valid NSID with dots"); 1398 + assert!(!crate::is_valid_nsid("invalid-nsid"), "Should reject NSID without dots"); 1399 + assert!(!crate::is_valid_nsid("123invalid.nsid"), "Should reject NSID starting with number"); 1400 + } 1401 + 1402 + #[test] 1403 + fn test_multiple_validation_errors() { 1404 + // Test lexicon with multiple invalid types in different definitions 1405 + let multiple_errors_lexicon = json!({ 1406 + "lexicon": 1, 1407 + "id": "com.example.test", 1408 + "defs": { 1409 + "post": { 1410 + "type": "banana", // Invalid type 1411 + "description": "A post" 1412 + }, 1413 + "comment": { 1414 + "type": "pizza", // Invalid type 1415 + "description": "A comment" 1416 + }, 1417 + "user": { 1418 + "type": "record", // Valid type 1419 + "record": { 1420 + "type": "object", 1421 + "properties": { 1422 + "name": { 1423 + "type": "string" 1424 + }, 1425 + "score": { 1426 + "type": "magical", // Invalid type in property 1427 + "description": "User score" 1428 + } 1429 + } 1430 + } 1431 + } 1432 + } 1433 + }); 1434 + 1435 + let result = LexiconValidator::new(vec![multiple_errors_lexicon]); 1436 + assert!(result.is_err(), "Should reject lexicon with multiple invalid types"); 1437 + 1438 + let error_msg = result.unwrap_err().to_string(); 1439 + 1440 + // Should contain all the invalid types in the error message 1441 + assert!(error_msg.contains("banana"), "Should mention 'banana' type error"); 1442 + assert!(error_msg.contains("pizza"), "Should mention 'pizza' type error"); 1443 + assert!(error_msg.contains("magical"), "Should mention 'magical' type error in property"); 1444 + assert!(error_msg.contains("Multiple validation errors"), "Should indicate multiple errors"); 1445 + 1446 + println!("Multiple errors captured: {}", error_msg); 1447 + } 1448 + 1449 + #[test] 1450 + fn test_multiple_property_errors() { 1451 + // Test lexicon with multiple invalid property types within a single record 1452 + let property_errors_lexicon = json!({ 1453 + "lexicon": 1, 1454 + "id": "com.example.props", 1455 + "defs": { 1456 + "main": { 1457 + "type": "record", 1458 + "record": { 1459 + "type": "object", 1460 + "properties": { 1461 + "title": { 1462 + "type": "whoa", // Invalid type 1463 + "description": "Title" 1464 + }, 1465 + "author": { 1466 + "type": "hey", // Invalid type 1467 + "description": "Author" 1468 + }, 1469 + "content": { 1470 + "type": "string" // Valid type 1471 + } 1472 + } 1473 + } 1474 + } 1475 + } 1476 + }); 1477 + 1478 + let result = LexiconValidator::new(vec![property_errors_lexicon]); 1479 + assert!(result.is_err(), "Should reject lexicon with multiple invalid property types"); 1480 + 1481 + let error_msg = result.unwrap_err().to_string(); 1482 + 1483 + // Should contain both property type errors 1484 + assert!(error_msg.contains("whoa"), "Should mention 'whoa' property type error"); 1485 + assert!(error_msg.contains("hey"), "Should mention 'hey' property type error"); 1486 + assert!(error_msg.contains("Multiple validation errors"), "Should indicate multiple errors"); 1487 + 1488 + println!("Multiple property errors captured: {}", error_msg); 1489 + } 1490 + 1491 + #[test] 1492 + fn test_grapheme_validation() { 1493 + let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 1494 + 1495 + // Valid emoji within grapheme limit 1496 + let valid_emoji = json!({ "emoji": "👨‍👩‍👧‍👦" }); // 1 grapheme (family emoji) 1497 + assert!(validator.validate_record("com.example.stringGraphemes", &valid_emoji).is_ok()); 1498 + 1499 + // Too many graphemes 1500 + let too_many_emoji = json!({ "emoji": "🎉🎊🎈🎁" }); // 4 graphemes > max 3 1501 + assert!(validator.validate_record("com.example.stringGraphemes", &too_many_emoji).is_err()); 1502 + 1503 + // Edge case: exactly at limit 1504 + let at_limit = json!({ "emoji": "👍👎👌" }); // exactly 3 graphemes 1505 + assert!(validator.validate_record("com.example.stringGraphemes", &at_limit).is_ok()); 1506 + } 1507 + 1508 + #[test] 1509 + fn test_blob_validation() { 1510 + let validator = LexiconValidator::new(get_test_lexicons()).unwrap(); 1511 + 1512 + // Valid blob with accepted MIME type and size 1513 + let valid_blob = json!({ 1514 + "image": { 1515 + "$type": "blob", 1516 + "ref": { "$link": "bafytest123" }, 1517 + "mimeType": "image/png", 1518 + "size": 500000 1519 + } 1520 + }); 1521 + assert!(validator.validate_record("com.example.blobConstraints", &valid_blob).is_ok()); 1522 + 1523 + // Invalid MIME type 1524 + let invalid_mime = json!({ 1525 + "image": { 1526 + "$type": "blob", 1527 + "ref": { "$link": "bafytest123" }, 1528 + "mimeType": "image/gif", // not in accept list 1529 + "size": 500000 1530 + } 1531 + }); 1532 + assert!(validator.validate_record("com.example.blobConstraints", &invalid_mime).is_err()); 1533 + 1534 + // Size too large 1535 + let too_large = json!({ 1536 + "image": { 1537 + "$type": "blob", 1538 + "ref": { "$link": "bafytest123" }, 1539 + "mimeType": "image/png", 1540 + "size": 2000000 // exceeds maxSize 1541 + } 1542 + }); 1543 + assert!(validator.validate_record("com.example.blobConstraints", &too_large).is_err()); 1544 + } 1545 + 1546 + #[test] 1547 + fn test_structural_validation() { 1548 + // Test AT Protocol structural validation rules 1549 + 1550 + // Test array without items field 1551 + let array_no_items = json!({ 1552 + "lexicon": 1, 1553 + "id": "com.example.bad", 1554 + "defs": { 1555 + "main": { 1556 + "type": "array" 1557 + // Missing required "items" field 1558 + } 1559 + } 1560 + }); 1561 + let result = LexiconValidator::new(vec![array_no_items]); 1562 + assert!(result.is_err()); 1563 + assert!(result.unwrap_err().to_string().contains("must have 'items' field")); 1564 + 1565 + // Test union without refs field 1566 + let union_no_refs = json!({ 1567 + "lexicon": 1, 1568 + "id": "com.example.bad", 1569 + "defs": { 1570 + "main": { 1571 + "type": "union" 1572 + // Missing required "refs" field 1573 + } 1574 + } 1575 + }); 1576 + let result = LexiconValidator::new(vec![union_no_refs]); 1577 + assert!(result.is_err()); 1578 + assert!(result.unwrap_err().to_string().contains("must have 'refs' field")); 1579 + 1580 + // Test ref without ref field 1581 + let ref_no_ref = json!({ 1582 + "lexicon": 1, 1583 + "id": "com.example.bad", 1584 + "defs": { 1585 + "main": { 1586 + "type": "ref" 1587 + // Missing required "ref" field 1588 + } 1589 + } 1590 + }); 1591 + let result = LexiconValidator::new(vec![ref_no_ref]); 1592 + assert!(result.is_err()); 1593 + assert!(result.unwrap_err().to_string().contains("must have 'ref' field")); 1594 + 1595 + // Test object with required field not in properties 1596 + let object_bad_required = json!({ 1597 + "lexicon": 1, 1598 + "id": "com.example.bad", 1599 + "defs": { 1600 + "main": { 1601 + "type": "object", 1602 + "required": ["name", "missing"], 1603 + "properties": { 1604 + "name": { "type": "string" } 1605 + // "missing" field not in properties 1606 + } 1607 + } 1608 + } 1609 + }); 1610 + let result = LexiconValidator::new(vec![object_bad_required]); 1611 + assert!(result.is_err()); 1612 + assert!(result.unwrap_err().to_string().contains("required field 'missing' not found in properties")); 1613 + 1614 + // Test valid structural definitions 1615 + let valid_structural = json!({ 1616 + "lexicon": 1, 1617 + "id": "com.example.good", 1618 + "defs": { 1619 + "myArray": { 1620 + "type": "array", 1621 + "items": { "type": "string" } 1622 + }, 1623 + "myUnion": { 1624 + "type": "union", 1625 + "refs": ["#myArray", "#myObject"] 1626 + }, 1627 + "myRef": { 1628 + "type": "ref", 1629 + "ref": "#myObject" 1630 + }, 1631 + "myObject": { 1632 + "type": "object", 1633 + "required": ["name"], 1634 + "properties": { 1635 + "name": { "type": "string" }, 1636 + "optional": { "type": "integer" } 1637 + } 1638 + } 1639 + } 1640 + }); 1641 + let result = LexiconValidator::new(vec![valid_structural]); 1642 + assert!(result.is_ok(), "Valid structural definitions should pass"); 1643 + } 1644 + 1645 + #[test] 1646 + fn test_default_validation() { 1647 + // Test that invalid defaults are caught during lexicon compilation 1648 + 1649 + // String default violating minLength 1650 + let string_minlength_violation = json!({ 1651 + "lexicon": 1, 1652 + "id": "com.example.stringMinLengthDefault", 1653 + "defs": { 1654 + "main": { 1655 + "type": "record", 1656 + "record": { 1657 + "type": "object", 1658 + "properties": { 1659 + "name": { 1660 + "type": "string", 1661 + "minLength": 5, 1662 + "default": "hi" 1663 + } 1664 + } 1665 + } 1666 + } 1667 + } 1668 + }); 1669 + let result = LexiconValidator::new(vec![string_minlength_violation]); 1670 + assert!(result.is_err()); 1671 + assert!(result.unwrap_err().to_string().contains("default value 'hi' length 2 is less than minimum 5")); 1672 + 1673 + // String default violating enum 1674 + let string_enum_violation = json!({ 1675 + "lexicon": 1, 1676 + "id": "com.example.stringEnumDefault", 1677 + "defs": { 1678 + "main": { 1679 + "type": "record", 1680 + "record": { 1681 + "type": "object", 1682 + "properties": { 1683 + "color": { 1684 + "type": "string", 1685 + "enum": ["red", "blue", "green"], 1686 + "default": "yellow" 1687 + } 1688 + } 1689 + } 1690 + } 1691 + } 1692 + }); 1693 + let result = LexiconValidator::new(vec![string_enum_violation]); 1694 + assert!(result.is_err()); 1695 + assert!(result.unwrap_err().to_string().contains("default value 'yellow' must be one of (red|blue|green)")); 1696 + 1697 + // Integer default violating minimum 1698 + let integer_minimum_violation = json!({ 1699 + "lexicon": 1, 1700 + "id": "com.example.integerMinimumDefault", 1701 + "defs": { 1702 + "main": { 1703 + "type": "record", 1704 + "record": { 1705 + "type": "object", 1706 + "properties": { 1707 + "age": { 1708 + "type": "integer", 1709 + "minimum": 10, 1710 + "default": 5 1711 + } 1712 + } 1713 + } 1714 + } 1715 + } 1716 + }); 1717 + let result = LexiconValidator::new(vec![integer_minimum_violation]); 1718 + assert!(result.is_err()); 1719 + assert!(result.unwrap_err().to_string().contains("default value 5 is less than minimum 10")); 1720 + 1721 + // Boolean default violating const 1722 + let boolean_const_violation = json!({ 1723 + "lexicon": 1, 1724 + "id": "com.example.booleanConstDefault", 1725 + "defs": { 1726 + "main": { 1727 + "type": "record", 1728 + "record": { 1729 + "type": "object", 1730 + "properties": { 1731 + "enabled": { 1732 + "type": "boolean", 1733 + "const": true, 1734 + "default": false 1735 + } 1736 + } 1737 + } 1738 + } 1739 + } 1740 + }); 1741 + let result = LexiconValidator::new(vec![boolean_const_violation]); 1742 + assert!(result.is_err()); 1743 + assert!(result.unwrap_err().to_string().contains("default value false does not match const value true")); 1744 + 1745 + // Test that valid defaults pass - the existing kitchen sink includes com.example.validDefaults 1746 + let lexicons = get_test_lexicons(); 1747 + let result = LexiconValidator::new(lexicons); 1748 + match result { 1749 + Ok(_) => {}, // Success 1750 + Err(e) => panic!("Kitchen sink with valid defaults should pass validation, but got error: {}", e), 1751 + } 1179 1752 }
+196
packages/lexicon-rs/src/types.rs
··· 49 49 } 50 50 } 51 51 52 + /// String constraints for lexicon string types 53 + #[derive(Debug, Clone, Default)] 54 + pub struct StringConstraints { 55 + pub min_length: Option<u32>, 56 + pub max_length: Option<u32>, 57 + pub min_graphemes: Option<u32>, 58 + pub max_graphemes: Option<u32>, 59 + pub enum_values: Option<Vec<String>>, 60 + pub const_value: Option<String>, 61 + pub default: Option<String>, 62 + pub format: Option<StringFormat>, 63 + pub known_values: Option<Vec<String>>, 64 + } 65 + 66 + /// Integer constraints for lexicon integer types 67 + #[derive(Debug, Clone, Default)] 68 + pub struct IntegerConstraints { 69 + pub minimum: Option<i64>, 70 + pub maximum: Option<i64>, 71 + pub enum_values: Option<Vec<i64>>, 72 + pub const_value: Option<i64>, 73 + pub default: Option<i64>, 74 + } 75 + 76 + /// Array constraints for lexicon array types 77 + #[derive(Debug, Clone, Default)] 78 + pub struct ArrayConstraints { 79 + pub min_length: Option<u32>, 80 + pub max_length: Option<u32>, 81 + } 82 + 83 + /// Boolean constraints for lexicon boolean types 84 + #[derive(Debug, Clone, Default)] 85 + pub struct BooleanConstraints { 86 + pub const_value: Option<bool>, 87 + pub default: Option<bool>, 88 + } 89 + 90 + /// Bytes constraints for lexicon bytes types 91 + #[derive(Debug, Clone, Default)] 92 + pub struct BytesConstraints { 93 + pub min_length: Option<u32>, 94 + pub max_length: Option<u32>, 95 + } 96 + 97 + /// Blob constraints for lexicon blob types 98 + #[derive(Debug, Clone, Default)] 99 + pub struct BlobConstraints { 100 + pub accept: Option<Vec<String>>, 101 + pub max_size: Option<u64>, 102 + } 103 + 52 104 /// Validation context to track the current path in the object being validated 53 105 #[derive(Debug, Clone)] 54 106 pub struct ValidationContext { 55 107 pub path: Vec<String>, 108 + } 109 + 110 + impl StringConstraints { 111 + pub fn from_json(value: &Value) -> Self { 112 + let mut constraints = Self::default(); 113 + 114 + if let Some(min_len) = value.get("minLength").and_then(|v| v.as_u64()) { 115 + constraints.min_length = Some(min_len as u32); 116 + } 117 + if let Some(max_len) = value.get("maxLength").and_then(|v| v.as_u64()) { 118 + constraints.max_length = Some(max_len as u32); 119 + } 120 + if let Some(min_graphemes) = value.get("minGraphemes").and_then(|v| v.as_u64()) { 121 + constraints.min_graphemes = Some(min_graphemes as u32); 122 + } 123 + if let Some(max_graphemes) = value.get("maxGraphemes").and_then(|v| v.as_u64()) { 124 + constraints.max_graphemes = Some(max_graphemes as u32); 125 + } 126 + if let Some(format_str) = value.get("format").and_then(|v| v.as_str()) { 127 + constraints.format = StringFormat::from_str(format_str); 128 + } 129 + if let Some(const_val) = value.get("const").and_then(|v| v.as_str()) { 130 + constraints.const_value = Some(const_val.to_string()); 131 + } 132 + if let Some(default_val) = value.get("default").and_then(|v| v.as_str()) { 133 + constraints.default = Some(default_val.to_string()); 134 + } 135 + if let Some(enum_array) = value.get("enum").and_then(|v| v.as_array()) { 136 + let enum_values: Vec<String> = enum_array 137 + .iter() 138 + .filter_map(|v| v.as_str().map(|s| s.to_string())) 139 + .collect(); 140 + if !enum_values.is_empty() { 141 + constraints.enum_values = Some(enum_values); 142 + } 143 + } 144 + if let Some(known_array) = value.get("knownValues").and_then(|v| v.as_array()) { 145 + let known_values: Vec<String> = known_array 146 + .iter() 147 + .filter_map(|v| v.as_str().map(|s| s.to_string())) 148 + .collect(); 149 + if !known_values.is_empty() { 150 + constraints.known_values = Some(known_values); 151 + } 152 + } 153 + 154 + constraints 155 + } 156 + } 157 + 158 + impl IntegerConstraints { 159 + pub fn from_json(value: &Value) -> Self { 160 + let mut constraints = Self::default(); 161 + 162 + if let Some(min) = value.get("minimum").and_then(|v| v.as_i64()) { 163 + constraints.minimum = Some(min); 164 + } 165 + if let Some(max) = value.get("maximum").and_then(|v| v.as_i64()) { 166 + constraints.maximum = Some(max); 167 + } 168 + if let Some(const_val) = value.get("const").and_then(|v| v.as_i64()) { 169 + constraints.const_value = Some(const_val); 170 + } 171 + if let Some(default_val) = value.get("default").and_then(|v| v.as_i64()) { 172 + constraints.default = Some(default_val); 173 + } 174 + if let Some(enum_array) = value.get("enum").and_then(|v| v.as_array()) { 175 + let enum_values: Vec<i64> = enum_array 176 + .iter() 177 + .filter_map(|v| v.as_i64()) 178 + .collect(); 179 + if !enum_values.is_empty() { 180 + constraints.enum_values = Some(enum_values); 181 + } 182 + } 183 + 184 + constraints 185 + } 186 + } 187 + 188 + impl ArrayConstraints { 189 + pub fn from_json(value: &Value) -> Self { 190 + let mut constraints = Self::default(); 191 + 192 + if let Some(min_len) = value.get("minLength").and_then(|v| v.as_u64()) { 193 + constraints.min_length = Some(min_len as u32); 194 + } 195 + if let Some(max_len) = value.get("maxLength").and_then(|v| v.as_u64()) { 196 + constraints.max_length = Some(max_len as u32); 197 + } 198 + 199 + constraints 200 + } 201 + } 202 + 203 + impl BooleanConstraints { 204 + pub fn from_json(value: &Value) -> Self { 205 + let mut constraints = Self::default(); 206 + 207 + if let Some(const_val) = value.get("const").and_then(|v| v.as_bool()) { 208 + constraints.const_value = Some(const_val); 209 + } 210 + if let Some(default_val) = value.get("default").and_then(|v| v.as_bool()) { 211 + constraints.default = Some(default_val); 212 + } 213 + 214 + constraints 215 + } 216 + } 217 + 218 + impl BytesConstraints { 219 + pub fn from_json(value: &Value) -> Self { 220 + let mut constraints = Self::default(); 221 + 222 + if let Some(min_len) = value.get("minLength").and_then(|v| v.as_u64()) { 223 + constraints.min_length = Some(min_len as u32); 224 + } 225 + if let Some(max_len) = value.get("maxLength").and_then(|v| v.as_u64()) { 226 + constraints.max_length = Some(max_len as u32); 227 + } 228 + 229 + constraints 230 + } 231 + } 232 + 233 + impl BlobConstraints { 234 + pub fn from_json(value: &Value) -> Self { 235 + let mut constraints = Self::default(); 236 + 237 + if let Some(accept_array) = value.get("accept").and_then(|v| v.as_array()) { 238 + let accept_values: Vec<String> = accept_array 239 + .iter() 240 + .filter_map(|v| v.as_str().map(|s| s.to_string())) 241 + .collect(); 242 + if !accept_values.is_empty() { 243 + constraints.accept = Some(accept_values); 244 + } 245 + } 246 + if let Some(max_size) = value.get("maxSize").and_then(|v| v.as_u64()) { 247 + constraints.max_size = Some(max_size); 248 + } 249 + 250 + constraints 251 + } 56 252 } 57 253 58 254 impl ValidationContext {
+1216 -215
packages/lexicon-rs/src/validator.rs
··· 2 2 use regex::Regex; 3 3 use serde_json::Value; 4 4 use std::collections::HashMap; 5 + use unicode_segmentation::UnicodeSegmentation; 5 6 6 7 use super::errors::ValidationError; 7 - use super::types::{LexiconDoc, StringFormat, ValidationContext}; 8 + use super::types::{ 9 + ArrayConstraints, BlobConstraints, BooleanConstraints, BytesConstraints, IntegerConstraints, 10 + LexiconDoc, StringConstraints, StringFormat, ValidationContext 11 + }; 8 12 9 - #[derive(Clone)] 13 + #[derive(Clone, Debug)] 10 14 pub struct LexiconValidator { 11 15 lexicons: HashMap<String, LexiconDoc>, 12 16 } ··· 17 21 let mut lexicon_map = HashMap::new(); 18 22 19 23 for lexicon_value in lexicons { 24 + // Validate that only allowed top-level fields are present 25 + if let Some(obj) = lexicon_value.as_object() { 26 + let allowed_fields = ["lexicon", "id", "defs", "revision", "description"]; 27 + let mut unknown_fields = Vec::new(); 28 + 29 + for key in obj.keys() { 30 + if !allowed_fields.contains(&key.as_str()) { 31 + unknown_fields.push(key.clone()); 32 + } 33 + } 34 + 35 + if !unknown_fields.is_empty() { 36 + return Err(ValidationError::InvalidSchema(format!( 37 + "Unrecognized key(s) in lexicon document: {}", 38 + unknown_fields.join(", ") 39 + ))); 40 + } 41 + } 42 + 43 + // Validate lexicon version field 44 + let lexicon_version = lexicon_value["lexicon"] 45 + .as_u64() 46 + .ok_or_else(|| ValidationError::InvalidSchema("Missing or invalid lexicon version field".to_string()))?; 47 + 48 + if lexicon_version != 1 { 49 + return Err(ValidationError::InvalidSchema(format!( 50 + "Unsupported lexicon version: {}. Only version 1 is supported.", 51 + lexicon_version 52 + ))); 53 + } 54 + 20 55 let id = lexicon_value["id"] 21 56 .as_str() 22 57 .ok_or_else(|| ValidationError::InvalidSchema("Missing lexicon id".to_string()))? 23 58 .to_string(); 24 59 60 + // Validate lexicon ID format (NSID) 61 + if !crate::is_valid_nsid(&id) { 62 + return Err(ValidationError::InvalidSchema(format!( 63 + "Invalid lexicon ID format: '{}'. Must be a valid NSID.", 64 + id 65 + ))); 66 + } 67 + 25 68 let defs = lexicon_value["defs"].clone(); 26 69 if defs.is_null() { 27 70 return Err(ValidationError::InvalidSchema(format!( ··· 30 73 ))); 31 74 } 32 75 33 - lexicon_map.insert(id.clone(), LexiconDoc { id, defs }); 76 + let lexicon_doc = LexiconDoc { id: id.clone(), defs }; 77 + 78 + // Validate the lexicon definitions immediately upon loading 79 + Self::validate_lexicon_definitions_static(&lexicon_doc.defs)?; 80 + 81 + lexicon_map.insert(id, lexicon_doc); 34 82 } 35 83 36 84 Ok(Self { ··· 103 151 Ok(()) 104 152 } 105 153 154 + /// Static version of lexicon definitions validation for use during loading 155 + fn validate_lexicon_definitions_static(definitions: &Value) -> Result<(), ValidationError> { 156 + let definitions_obj = definitions.as_object().ok_or_else(|| { 157 + ValidationError::InvalidSchema("Lexicon definitions must be a JSON object".to_string()) 158 + })?; 159 + 160 + let mut errors = Vec::new(); 161 + 162 + // Each key should be a valid definition name, each value should be a valid definition 163 + for (def_name, def_value) in definitions_obj { 164 + // Validate definition name (should be camelCase identifier - letters and numbers only) 165 + if def_name.is_empty() || !def_name.chars().all(|c| c.is_ascii_alphanumeric()) { 166 + errors.push(format!( 167 + "Invalid definition name '{}': must be camelCase (letters and numbers only)", 168 + def_name 169 + )); 170 + continue; 171 + } 172 + 173 + // Validate definition structure 174 + let def_type = match def_value.get("type").and_then(|v| v.as_str()) { 175 + Some(t) => t, 176 + None => { 177 + errors.push(format!( 178 + "Definition '{}' missing required 'type' field", 179 + def_name 180 + )); 181 + continue; 182 + } 183 + }; 184 + 185 + // Validate based on definition type 186 + match def_type { 187 + "record" => { 188 + if let Err(e) = Self::validate_record_definition_static(def_name, def_value) { 189 + errors.push(e.to_string()); 190 + } 191 + } 192 + "object" => { 193 + if let Err(e) = Self::validate_object_definition_static(def_name, def_value) { 194 + errors.push(e.to_string()); 195 + } 196 + } 197 + "string" | "integer" | "boolean" | "array" | "union" | "ref" | "blob" | "bytes" 198 + | "cid-link" | "unknown" | "token" | "null" => { 199 + // Basic types are valid, could add more specific validation here 200 + if let Err(e) = Self::validate_type_definition_static(def_name, def_value, def_type) { 201 + errors.push(e.to_string()); 202 + } 203 + } 204 + _ => { 205 + errors.push(format!( 206 + "Definition '{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null", 207 + def_name, def_type 208 + )); 209 + } 210 + } 211 + } 212 + 213 + if errors.is_empty() { 214 + Ok(()) 215 + } else { 216 + Err(ValidationError::MultipleErrors { errors }) 217 + } 218 + } 219 + 106 220 /// Validate that a JSON value contains valid lexicon definitions 107 221 fn validate_lexicon_definitions(&self, definitions: &Value) -> Result<(), ValidationError> { 108 222 let definitions_obj = definitions.as_object().ok_or_else(|| { ··· 135 249 "record" => self.validate_record_definition(def_name, def_value)?, 136 250 "object" => self.validate_object_definition(def_name, def_value)?, 137 251 "string" | "integer" | "boolean" | "array" | "union" | "ref" | "blob" | "bytes" 138 - | "cid-link" | "unknown" | "token" => { 252 + | "cid-link" | "unknown" | "token" | "null" => { 139 253 // Basic types are valid, could add more specific validation here 254 + self.validate_type_definition(def_name, def_value, def_type)?; 140 255 } 141 256 _ => { 142 257 return Err(ValidationError::InvalidSchema(format!( 143 - "Definition '{}' has unknown type '{}'", 258 + "Definition '{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null", 144 259 def_name, def_type 145 260 ))); 146 261 } ··· 180 295 def_name 181 296 ))); 182 297 } 298 + 299 + // Validate each property's type 300 + for (prop_name, prop_def) in properties.as_object().unwrap() { 301 + self.validate_property_definition(def_name, prop_name, prop_def)?; 302 + } 183 303 } 184 304 185 305 Ok(()) 186 306 } 187 307 308 + /// Static version of record definition validation 309 + fn validate_record_definition_static( 310 + def_name: &str, 311 + def_value: &Value, 312 + ) -> Result<(), ValidationError> { 313 + let mut errors = Vec::new(); 314 + 315 + // Validate allowed fields for record definition 316 + let allowed_record_fields = [ 317 + "type", "record", "description", "key" 318 + ]; 319 + Self::validate_allowed_fields(def_name, def_value, &allowed_record_fields, "Record", &mut errors); 320 + 321 + // Record definitions should have a "record" field 322 + let record_def = match def_value.get("record") { 323 + Some(r) => r, 324 + None => { 325 + return Err(ValidationError::InvalidSchema(format!( 326 + "Record definition '{}' missing 'record' field", 327 + def_name 328 + ))); 329 + } 330 + }; 331 + 332 + // The record field should be an object type 333 + if record_def.get("type").and_then(|v| v.as_str()) != Some("object") { 334 + errors.push(format!( 335 + "Record definition '{}' record field must be type 'object'", 336 + def_name 337 + )); 338 + } 339 + 340 + // Validate allowed fields for the nested record object 341 + let allowed_object_fields = [ 342 + "type", "properties", "required", "nullable", "description" 343 + ]; 344 + Self::validate_allowed_fields(&format!("{}.record", def_name), record_def, &allowed_object_fields, "Object", &mut errors); 345 + 346 + // Validate properties if they exist 347 + if let Some(properties) = record_def.get("properties") { 348 + if !properties.is_object() { 349 + errors.push(format!( 350 + "Record definition '{}' properties must be an object", 351 + def_name 352 + )); 353 + } else { 354 + // Validate each property's type 355 + for (prop_name, prop_def) in properties.as_object().unwrap() { 356 + Self::validate_property_definition_static(def_name, prop_name, prop_def, &mut errors); 357 + } 358 + } 359 + } 360 + 361 + // Validate required field if it exists 362 + if let Some(required) = record_def.get("required") { 363 + if !required.is_array() { 364 + errors.push(format!( 365 + "Record definition '{}' required field must be an array", 366 + def_name 367 + )); 368 + } else { 369 + // Check that all required fields exist in properties 370 + if let Some(properties) = record_def.get("properties").and_then(|p| p.as_object()) { 371 + for req_field in required.as_array().unwrap() { 372 + if let Some(field_name) = req_field.as_str() { 373 + if !properties.contains_key(field_name) { 374 + errors.push(format!( 375 + "Record definition '{}' required field '{}' not found in properties", 376 + def_name, field_name 377 + )); 378 + } 379 + } 380 + } 381 + } 382 + } 383 + } 384 + 385 + if errors.is_empty() { 386 + Ok(()) 387 + } else { 388 + Err(ValidationError::MultipleErrors { errors }) 389 + } 390 + } 391 + 392 + /// Static version of object definition validation 393 + fn validate_object_definition_static( 394 + def_name: &str, 395 + def_value: &Value, 396 + ) -> Result<(), ValidationError> { 397 + let mut errors = Vec::new(); 398 + 399 + // Validate allowed fields 400 + let allowed_object_fields = [ 401 + "type", "properties", "required", "nullable", "description" 402 + ]; 403 + Self::validate_allowed_fields(def_name, def_value, &allowed_object_fields, "Object", &mut errors); 404 + 405 + // Object definitions should have properties 406 + if let Some(properties) = def_value.get("properties") { 407 + if !properties.is_object() { 408 + errors.push(format!( 409 + "Object definition '{}' properties must be an object", 410 + def_name 411 + )); 412 + } else { 413 + // Validate each property's type 414 + for (prop_name, prop_def) in properties.as_object().unwrap() { 415 + Self::validate_property_definition_static(def_name, prop_name, prop_def, &mut errors); 416 + } 417 + } 418 + } 419 + 420 + // Validate required field if it exists 421 + if let Some(required) = def_value.get("required") { 422 + if !required.is_array() { 423 + errors.push(format!( 424 + "Object definition '{}' required field must be an array", 425 + def_name 426 + )); 427 + } else { 428 + // Check that all required fields exist in properties 429 + if let Some(properties) = def_value.get("properties").and_then(|p| p.as_object()) { 430 + for req_field in required.as_array().unwrap() { 431 + if let Some(field_name) = req_field.as_str() { 432 + if !properties.contains_key(field_name) { 433 + errors.push(format!( 434 + "Object definition '{}' required field '{}' not found in properties", 435 + def_name, field_name 436 + )); 437 + } 438 + } 439 + } 440 + } 441 + } 442 + } 443 + 444 + // Validate nullable field if present 445 + if let Some(nullable) = def_value.get("nullable") { 446 + if !nullable.is_array() { 447 + errors.push(format!( 448 + "Object definition '{}' nullable field must be an array", 449 + def_name 450 + )); 451 + } 452 + } 453 + 454 + if errors.is_empty() { 455 + Ok(()) 456 + } else { 457 + Err(ValidationError::MultipleErrors { errors }) 458 + } 459 + } 460 + 461 + /// Validate that only allowed fields are present in a definition 462 + fn validate_allowed_fields( 463 + def_name: &str, 464 + def_value: &Value, 465 + allowed_fields: &[&str], 466 + def_type: &str, 467 + errors: &mut Vec<String>, 468 + ) { 469 + if let Some(obj) = def_value.as_object() { 470 + let mut unknown_fields = Vec::new(); 471 + 472 + for key in obj.keys() { 473 + if !allowed_fields.contains(&key.as_str()) { 474 + unknown_fields.push(key.clone()); 475 + } 476 + } 477 + 478 + if !unknown_fields.is_empty() { 479 + errors.push(format!( 480 + "{} definition '{}' has unknown field(s): {}. Allowed fields are: {}", 481 + def_type, 482 + def_name, 483 + unknown_fields.join(", "), 484 + allowed_fields.join(", ") 485 + )); 486 + } 487 + } 488 + } 489 + 490 + /// Static version of type definition validation with AT Protocol structural rules 491 + fn validate_type_definition_static( 492 + def_name: &str, 493 + def_value: &Value, 494 + type_name: &str, 495 + ) -> Result<(), ValidationError> { 496 + let mut errors = Vec::new(); 497 + 498 + // Apply AT Protocol structural validation rules 499 + match type_name { 500 + "object" => { 501 + let allowed_object_fields = [ 502 + "type", "properties", "required", "nullable", "description" 503 + ]; 504 + Self::validate_allowed_fields(def_name, def_value, &allowed_object_fields, "Object", &mut errors); 505 + 506 + // Object type should have properties field 507 + if let Some(properties) = def_value.get("properties") { 508 + if !properties.is_object() { 509 + errors.push(format!( 510 + "Object definition '{}' properties must be an object", 511 + def_name 512 + )); 513 + } else { 514 + // Validate each property's type 515 + for (prop_name, prop_def) in properties.as_object().unwrap() { 516 + Self::validate_property_definition_static(def_name, prop_name, prop_def, &mut errors); 517 + } 518 + } 519 + } 520 + 521 + // Validate required field if present 522 + if let Some(required) = def_value.get("required") { 523 + if !required.is_array() { 524 + errors.push(format!( 525 + "Object definition '{}' required field must be an array", 526 + def_name 527 + )); 528 + } else { 529 + // Check that all required fields exist in properties 530 + if let Some(properties) = def_value.get("properties").and_then(|p| p.as_object()) { 531 + for req_field in required.as_array().unwrap() { 532 + if let Some(field_name) = req_field.as_str() { 533 + if !properties.contains_key(field_name) { 534 + errors.push(format!( 535 + "Object definition '{}' required field '{}' not found in properties", 536 + def_name, field_name 537 + )); 538 + } 539 + } 540 + } 541 + } 542 + } 543 + } 544 + 545 + // Validate nullable field if present 546 + if let Some(nullable) = def_value.get("nullable") { 547 + if !nullable.is_array() { 548 + errors.push(format!( 549 + "Object definition '{}' nullable field must be an array", 550 + def_name 551 + )); 552 + } 553 + } 554 + }, 555 + "array" => { 556 + // Array type must have items field 557 + if def_value.get("items").is_none() { 558 + errors.push(format!( 559 + "Array definition '{}' must have 'items' field", 560 + def_name 561 + )); 562 + } else { 563 + let items = &def_value["items"]; 564 + // Validate that items has a type 565 + if !items.get("type").and_then(|t| t.as_str()).is_some() && 566 + !items.get("ref").and_then(|r| r.as_str()).is_some() { 567 + errors.push(format!( 568 + "Array definition '{}' items must have 'type' or 'ref' field", 569 + def_name 570 + )); 571 + } 572 + } 573 + }, 574 + "union" => { 575 + // Union type must have refs field 576 + if def_value.get("refs").is_none() { 577 + errors.push(format!( 578 + "Union definition '{}' must have 'refs' field", 579 + def_name 580 + )); 581 + } else { 582 + let refs = &def_value["refs"]; 583 + if !refs.is_array() { 584 + errors.push(format!( 585 + "Union definition '{}' refs field must be an array", 586 + def_name 587 + )); 588 + } else if refs.as_array().unwrap().is_empty() { 589 + errors.push(format!( 590 + "Union definition '{}' refs field must not be empty", 591 + def_name 592 + )); 593 + } 594 + } 595 + }, 596 + "ref" => { 597 + // Reference type must have ref field 598 + if def_value.get("ref").is_none() { 599 + errors.push(format!( 600 + "Reference definition '{}' must have 'ref' field", 601 + def_name 602 + )); 603 + } else { 604 + let ref_val = &def_value["ref"]; 605 + if !ref_val.is_string() { 606 + errors.push(format!( 607 + "Reference definition '{}' ref field must be a string", 608 + def_name 609 + )); 610 + } 611 + } 612 + }, 613 + "blob" => { 614 + // Blob type constraints are validated at runtime, not at definition time 615 + // But we can validate accept and maxSize field types if present 616 + if let Some(accept) = def_value.get("accept") { 617 + if !accept.is_array() { 618 + errors.push(format!( 619 + "Blob definition '{}' accept field must be an array", 620 + def_name 621 + )); 622 + } 623 + } 624 + if let Some(max_size) = def_value.get("maxSize") { 625 + if !max_size.is_number() { 626 + errors.push(format!( 627 + "Blob definition '{}' maxSize field must be a number", 628 + def_name 629 + )); 630 + } 631 + } 632 + }, 633 + "string" => { 634 + // Validate string constraint field types 635 + if let Some(min_len) = def_value.get("minLength") { 636 + if !min_len.is_number() { 637 + errors.push(format!( 638 + "String definition '{}' minLength must be a number", 639 + def_name 640 + )); 641 + } 642 + } 643 + if let Some(max_len) = def_value.get("maxLength") { 644 + if !max_len.is_number() { 645 + errors.push(format!( 646 + "String definition '{}' maxLength must be a number", 647 + def_name 648 + )); 649 + } 650 + } 651 + if let Some(enum_val) = def_value.get("enum") { 652 + if !enum_val.is_array() { 653 + errors.push(format!( 654 + "String definition '{}' enum field must be an array", 655 + def_name 656 + )); 657 + } 658 + } 659 + if let Some(format) = def_value.get("format") { 660 + if !format.is_string() { 661 + errors.push(format!( 662 + "String definition '{}' format field must be a string", 663 + def_name 664 + )); 665 + } else if StringFormat::from_str(format.as_str().unwrap()).is_none() { 666 + errors.push(format!( 667 + "String definition '{}' has unknown format '{}'", 668 + def_name, format.as_str().unwrap() 669 + )); 670 + } 671 + } 672 + }, 673 + "integer" => { 674 + // Validate integer constraint field types 675 + if let Some(min) = def_value.get("minimum") { 676 + if !min.is_number() { 677 + errors.push(format!( 678 + "Integer definition '{}' minimum must be a number", 679 + def_name 680 + )); 681 + } 682 + } 683 + if let Some(max) = def_value.get("maximum") { 684 + if !max.is_number() { 685 + errors.push(format!( 686 + "Integer definition '{}' maximum must be a number", 687 + def_name 688 + )); 689 + } 690 + } 691 + if let Some(enum_val) = def_value.get("enum") { 692 + if !enum_val.is_array() { 693 + errors.push(format!( 694 + "Integer definition '{}' enum field must be an array", 695 + def_name 696 + )); 697 + } 698 + } 699 + }, 700 + "bytes" => { 701 + // Validate bytes constraint field types 702 + if let Some(min_len) = def_value.get("minLength") { 703 + if !min_len.is_number() { 704 + errors.push(format!( 705 + "Bytes definition '{}' minLength must be a number", 706 + def_name 707 + )); 708 + } 709 + } 710 + if let Some(max_len) = def_value.get("maxLength") { 711 + if !max_len.is_number() { 712 + errors.push(format!( 713 + "Bytes definition '{}' maxLength must be a number", 714 + def_name 715 + )); 716 + } 717 + } 718 + }, 719 + // token, null, unknown, cid-link, boolean don't require special structural validation 720 + _ => {} 721 + } 722 + 723 + if errors.is_empty() { 724 + Ok(()) 725 + } else { 726 + Err(ValidationError::MultipleErrors { errors }) 727 + } 728 + } 729 + 730 + /// Static version of property definition validation 731 + fn validate_property_definition_static( 732 + parent_name: &str, 733 + prop_name: &str, 734 + prop_def: &Value, 735 + errors: &mut Vec<String>, 736 + ) { 737 + if let Some(prop_type) = prop_def.get("type").and_then(|v| v.as_str()) { 738 + // Check if the property type is valid and validate allowed fields for each type 739 + match prop_type { 740 + "string" => { 741 + let allowed_fields = [ 742 + "type", "description", "default", "const", "enum", "format", 743 + "minLength", "maxLength", "minGraphemes", "maxGraphemes", "knownValues" 744 + ]; 745 + Self::validate_allowed_fields( 746 + &format!("{}.{}", parent_name, prop_name), 747 + prop_def, 748 + &allowed_fields, 749 + "String property", 750 + errors 751 + ); 752 + 753 + // Validate default value against constraints 754 + if let Some(default_val) = prop_def.get("default").and_then(|v| v.as_str()) { 755 + Self::validate_string_default(parent_name, prop_name, default_val, prop_def, errors); 756 + } 757 + } 758 + "integer" => { 759 + let allowed_fields = [ 760 + "type", "description", "default", "const", "enum", 761 + "minimum", "maximum" 762 + ]; 763 + Self::validate_allowed_fields( 764 + &format!("{}.{}", parent_name, prop_name), 765 + prop_def, 766 + &allowed_fields, 767 + "Integer property", 768 + errors 769 + ); 770 + 771 + // Validate default value against constraints 772 + if let Some(default_val) = prop_def.get("default").and_then(|v| v.as_i64()) { 773 + Self::validate_integer_default(parent_name, prop_name, default_val, prop_def, errors); 774 + } 775 + } 776 + "boolean" => { 777 + let allowed_fields = ["type", "description", "default", "const"]; 778 + Self::validate_allowed_fields( 779 + &format!("{}.{}", parent_name, prop_name), 780 + prop_def, 781 + &allowed_fields, 782 + "Boolean property", 783 + errors 784 + ); 785 + 786 + // Validate default value against constraints 787 + if let Some(default_val) = prop_def.get("default").and_then(|v| v.as_bool()) { 788 + Self::validate_boolean_default(parent_name, prop_name, default_val, prop_def, errors); 789 + } 790 + } 791 + "array" => { 792 + let allowed_fields = [ 793 + "type", "description", "items", "minLength", "maxLength" 794 + ]; 795 + Self::validate_allowed_fields( 796 + &format!("{}.{}", parent_name, prop_name), 797 + prop_def, 798 + &allowed_fields, 799 + "Array property", 800 + errors 801 + ); 802 + 803 + // Validate items if present 804 + if let Some(items) = prop_def.get("items") { 805 + let items_path = format!("{}.items", prop_name); 806 + Self::validate_property_definition_static(parent_name, &items_path, items, errors); 807 + } 808 + } 809 + "object" => { 810 + let allowed_fields = [ 811 + "type", "description", "properties", "required", "nullable" 812 + ]; 813 + Self::validate_allowed_fields( 814 + &format!("{}.{}", parent_name, prop_name), 815 + prop_def, 816 + &allowed_fields, 817 + "Object property", 818 + errors 819 + ); 820 + 821 + // Recursively validate nested properties 822 + if let Some(nested_props) = prop_def.get("properties") { 823 + if !nested_props.is_object() { 824 + errors.push(format!( 825 + "Property '{}.{}' properties must be an object", 826 + parent_name, prop_name 827 + )); 828 + } else { 829 + for (nested_name, nested_def) in nested_props.as_object().unwrap() { 830 + let nested_path = format!("{}.{}", prop_name, nested_name); 831 + Self::validate_property_definition_static(parent_name, &nested_path, nested_def, errors); 832 + } 833 + } 834 + } 835 + } 836 + "ref" => { 837 + let allowed_fields = ["type", "description", "ref"]; 838 + Self::validate_allowed_fields( 839 + &format!("{}.{}", parent_name, prop_name), 840 + prop_def, 841 + &allowed_fields, 842 + "Ref property", 843 + errors 844 + ); 845 + } 846 + "union" => { 847 + let allowed_fields = ["type", "description", "refs", "closed"]; 848 + Self::validate_allowed_fields( 849 + &format!("{}.{}", parent_name, prop_name), 850 + prop_def, 851 + &allowed_fields, 852 + "Union property", 853 + errors 854 + ); 855 + } 856 + "blob" => { 857 + let allowed_fields = ["type", "description", "accept", "maxSize"]; 858 + Self::validate_allowed_fields( 859 + &format!("{}.{}", parent_name, prop_name), 860 + prop_def, 861 + &allowed_fields, 862 + "Blob property", 863 + errors 864 + ); 865 + } 866 + "bytes" => { 867 + let allowed_fields = ["type", "description", "minLength", "maxLength"]; 868 + Self::validate_allowed_fields( 869 + &format!("{}.{}", parent_name, prop_name), 870 + prop_def, 871 + &allowed_fields, 872 + "Bytes property", 873 + errors 874 + ); 875 + } 876 + "record" | "cid-link" | "unknown" | "token" | "null" => { 877 + let allowed_fields = ["type", "description"]; 878 + Self::validate_allowed_fields( 879 + &format!("{}.{}", parent_name, prop_name), 880 + prop_def, 881 + &allowed_fields, 882 + &format!("{} property", prop_type), 883 + errors 884 + ); 885 + } 886 + _ => { 887 + errors.push(format!( 888 + "Property '{}.{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null", 889 + parent_name, prop_name, prop_type 890 + )); 891 + } 892 + } 893 + } else { 894 + errors.push(format!( 895 + "Property '{}.{}' missing required 'type' field", 896 + parent_name, prop_name 897 + )); 898 + } 899 + } 900 + 188 901 /// Validate an object definition structure 189 902 fn validate_object_definition( 190 903 &self, ··· 199 912 def_name 200 913 ))); 201 914 } 915 + 916 + // Validate each property's type 917 + for (prop_name, prop_def) in properties.as_object().unwrap() { 918 + self.validate_property_definition(def_name, prop_name, prop_def)?; 919 + } 202 920 } 203 921 204 922 // Validate required field if it exists ··· 208 926 "Object definition '{}' required field must be an array", 209 927 def_name 210 928 ))); 929 + } 930 + } 931 + 932 + Ok(()) 933 + } 934 + 935 + /// Validate a type definition (for basic types) 936 + fn validate_type_definition( 937 + &self, 938 + def_name: &str, 939 + def_value: &Value, 940 + type_name: &str, 941 + ) -> Result<(), ValidationError> { 942 + // For basic types, validate any nested properties if they exist 943 + if let Some(properties) = def_value.get("properties") { 944 + if !properties.is_object() { 945 + return Err(ValidationError::InvalidSchema(format!( 946 + "{} definition '{}' properties must be an object", 947 + type_name, def_name 948 + ))); 949 + } 950 + 951 + // Validate each property's type 952 + for (prop_name, prop_def) in properties.as_object().unwrap() { 953 + self.validate_property_definition(def_name, prop_name, prop_def)?; 954 + } 955 + } 956 + 957 + Ok(()) 958 + } 959 + 960 + /// Validate a property definition's type 961 + fn validate_property_definition( 962 + &self, 963 + parent_name: &str, 964 + prop_name: &str, 965 + prop_def: &Value, 966 + ) -> Result<(), ValidationError> { 967 + if let Some(prop_type) = prop_def.get("type").and_then(|v| v.as_str()) { 968 + // Check if the property type is valid 969 + match prop_type { 970 + "record" | "object" | "string" | "integer" | "boolean" | "array" | "union" 971 + | "ref" | "blob" | "bytes" | "cid-link" | "unknown" | "token" | "null" => { 972 + // Valid type, recursively validate if it has properties 973 + if prop_type == "object" { 974 + if let Some(nested_props) = prop_def.get("properties") { 975 + if !nested_props.is_object() { 976 + return Err(ValidationError::InvalidSchema(format!( 977 + "Property '{}.{}' properties must be an object", 978 + parent_name, prop_name 979 + ))); 980 + } 981 + 982 + for (nested_name, nested_def) in nested_props.as_object().unwrap() { 983 + let nested_path = format!("{}.{}", prop_name, nested_name); 984 + self.validate_property_definition(parent_name, &nested_path, nested_def)?; 985 + } 986 + } 987 + } 988 + } 989 + _ => { 990 + return Err(ValidationError::InvalidSchema(format!( 991 + "Property '{}.{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null", 992 + parent_name, prop_name, prop_type 993 + ))); 994 + } 211 995 } 212 996 } 213 997 ··· 439 1223 actual: format!("{:?}", value), 440 1224 })?; 441 1225 442 - // Check min/max length (in UTF-8 bytes) 443 - if let Some(min_length) = schema["minLength"].as_u64() { 444 - if (string_val.len() as u64) < min_length { 445 - return Err(ValidationError::StringValidationFailed { 446 - path: ctx.path_string(), 447 - message: format!( 448 - "String length {} is less than minimum {}", 449 - string_val.len(), 450 - min_length 451 - ), 452 - }); 453 - } 454 - } 1226 + // Parse constraints from schema 1227 + let constraints = StringConstraints::from_json(schema); 455 1228 456 - if let Some(max_length) = schema["maxLength"].as_u64() { 457 - if (string_val.len() as u64) > max_length { 458 - return Err(ValidationError::StringValidationFailed { 459 - path: ctx.path_string(), 460 - message: format!( 461 - "String length {} exceeds maximum {}", 462 - string_val.len(), 463 - max_length 464 - ), 465 - }); 466 - } 467 - } 468 - 469 - // Check min/max graphemes (simplified grapheme counting) 470 - if let Some(min_graphemes) = schema["minGraphemes"].as_u64() { 471 - let grapheme_count = self.count_graphemes(string_val); 472 - if (grapheme_count as u64) < min_graphemes { 473 - return Err(ValidationError::StringValidationFailed { 474 - path: ctx.path_string(), 475 - message: format!( 476 - "String has {} graphemes, less than minimum {}", 477 - grapheme_count, min_graphemes 478 - ), 479 - }); 480 - } 481 - } 482 - 483 - if let Some(max_graphemes) = schema["maxGraphemes"].as_u64() { 484 - let grapheme_count = self.count_graphemes(string_val); 485 - if (grapheme_count as u64) > max_graphemes { 486 - return Err(ValidationError::StringValidationFailed { 487 - path: ctx.path_string(), 488 - message: format!( 489 - "String has {} graphemes, exceeds maximum {}", 490 - grapheme_count, max_graphemes 491 - ), 492 - }); 493 - } 494 - } 495 - 496 - // Check const value 497 - if let Some(const_val) = schema["const"].as_str() { 498 - if string_val != const_val { 499 - return Err(ValidationError::TypeMismatch { 500 - path: ctx.path_string(), 501 - expected: format!("\"{}\"", const_val), 502 - actual: format!("\"{}\"", string_val), 503 - }); 504 - } 505 - } 506 - 507 - // Check enum values 508 - if let Some(enum_values) = schema["enum"].as_array() { 509 - let valid = enum_values.iter().any(|v| v.as_str() == Some(string_val)); 510 - if !valid { 511 - return Err(ValidationError::EnumValidationFailed { 512 - path: ctx.path_string(), 513 - }); 514 - } 515 - } 516 - 517 - // Check format 518 - if let Some(format_str) = schema["format"].as_str() { 519 - if let Some(format) = StringFormat::from_str(format_str) { 520 - self.validate_string_format(string_val, format, ctx)? 521 - } 522 - } 1229 + // Validate all string constraints 1230 + self.validate_string_constraints(string_val, &constraints, ctx)?; 523 1231 524 1232 Ok(()) 525 1233 } ··· 694 1402 actual: format!("{:?}", value), 695 1403 })?; 696 1404 697 - if let Some(minimum) = schema["minimum"].as_i64() { 698 - if int_val < minimum { 699 - return Err(ValidationError::IntegerValidationFailed { 700 - path: ctx.path_string(), 701 - message: format!("Value {} is less than minimum {}", int_val, minimum), 702 - }); 703 - } 704 - } 705 - 706 - if let Some(maximum) = schema["maximum"].as_i64() { 707 - if int_val > maximum { 708 - return Err(ValidationError::IntegerValidationFailed { 709 - path: ctx.path_string(), 710 - message: format!("Value {} exceeds maximum {}", int_val, maximum), 711 - }); 712 - } 713 - } 1405 + // Parse constraints from schema 1406 + let constraints = IntegerConstraints::from_json(schema); 714 1407 715 - // Check const value 716 - if let Some(const_val) = schema["const"].as_i64() { 717 - if int_val != const_val { 718 - return Err(ValidationError::TypeMismatch { 719 - path: ctx.path_string(), 720 - expected: format!("{}", const_val), 721 - actual: format!("{}", int_val), 722 - }); 723 - } 724 - } 725 - 726 - if let Some(enum_values) = schema["enum"].as_array() { 727 - let valid = enum_values.iter().any(|v| v.as_i64() == Some(int_val)); 728 - if !valid { 729 - return Err(ValidationError::EnumValidationFailed { 730 - path: ctx.path_string(), 731 - }); 732 - } 733 - } 1408 + // Validate all integer constraints 1409 + self.validate_integer_constraints(int_val, &constraints, ctx)?; 734 1410 735 1411 Ok(()) 736 1412 } ··· 742 1418 schema: &Value, 743 1419 ctx: &ValidationContext, 744 1420 ) -> Result<(), ValidationError> { 745 - value 1421 + let bool_val = value 746 1422 .as_bool() 747 1423 .ok_or_else(|| ValidationError::TypeMismatch { 748 1424 path: ctx.path_string(), ··· 750 1426 actual: format!("{:?}", value), 751 1427 })?; 752 1428 753 - // Check const value if specified 754 - if let Some(const_val) = schema["const"].as_bool() { 755 - if value.as_bool() != Some(const_val) { 756 - return Err(ValidationError::TypeMismatch { 757 - path: ctx.path_string(), 758 - expected: format!("{}", const_val), 759 - actual: format!("{:?}", value), 760 - }); 761 - } 762 - } 1429 + // Parse constraints from schema 1430 + let constraints = BooleanConstraints::from_json(schema); 1431 + 1432 + // Validate all boolean constraints 1433 + self.validate_boolean_constraints(bool_val, &constraints, ctx)?; 763 1434 764 1435 Ok(()) 765 1436 } ··· 779 1450 actual: format!("{:?}", value), 780 1451 })?; 781 1452 782 - // Check min/max length 783 - if let Some(min_length) = schema["minLength"].as_u64() { 784 - if (array.len() as u64) < min_length { 785 - return Err(ValidationError::ArrayValidationFailed { 786 - path: ctx.path_string(), 787 - message: format!( 788 - "Array length {} is less than minimum {}", 789 - array.len(), 790 - min_length 791 - ), 792 - }); 793 - } 794 - } 1453 + // Parse constraints from schema 1454 + let constraints = ArrayConstraints::from_json(schema); 795 1455 796 - if let Some(max_length) = schema["maxLength"].as_u64() { 797 - if (array.len() as u64) > max_length { 798 - return Err(ValidationError::ArrayValidationFailed { 799 - path: ctx.path_string(), 800 - message: format!( 801 - "Array length {} exceeds maximum {}", 802 - array.len(), 803 - max_length 804 - ), 805 - }); 806 - } 807 - } 1456 + // Validate array constraints 1457 + self.validate_array_constraints(array, &constraints, ctx)?; 808 1458 809 1459 // Validate items 810 1460 if let Some(items_schema) = schema.get("items") { ··· 928 1578 fn validate_blob( 929 1579 &self, 930 1580 value: &Value, 931 - _schema: &Value, 1581 + schema: &Value, 932 1582 ctx: &ValidationContext, 933 1583 ) -> Result<(), ValidationError> { 934 1584 let obj = value ··· 966 1616 } 967 1617 968 1618 // Check other required fields 969 - if !obj.contains_key("mimeType") { 970 - return Err(ValidationError::RequiredFieldMissing { 1619 + let mime_type = obj.get("mimeType").and_then(|v| v.as_str()).ok_or_else(|| { 1620 + ValidationError::RequiredFieldMissing { 971 1621 path: ctx.with_field("mimeType").path_string(), 972 - }); 973 - } 1622 + } 1623 + })?; 974 1624 975 - if !obj.contains_key("size") { 976 - return Err(ValidationError::RequiredFieldMissing { 1625 + let size = obj.get("size").and_then(|v| v.as_u64()).ok_or_else(|| { 1626 + ValidationError::RequiredFieldMissing { 977 1627 path: ctx.with_field("size").path_string(), 978 - }); 979 - } 1628 + } 1629 + })?; 1630 + 1631 + // Parse constraints from schema 1632 + let constraints = BlobConstraints::from_json(schema); 1633 + 1634 + // Validate blob constraints 1635 + self.validate_blob_constraints(mime_type, size, &constraints, ctx)?; 980 1636 981 1637 Ok(()) 982 1638 } ··· 1009 1665 }); 1010 1666 } 1011 1667 1012 - // Check length constraints (bytes length, not string length) 1668 + // Calculate decoded byte length 1013 1669 let decoded_len = 1014 1670 (string_val.len() * 3 / 4) - string_val.chars().filter(|&c| c == '=').count(); 1015 1671 1016 - if let Some(min_length) = schema["minLength"].as_u64() { 1017 - if (decoded_len as u64) < min_length { 1018 - return Err(ValidationError::StringValidationFailed { 1019 - path: ctx.path_string(), 1020 - message: format!( 1021 - "Bytes length {} is less than minimum {}", 1022 - decoded_len, min_length 1023 - ), 1024 - }); 1025 - } 1026 - } 1672 + // Parse constraints from schema 1673 + let constraints = BytesConstraints::from_json(schema); 1027 1674 1028 - if let Some(max_length) = schema["maxLength"].as_u64() { 1029 - if (decoded_len as u64) > max_length { 1030 - return Err(ValidationError::StringValidationFailed { 1031 - path: ctx.path_string(), 1032 - message: format!( 1033 - "Bytes length {} exceeds maximum {}", 1034 - decoded_len, max_length 1035 - ), 1036 - }); 1037 - } 1038 - } 1675 + // Validate bytes constraints 1676 + self.validate_bytes_constraints(decoded_len, &constraints, ctx)?; 1039 1677 1040 1678 Ok(()) 1041 1679 } ··· 1094 1732 Ok(()) 1095 1733 } 1096 1734 1097 - /// Simplified grapheme counting (approximation) 1098 - /// This is a basic implementation - for full compliance with TS version, 1099 - /// would need proper Unicode grapheme cluster segmentation 1735 + /// Count grapheme clusters properly using unicode-segmentation 1100 1736 fn count_graphemes(&self, s: &str) -> usize { 1101 - // Very simplified approach: count Unicode scalar values, with basic combining character handling 1102 - let mut count = 0; 1103 - let mut chars = s.chars().peekable(); 1737 + s.graphemes(true).count() 1738 + } 1739 + 1740 + /// Validate string constraints (length, graphemes, enum, const, default, format) 1741 + fn validate_string_constraints( 1742 + &self, 1743 + value: &str, 1744 + constraints: &StringConstraints, 1745 + ctx: &ValidationContext, 1746 + ) -> Result<(), ValidationError> { 1747 + // Check const constraint 1748 + if let Some(const_value) = &constraints.const_value { 1749 + if value != const_value { 1750 + return Err(ValidationError::StringValidationFailed { 1751 + path: ctx.path_string(), 1752 + message: format!("Value '{}' does not match const value '{}'", value, const_value), 1753 + }); 1754 + } 1755 + } 1756 + 1757 + // Check enum constraint 1758 + if let Some(enum_values) = &constraints.enum_values { 1759 + if !enum_values.contains(&value.to_string()) { 1760 + return Err(ValidationError::StringValidationFailed { 1761 + path: ctx.path_string(), 1762 + message: format!("Value '{}' must be one of ({})", value, enum_values.join("|")), 1763 + }); 1764 + } 1765 + } 1766 + 1767 + // Check length constraints (UTF-8 byte length) 1768 + let byte_len = value.len() as u32; 1769 + if let Some(min_len) = constraints.min_length { 1770 + if byte_len < min_len { 1771 + return Err(ValidationError::StringValidationFailed { 1772 + path: ctx.path_string(), 1773 + message: format!("String length {} is less than minimum {}", byte_len, min_len), 1774 + }); 1775 + } 1776 + } 1777 + if let Some(max_len) = constraints.max_length { 1778 + if byte_len > max_len { 1779 + return Err(ValidationError::StringValidationFailed { 1780 + path: ctx.path_string(), 1781 + message: format!("String length {} exceeds maximum {}", byte_len, max_len), 1782 + }); 1783 + } 1784 + } 1785 + 1786 + // Check grapheme constraints (optimized like AT Protocol) 1787 + if constraints.min_graphemes.is_some() || constraints.max_graphemes.is_some() { 1788 + let char_len = value.chars().count() as u32; 1789 + 1790 + if let Some(max_graphemes) = constraints.max_graphemes { 1791 + // If char count is within limit, skip expensive grapheme calculation 1792 + if char_len > max_graphemes { 1793 + let grapheme_count = self.count_graphemes(value) as u32; 1794 + if grapheme_count > max_graphemes { 1795 + return Err(ValidationError::StringValidationFailed { 1796 + path: ctx.path_string(), 1797 + message: format!("String grapheme count {} exceeds maximum {}", grapheme_count, max_graphemes), 1798 + }); 1799 + } 1800 + } 1801 + } 1802 + 1803 + if let Some(min_graphemes) = constraints.min_graphemes { 1804 + // For minimum, we need to count accurately 1805 + let grapheme_count = self.count_graphemes(value) as u32; 1806 + if grapheme_count < min_graphemes { 1807 + return Err(ValidationError::StringValidationFailed { 1808 + path: ctx.path_string(), 1809 + message: format!("String grapheme count {} is less than minimum {}", grapheme_count, min_graphemes), 1810 + }); 1811 + } 1812 + } 1813 + } 1814 + 1815 + // Check format constraint 1816 + if let Some(format) = &constraints.format { 1817 + self.validate_string_format(value, format.clone(), ctx)?; 1818 + } 1819 + 1820 + Ok(()) 1821 + } 1822 + 1823 + /// Validate integer constraints (minimum, maximum, enum, const, default) 1824 + fn validate_integer_constraints( 1825 + &self, 1826 + value: i64, 1827 + constraints: &IntegerConstraints, 1828 + ctx: &ValidationContext, 1829 + ) -> Result<(), ValidationError> { 1830 + // Check const constraint 1831 + if let Some(const_value) = constraints.const_value { 1832 + if value != const_value { 1833 + return Err(ValidationError::IntegerValidationFailed { 1834 + path: ctx.path_string(), 1835 + message: format!("Value {} does not match const value {}", value, const_value), 1836 + }); 1837 + } 1838 + } 1839 + 1840 + // Check enum constraint 1841 + if let Some(enum_values) = &constraints.enum_values { 1842 + if !enum_values.contains(&value) { 1843 + let enum_str: Vec<String> = enum_values.iter().map(|v| v.to_string()).collect(); 1844 + return Err(ValidationError::IntegerValidationFailed { 1845 + path: ctx.path_string(), 1846 + message: format!("Value {} must be one of ({})", value, enum_str.join("|")), 1847 + }); 1848 + } 1849 + } 1850 + 1851 + // Check range constraints 1852 + if let Some(minimum) = constraints.minimum { 1853 + if value < minimum { 1854 + return Err(ValidationError::IntegerValidationFailed { 1855 + path: ctx.path_string(), 1856 + message: format!("Value {} is less than minimum {}", value, minimum), 1857 + }); 1858 + } 1859 + } 1860 + if let Some(maximum) = constraints.maximum { 1861 + if value > maximum { 1862 + return Err(ValidationError::IntegerValidationFailed { 1863 + path: ctx.path_string(), 1864 + message: format!("Value {} exceeds maximum {}", value, maximum), 1865 + }); 1866 + } 1867 + } 1868 + 1869 + Ok(()) 1870 + } 1871 + 1872 + /// Validate array constraints (minLength, maxLength) 1873 + fn validate_array_constraints( 1874 + &self, 1875 + array: &[Value], 1876 + constraints: &ArrayConstraints, 1877 + ctx: &ValidationContext, 1878 + ) -> Result<(), ValidationError> { 1879 + let len = array.len() as u32; 1880 + 1881 + if let Some(min_len) = constraints.min_length { 1882 + if len < min_len { 1883 + return Err(ValidationError::ArrayValidationFailed { 1884 + path: ctx.path_string(), 1885 + message: format!("Array length {} is less than minimum {}", len, min_len), 1886 + }); 1887 + } 1888 + } 1889 + if let Some(max_len) = constraints.max_length { 1890 + if len > max_len { 1891 + return Err(ValidationError::ArrayValidationFailed { 1892 + path: ctx.path_string(), 1893 + message: format!("Array length {} exceeds maximum {}", len, max_len), 1894 + }); 1895 + } 1896 + } 1897 + 1898 + Ok(()) 1899 + } 1900 + 1901 + /// Validate boolean constraints (const, default) 1902 + fn validate_boolean_constraints( 1903 + &self, 1904 + value: bool, 1905 + constraints: &BooleanConstraints, 1906 + ctx: &ValidationContext, 1907 + ) -> Result<(), ValidationError> { 1908 + // Check const constraint 1909 + if let Some(const_value) = constraints.const_value { 1910 + if value != const_value { 1911 + return Err(ValidationError::TypeMismatch { 1912 + path: ctx.path_string(), 1913 + expected: format!("boolean const value {}", const_value), 1914 + actual: value.to_string(), 1915 + }); 1916 + } 1917 + } 1918 + 1919 + Ok(()) 1920 + } 1921 + 1922 + /// Validate bytes constraints (minLength, maxLength for decoded bytes) 1923 + fn validate_bytes_constraints( 1924 + &self, 1925 + decoded_len: usize, 1926 + constraints: &BytesConstraints, 1927 + ctx: &ValidationContext, 1928 + ) -> Result<(), ValidationError> { 1929 + let len = decoded_len as u32; 1930 + 1931 + if let Some(min_len) = constraints.min_length { 1932 + if len < min_len { 1933 + return Err(ValidationError::StringValidationFailed { 1934 + path: ctx.path_string(), 1935 + message: format!("Bytes length {} is less than minimum {}", len, min_len), 1936 + }); 1937 + } 1938 + } 1939 + if let Some(max_len) = constraints.max_length { 1940 + if len > max_len { 1941 + return Err(ValidationError::StringValidationFailed { 1942 + path: ctx.path_string(), 1943 + message: format!("Bytes length {} exceeds maximum {}", len, max_len), 1944 + }); 1945 + } 1946 + } 1947 + 1948 + Ok(()) 1949 + } 1950 + 1951 + /// Validate blob constraints (accept mime types, maxSize) 1952 + fn validate_blob_constraints( 1953 + &self, 1954 + mime_type: &str, 1955 + size: u64, 1956 + constraints: &BlobConstraints, 1957 + ctx: &ValidationContext, 1958 + ) -> Result<(), ValidationError> { 1959 + // Check accepted mime types 1960 + if let Some(accept_types) = &constraints.accept { 1961 + if !accept_types.contains(&mime_type.to_string()) { 1962 + return Err(ValidationError::StringValidationFailed { 1963 + path: ctx.path_string(), 1964 + message: format!("MIME type '{}' not in accepted types: {}", mime_type, accept_types.join(", ")), 1965 + }); 1966 + } 1967 + } 1968 + 1969 + // Check max size 1970 + if let Some(max_size) = constraints.max_size { 1971 + if size > max_size { 1972 + return Err(ValidationError::StringValidationFailed { 1973 + path: ctx.path_string(), 1974 + message: format!("Blob size {} exceeds maximum {}", size, max_size), 1975 + }); 1976 + } 1977 + } 1978 + 1979 + Ok(()) 1980 + } 1981 + 1982 + /// Validate that a string default value satisfies the field's constraints 1983 + fn validate_string_default( 1984 + parent_name: &str, 1985 + prop_name: &str, 1986 + default_val: &str, 1987 + prop_def: &Value, 1988 + errors: &mut Vec<String>, 1989 + ) { 1990 + let field_path = format!("{}.{}", parent_name, prop_name); 1991 + 1992 + // Check const constraint 1993 + if let Some(const_val) = prop_def.get("const").and_then(|v| v.as_str()) { 1994 + if default_val != const_val { 1995 + errors.push(format!( 1996 + "String property '{}' default value '{}' does not match const value '{}'", 1997 + field_path, default_val, const_val 1998 + )); 1999 + } 2000 + } 2001 + 2002 + // Check enum constraint 2003 + if let Some(enum_array) = prop_def.get("enum").and_then(|v| v.as_array()) { 2004 + let enum_values: Vec<String> = enum_array 2005 + .iter() 2006 + .filter_map(|v| v.as_str()) 2007 + .map(|s| s.to_string()) 2008 + .collect(); 2009 + if !enum_values.contains(&default_val.to_string()) { 2010 + errors.push(format!( 2011 + "String property '{}' default value '{}' must be one of ({})", 2012 + field_path, default_val, enum_values.join("|") 2013 + )); 2014 + } 2015 + } 1104 2016 1105 - while let Some(_ch) = chars.next() { 1106 - count += 1; 2017 + // Check length constraints 2018 + let byte_len = default_val.len() as u32; 2019 + if let Some(min_len) = prop_def.get("minLength").and_then(|v| v.as_u64()) { 2020 + if byte_len < min_len as u32 { 2021 + errors.push(format!( 2022 + "String property '{}' default value '{}' length {} is less than minimum {}", 2023 + field_path, default_val, byte_len, min_len 2024 + )); 2025 + } 2026 + } 2027 + if let Some(max_len) = prop_def.get("maxLength").and_then(|v| v.as_u64()) { 2028 + if byte_len > max_len as u32 { 2029 + errors.push(format!( 2030 + "String property '{}' default value '{}' length {} exceeds maximum {}", 2031 + field_path, default_val, byte_len, max_len 2032 + )); 2033 + } 2034 + } 1107 2035 1108 - // Skip combining characters that follow this base character 1109 - while let Some(&next_ch) = chars.peek() { 1110 - if self.is_combining_character(next_ch) { 1111 - chars.next(); // consume the combining character 1112 - } else { 1113 - break; 2036 + // Check grapheme constraints 2037 + if prop_def.get("minGraphemes").is_some() || prop_def.get("maxGraphemes").is_some() { 2038 + let char_len = default_val.chars().count() as u32; 2039 + if let Some(min_graphemes) = prop_def.get("minGraphemes").and_then(|v| v.as_u64()) { 2040 + if char_len < min_graphemes as u32 { 2041 + errors.push(format!( 2042 + "String property '{}' default value '{}' grapheme length {} is less than minimum {}", 2043 + field_path, default_val, char_len, min_graphemes 2044 + )); 1114 2045 } 1115 2046 } 2047 + if let Some(max_graphemes) = prop_def.get("maxGraphemes").and_then(|v| v.as_u64()) { 2048 + if char_len > max_graphemes as u32 { 2049 + errors.push(format!( 2050 + "String property '{}' default value '{}' grapheme length {} exceeds maximum {}", 2051 + field_path, default_val, char_len, max_graphemes 2052 + )); 2053 + } 2054 + } 2055 + } 2056 + } 2057 + 2058 + /// Validate that an integer default value satisfies the field's constraints 2059 + fn validate_integer_default( 2060 + parent_name: &str, 2061 + prop_name: &str, 2062 + default_val: i64, 2063 + prop_def: &Value, 2064 + errors: &mut Vec<String>, 2065 + ) { 2066 + let field_path = format!("{}.{}", parent_name, prop_name); 2067 + 2068 + // Check const constraint 2069 + if let Some(const_val) = prop_def.get("const").and_then(|v| v.as_i64()) { 2070 + if default_val != const_val { 2071 + errors.push(format!( 2072 + "Integer property '{}' default value {} does not match const value {}", 2073 + field_path, default_val, const_val 2074 + )); 2075 + } 1116 2076 } 1117 2077 1118 - count 2078 + // Check enum constraint 2079 + if let Some(enum_array) = prop_def.get("enum").and_then(|v| v.as_array()) { 2080 + let enum_values: Vec<i64> = enum_array 2081 + .iter() 2082 + .filter_map(|v| v.as_i64()) 2083 + .collect(); 2084 + if !enum_values.contains(&default_val) { 2085 + errors.push(format!( 2086 + "Integer property '{}' default value {} must be one of ({})", 2087 + field_path, default_val, 2088 + enum_values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join("|") 2089 + )); 2090 + } 2091 + } 2092 + 2093 + // Check minimum constraint 2094 + if let Some(minimum) = prop_def.get("minimum").and_then(|v| v.as_i64()) { 2095 + if default_val < minimum { 2096 + errors.push(format!( 2097 + "Integer property '{}' default value {} is less than minimum {}", 2098 + field_path, default_val, minimum 2099 + )); 2100 + } 2101 + } 2102 + 2103 + // Check maximum constraint 2104 + if let Some(maximum) = prop_def.get("maximum").and_then(|v| v.as_i64()) { 2105 + if default_val > maximum { 2106 + errors.push(format!( 2107 + "Integer property '{}' default value {} exceeds maximum {}", 2108 + field_path, default_val, maximum 2109 + )); 2110 + } 2111 + } 1119 2112 } 1120 2113 1121 - /// Check if a character is a combining character (very simplified) 1122 - fn is_combining_character(&self, ch: char) -> bool { 1123 - // This is a simplified check for common combining marks 1124 - // In a full implementation, would need proper Unicode category checking 1125 - matches!(ch as u32, 1126 - 0x0300..=0x036F | // Combining Diacritical Marks 1127 - 0x1AB0..=0x1AFF | // Combining Diacritical Marks Extended 1128 - 0x1DC0..=0x1DFF | // Combining Diacritical Marks Supplement 1129 - 0x20D0..=0x20FF | // Combining Diacritical Marks for Symbols 1130 - 0xFE20..=0xFE2F // Combining Half Marks 1131 - ) 2114 + /// Validate that a boolean default value satisfies the field's constraints 2115 + fn validate_boolean_default( 2116 + parent_name: &str, 2117 + prop_name: &str, 2118 + default_val: bool, 2119 + prop_def: &Value, 2120 + errors: &mut Vec<String>, 2121 + ) { 2122 + let field_path = format!("{}.{}", parent_name, prop_name); 2123 + 2124 + // Check const constraint 2125 + if let Some(const_val) = prop_def.get("const").and_then(|v| v.as_bool()) { 2126 + if default_val != const_val { 2127 + errors.push(format!( 2128 + "Boolean property '{}' default value {} does not match const value {}", 2129 + field_path, default_val, const_val 2130 + )); 2131 + } 2132 + } 1132 2133 } 1133 2134 }
+2 -2
packages/lexicon/deno.json
··· 1 1 { 2 2 "name": "@slices/lexicon", 3 - "version": "0.1.0-alpha.2", 3 + "version": "0.1.0-alpha.3", 4 4 "description": "AT Protocol lexicon validation", 5 5 "license": "MIT", 6 6 "exports": "./mod.ts", 7 7 "tasks": { 8 8 "dev": "deno run --watch mod.ts", 9 - "build": "cd ../lexicon-rs && wasm-pack build --target web --out-dir pkg && cp pkg/* ../lexicon/wasm/", 9 + "build": "cd ../lexicon-rs && wasm-pack build --target web --features wasm && cp pkg/* ../lexicon/wasm/", 10 10 "test": "deno test --allow-read tests/", 11 11 "test:basic": "deno run --allow-read tests/basic_test.ts", 12 12 "test:comprehensive": "deno test --allow-read tests/comprehensive_test.ts",
+3 -3
packages/lexicon/mod.ts
··· 8 8 WasmLexiconValidator, 9 9 validate_string_format, 10 10 is_valid_nsid, 11 - } from "./wasm/lexicon_validator.js"; 11 + } from "./wasm/slices_lexicon.js"; 12 12 13 13 // Initialize WASM module 14 14 let wasmInitialized = false; 15 15 16 16 async function ensureWasmInit() { 17 17 if (!wasmInitialized) { 18 - const wasmUrl = new URL("./wasm/lexicon_validator_bg.wasm", import.meta.url); 19 - await init(wasmUrl); 18 + const wasmUrl = new URL("./wasm/slices_lexicon_bg.wasm", import.meta.url); 19 + await init({ module_or_path: wasmUrl }); 20 20 wasmInitialized = true; 21 21 } 22 22 }
+18 -8
packages/lexicon/tests/comprehensive_test.ts
··· 84 84 }); 85 85 86 86 Deno.test("validateStringFormat - invalid DID", async () => { 87 - await assertThrows( 88 - async () => await validateStringFormat("not-a-did", "did"), 89 - ValidationError 90 - ); 87 + try { 88 + await validateStringFormat("not-a-did", "did"); 89 + throw new Error("Expected validation to fail"); 90 + } catch (error) { 91 + if (!(error instanceof ValidationError)) { 92 + throw error; 93 + } 94 + // Expected error - test passes 95 + } 91 96 }); 92 97 93 98 Deno.test("validateStringFormat - valid handle", async () => { ··· 96 101 }); 97 102 98 103 Deno.test("validateStringFormat - invalid handle", async () => { 99 - await assertThrows( 100 - async () => await validateStringFormat("invalid..handle", "handle"), 101 - ValidationError 102 - ); 104 + try { 105 + await validateStringFormat("invalid..handle", "handle"); 106 + throw new Error("Expected validation to fail"); 107 + } catch (error) { 108 + if (!(error instanceof ValidationError)) { 109 + throw error; 110 + } 111 + // Expected error - test passes 112 + } 103 113 }); 104 114 105 115 Deno.test("isValidNsid - valid NSID", async () => {
packages/lexicon/wasm/lexicon_validator_bg.wasm

This is a binary file and will not be displayed.

+1 -1
packages/lexicon/wasm/package.json
··· 2 2 "name": "slices-lexicon", 3 3 "type": "module", 4 4 "description": "AT Protocol lexicon validation library for Slices", 5 - "version": "0.1.2", 5 + "version": "0.1.4", 6 6 "license": "MIT", 7 7 "files": [ 8 8 "slices_lexicon_bg.wasm",
packages/lexicon/wasm/slices_lexicon_bg.wasm

This is a binary file and will not be displayed.