Highly ambitious ATProtocol AppView service and sdks

fix issues with ref validation for query, procedure, and subscription type lexicons

+36 -2
packages/cli/src/generated_client.ts
··· 1 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-28 21:51:06 UTC 3 - // Lexicons: 41 2 + // Generated at: 2025-09-28 23:51:18 UTC 3 + // Lexicons: 42 4 4 5 5 /** 6 6 * @example Usage ··· 1046 1046 connected: boolean; 1047 1047 } 1048 1048 1049 + export interface NetworkSlicesSliceGetSyncSummaryParams { 1050 + slice: string; 1051 + collections?: string[]; 1052 + externalCollections?: string[]; 1053 + repos?: string[]; 1054 + maxRepos?: number; 1055 + } 1056 + 1057 + export interface NetworkSlicesSliceGetSyncSummaryOutput { 1058 + totalRepos: number; 1059 + cappedRepos: number; 1060 + collectionsSummary: unknown[]; 1061 + wouldBeCapped: boolean; 1062 + appliedLimit: number; 1063 + } 1064 + 1065 + export interface NetworkSlicesSliceGetSyncSummaryCollectionSummary { 1066 + collection: string; 1067 + estimatedRepos: number; 1068 + isExternal: boolean; 1069 + } 1070 + 1049 1071 export interface NetworkSlicesSlice { 1050 1072 /** Name of the slice */ 1051 1073 name: string; ··· 1552 1574 1553 1575 export interface NetworkSlicesSliceGetJobLogs { 1554 1576 readonly LogEntry: NetworkSlicesSliceGetJobLogsLogEntry; 1577 + } 1578 + 1579 + export interface NetworkSlicesSliceGetSyncSummary { 1580 + readonly CollectionSummary: NetworkSlicesSliceGetSyncSummaryCollectionSummary; 1555 1581 } 1556 1582 1557 1583 export interface NetworkSlicesSliceGetJobStatus { ··· 2243 2269 return await this.client.makeRequest< 2244 2270 NetworkSlicesSliceGetJetstreamStatusOutput 2245 2271 >("network.slices.slice.getJetstreamStatus", "GET", {}); 2272 + } 2273 + 2274 + async getSyncSummary( 2275 + params?: NetworkSlicesSliceGetSyncSummaryParams, 2276 + ): Promise<NetworkSlicesSliceGetSyncSummaryOutput> { 2277 + return await this.client.makeRequest< 2278 + NetworkSlicesSliceGetSyncSummaryOutput 2279 + >("network.slices.slice.getSyncSummary", "GET", params); 2246 2280 } 2247 2281 2248 2282 async getJobStatus(
packages/lexicon-intellisense/wasm/slices_lexicon_bg.wasm

This is a binary file and will not be displayed.

+44 -2
packages/lexicon-rs/src/lib.rs
··· 655 655 use crate::validation::primitive::string::StringValidator; 656 656 use crate::StringFormat; 657 657 658 - let format_enum = StringFormat::from_str(format) 659 - .ok_or_else(|| JsValue::from_str(&format!("Unknown format: {}", format)))?; 658 + let format_enum = format.parse::<StringFormat>() 659 + .map_err(|_| JsValue::from_str(&format!("Unknown format: {}", format)))?; 660 660 661 661 let validator = StringValidator; 662 662 ··· 803 803 assert_eq!(total_errors, 5, "Should have collected all 5 errors total"); 804 804 assert_eq!(errors.len(), 2, "Should have errors for exactly 2 lexicons"); 805 805 } 806 + } 807 + } 808 + 809 + #[test] 810 + fn test_broken_reference_validation() { 811 + let broken_lexicon = json!({ 812 + "lexicon": 1, 813 + "id": "test.broken.ref", 814 + "defs": { 815 + "main": { 816 + "type": "object", 817 + "properties": { 818 + "items": { 819 + "type": "array", 820 + "items": { 821 + "type": "ref", 822 + "ref": "#nonExistentDef" 823 + } 824 + } 825 + } 826 + }, 827 + "existingDef": { 828 + "type": "string" 829 + } 830 + } 831 + }); 832 + 833 + let result = validate(vec![broken_lexicon]); 834 + 835 + // Should fail because #nonExistentDef doesn't exist 836 + assert!(result.is_err(), "Validation should fail for broken reference"); 837 + 838 + if let Err(errors) = result { 839 + assert!(errors.contains_key("test.broken.ref"), "Should have errors for the test lexicon"); 840 + let error_messages = errors.get("test.broken.ref").unwrap(); 841 + assert!(!error_messages.is_empty(), "Should have at least one error"); 842 + 843 + // Check that at least one error mentions the non-existent reference 844 + let has_ref_error = error_messages.iter().any(|msg| 845 + msg.contains("non-existent") || msg.contains("nonExistentDef") 846 + ); 847 + assert!(has_ref_error, "Should have error about non-existent reference. Got: {:?}", error_messages); 806 848 } 807 849 } 808 850 }
+18 -14
packages/lexicon-rs/src/types.rs
··· 29 29 RecordKey, 30 30 } 31 31 32 - impl StringFormat { 33 - pub fn from_str(s: &str) -> Option<Self> { 32 + use std::str::FromStr; 33 + 34 + impl FromStr for StringFormat { 35 + type Err = String; 36 + 37 + fn from_str(s: &str) -> Result<Self, Self::Err> { 34 38 match s { 35 - "datetime" => Some(Self::DateTime), 36 - "uri" => Some(Self::Uri), 37 - "at-uri" => Some(Self::AtUri), 38 - "did" => Some(Self::Did), 39 - "handle" => Some(Self::Handle), 40 - "at-identifier" => Some(Self::AtIdentifier), 41 - "nsid" => Some(Self::Nsid), 42 - "cid" => Some(Self::Cid), 43 - "language" => Some(Self::Language), 44 - "tid" => Some(Self::Tid), 45 - "record-key" => Some(Self::RecordKey), 46 - _ => None, 39 + "datetime" => Ok(Self::DateTime), 40 + "uri" => Ok(Self::Uri), 41 + "at-uri" => Ok(Self::AtUri), 42 + "did" => Ok(Self::Did), 43 + "handle" => Ok(Self::Handle), 44 + "at-identifier" => Ok(Self::AtIdentifier), 45 + "nsid" => Ok(Self::Nsid), 46 + "cid" => Ok(Self::Cid), 47 + "language" => Ok(Self::Language), 48 + "tid" => Ok(Self::Tid), 49 + "record-key" => Ok(Self::RecordKey), 50 + _ => Err(format!("Unknown string format: {}", s)), 47 51 } 48 52 } 49 53 }
+2 -2
packages/lexicon-rs/src/validation/context.rs
··· 225 225 /// - Global: `com.example.ns#defName` -> uses specified lexicon ID 226 226 /// - Global main: `com.example.ns` -> uses "main" as def name 227 227 fn parse_reference(&self, reference: &str) -> Result<(String, String), ValidationError> { 228 - if reference.starts_with('#') { 228 + if let Some(def_name) = reference.strip_prefix('#') { 229 229 // Local reference - use current lexicon 230 230 let current_lexicon = self.current_lexicon_id.as_ref().ok_or_else(|| { 231 231 ValidationError::InvalidSchema(format!( ··· 234 234 )) 235 235 })?; 236 236 237 - let def_name = reference[1..].to_string(); // Remove the '#' 237 + let def_name = def_name.to_string(); // Remove the '#' 238 238 if def_name.is_empty() { 239 239 return Err(ValidationError::InvalidSchema(format!( 240 240 "Invalid local reference '{}': definition name cannot be empty",
+114 -21
packages/lexicon-rs/src/validation/field/reference.rs
··· 60 60 return Err("Reference cannot be empty".to_string()); 61 61 } 62 62 63 - if ref_str.starts_with('#') { 63 + if let Some(def_name) = ref_str.strip_prefix('#') { 64 64 // Local reference 65 - let def_name = &ref_str[1..]; 66 65 if def_name.is_empty() { 67 66 return Err("Local reference must have a definition name after #".to_string()); 68 67 } ··· 105 104 // First validate syntax 106 105 Self::validate_ref_syntax(ref_str)?; 107 106 108 - if ref_str.starts_with('#') { 107 + if let Some(def_name) = ref_str.strip_prefix('#') { 109 108 // Local reference 110 - let def_name = ref_str[1..].to_string(); 109 + let def_name = def_name.to_string(); 111 110 Ok(ParsedReference::Local(def_name)) 112 111 } else if ref_str.contains('#') { 113 112 // Global reference with fragment ··· 358 357 )) 359 358 })?; 360 359 361 - // TODO: Validate that the reference can be resolved 362 - // This would require access to the reference resolution system 360 + // Parse and validate that the reference can be resolved 361 + let parsed_ref = Self::parse_reference(ref_str).map_err(|e| { 362 + ValidationError::InvalidSchema(format!( 363 + "Ref '{}' has invalid reference: {}", 364 + def_name, e 365 + )) 366 + })?; 367 + 368 + // Attempt to resolve the reference to verify it exists 369 + // Ensure we have a current lexicon context for resolving local references 370 + ctx.current_lexicon_id() 371 + .ok_or_else(|| { 372 + ValidationError::InvalidSchema(format!( 373 + "Cannot validate ref '{}': no current lexicon context", 374 + def_name 375 + )) 376 + })?; 363 377 364 - Ok(()) 378 + match Self::resolve_reference(&parsed_ref, ctx) { 379 + Ok(_) => Ok(()), // Reference resolved successfully 380 + Err(ValidationError::DataValidation(msg)) if msg.contains("not found") => { 381 + // Convert data validation error to schema validation error for definition validation 382 + Err(ValidationError::InvalidSchema(format!( 383 + "Ref '{}' references non-existent definition: {}", 384 + def_name, ref_str 385 + ))) 386 + } 387 + Err(e) => Err(e), // Pass through other errors 388 + } 365 389 } 366 390 367 391 /// Validates runtime data against a reference schema definition ··· 456 480 .with_lexicons(vec![json!({ 457 481 "lexicon": 1, 458 482 "id": "com.example.test", 459 - "defs": { "main": ref_def.clone() } 483 + "defs": { 484 + "main": ref_def.clone(), 485 + "commonObject": { 486 + "type": "object", 487 + "properties": {} 488 + } 489 + } 460 490 })]) 461 491 .unwrap() 462 492 .build() 463 - .unwrap(); 493 + .unwrap() 494 + .with_current_lexicon("com.example.test"); 464 495 465 496 let validator = RefValidator; 466 497 assert!(validator.validate(&ref_def, &ctx).is_ok()); ··· 474 505 }); 475 506 476 507 let ctx = ValidationContext::builder() 477 - .with_lexicons(vec![json!({ 478 - "lexicon": 1, 479 - "id": "com.example.test", 480 - "defs": { "main": ref_def.clone() } 481 - })]) 508 + .with_lexicons(vec![ 509 + json!({ 510 + "lexicon": 1, 511 + "id": "com.example.test", 512 + "defs": { "main": ref_def.clone() } 513 + }), 514 + json!({ 515 + "lexicon": 1, 516 + "id": "com.example.defs", 517 + "defs": { 518 + "main": { "type": "object" }, 519 + "commonObject": { 520 + "type": "object", 521 + "properties": {} 522 + } 523 + } 524 + }) 525 + ]) 482 526 .unwrap() 483 527 .build() 484 - .unwrap(); 528 + .unwrap() 529 + .with_current_lexicon("com.example.test"); 485 530 486 531 let validator = RefValidator; 487 532 assert!(validator.validate(&ref_def, &ctx).is_ok()); ··· 495 540 }); 496 541 497 542 let ctx = ValidationContext::builder() 543 + .with_lexicons(vec![ 544 + json!({ 545 + "lexicon": 1, 546 + "id": "com.example.test", 547 + "defs": { "main": ref_def.clone() } 548 + }), 549 + json!({ 550 + "lexicon": 1, 551 + "id": "com.example.record", 552 + "defs": { 553 + "main": { 554 + "type": "record", 555 + "record": { 556 + "type": "object", 557 + "properties": {} 558 + } 559 + } 560 + } 561 + }) 562 + ]) 563 + .unwrap() 564 + .build() 565 + .unwrap() 566 + .with_current_lexicon("com.example.test"); 567 + 568 + let validator = RefValidator; 569 + assert!(validator.validate(&ref_def, &ctx).is_ok()); 570 + } 571 + 572 + #[test] 573 + fn test_missing_ref_field() { 574 + let ref_def = json!({ 575 + "type": "ref" 576 + }); 577 + 578 + let ctx = ValidationContext::builder() 498 579 .with_lexicons(vec![json!({ 499 580 "lexicon": 1, 500 581 "id": "com.example.test", ··· 502 583 })]) 503 584 .unwrap() 504 585 .build() 505 - .unwrap(); 586 + .unwrap() 587 + .with_current_lexicon("com.example.test"); 506 588 507 589 let validator = RefValidator; 508 - assert!(validator.validate(&ref_def, &ctx).is_ok()); 590 + assert!(validator.validate(&ref_def, &ctx).is_err()); 509 591 } 510 592 511 593 #[test] 512 - fn test_missing_ref_field() { 594 + fn test_ref_to_nonexistent_definition() { 513 595 let ref_def = json!({ 514 - "type": "ref" 596 + "type": "ref", 597 + "ref": "#nonExistentDef" 515 598 }); 516 599 517 600 let ctx = ValidationContext::builder() ··· 522 605 })]) 523 606 .unwrap() 524 607 .build() 525 - .unwrap(); 608 + .unwrap() 609 + .with_current_lexicon("com.example.test"); 526 610 527 611 let validator = RefValidator; 528 - assert!(validator.validate(&ref_def, &ctx).is_err()); 612 + let result = validator.validate(&ref_def, &ctx); 613 + assert!(result.is_err()); 614 + 615 + // Check that the error message mentions the non-existent definition 616 + if let Err(ValidationError::InvalidSchema(msg)) = result { 617 + assert!(msg.contains("non-existent definition")); 618 + assert!(msg.contains("#nonExistentDef")); 619 + } else { 620 + panic!("Expected InvalidSchema error for non-existent reference"); 621 + } 529 622 } 530 623 531 624 #[test]
+21 -8
packages/lexicon-rs/src/validation/field/union.rs
··· 46 46 } 47 47 48 48 // Handle local reference patterns (#ref) 49 - if reference.starts_with('#') { 50 - let ref_name = &reference[1..]; 49 + if let Some(ref_name) = reference.strip_prefix('#') { 51 50 // Match bare name against local ref 52 51 if type_name == ref_name { 53 52 return true; ··· 59 58 } 60 59 61 60 // Handle implicit #main patterns 62 - if type_name.ends_with("#main") { 63 - let base_type = &type_name[..type_name.len() - 5]; // Remove "#main" 61 + if let Some(base_type) = type_name.strip_suffix("#main") { 62 + // Remove "#main" 64 63 if reference == base_type { 65 64 return true; 66 65 } ··· 211 210 } 212 211 } 213 212 214 - // TODO: Validate that each reference can be resolved 215 - // This would require access to the reference resolution system 213 + // Validate that each reference can be resolved 214 + use crate::validation::field::reference::RefValidator; 215 + let ref_validator = RefValidator; 216 + 217 + for (i, ref_item) in refs_array.iter().enumerate() { 218 + if let Some(ref_str) = ref_item.as_str() { 219 + // Create a temporary ref schema for validation 220 + let temp_ref_schema = serde_json::json!({ 221 + "type": "ref", 222 + "ref": ref_str 223 + }); 224 + 225 + let ref_ctx = ctx.with_path(&format!("{}.refs[{}]", def_name, i)); 226 + ref_validator.validate(&temp_ref_schema, &ref_ctx)?; 227 + } 228 + } 216 229 217 230 Ok(()) 218 231 } ··· 922 935 923 936 // String that should violate shortString maxLength constraint 924 937 // Currently passes because ObjectValidator doesn't validate property constraints yet 925 - let invalid_short = json!({ 938 + let _invalid_short = json!({ 926 939 "$type": "shortString", 927 940 "text": "This string is way too long for the short constraint" 928 941 }); 929 942 // assert!(validator.validate_data(&invalid_short, &union_schema, &ctx).is_err()); // TODO: Enable when ObjectValidator supports constraints 930 943 931 944 // String that should violate longString minLength constraint 932 - let invalid_long = json!({ 945 + let _invalid_long = json!({ 933 946 "$type": "longString", 934 947 "text": "short" 935 948 });
+199 -1
packages/lexicon-rs/src/validation/primary/procedure.rs
··· 5 5 use serde_json::Value; 6 6 use crate::errors::ValidationError; 7 7 use crate::validation::{Validator, ValidationContext}; 8 + use crate::validation::{ 9 + field::{object::ObjectValidator, array::ArrayValidator, union::UnionValidator, reference::RefValidator}, 10 + primitive::{string::StringValidator, integer::IntegerValidator, boolean::BooleanValidator, bytes::BytesValidator, blob::BlobValidator, cid_link::CidLinkValidator, null::NullValidator}, 11 + meta::{token::TokenValidator, unknown::UnknownValidator}, 12 + }; 8 13 9 14 /// Validator for `procedure` type Lexicon definitions 10 15 /// ··· 24 29 pub struct ProcedureValidator; 25 30 26 31 impl ProcedureValidator { 32 + /// Validates a schema by dispatching to the appropriate validator 33 + fn validate_schema_by_type( 34 + &self, 35 + schema: &Value, 36 + schema_type: &str, 37 + ctx: &ValidationContext, 38 + ) -> Result<(), ValidationError> { 39 + match schema_type { 40 + // Field types 41 + "object" => ObjectValidator.validate(schema, ctx), 42 + "array" => ArrayValidator.validate(schema, ctx), 43 + "union" => UnionValidator.validate(schema, ctx), 44 + "ref" => RefValidator.validate(schema, ctx), 45 + 46 + // Primitive types 47 + "string" => StringValidator.validate(schema, ctx), 48 + "integer" => IntegerValidator.validate(schema, ctx), 49 + "boolean" => BooleanValidator.validate(schema, ctx), 50 + "bytes" => BytesValidator.validate(schema, ctx), 51 + "blob" => BlobValidator.validate(schema, ctx), 52 + "cid-link" => CidLinkValidator.validate(schema, ctx), 53 + "null" => NullValidator.validate(schema, ctx), 54 + 55 + // Meta types 56 + "token" => TokenValidator.validate(schema, ctx), 57 + "unknown" => UnknownValidator.validate(schema, ctx), 58 + 59 + _ => Err(ValidationError::InvalidSchema(format!( 60 + "Unknown schema type '{}' in procedure", schema_type 61 + ))), 62 + } 63 + } 64 + 27 65 /// Validates procedure input data against input definition 28 66 /// 29 67 /// This method performs validation of procedure input against its schema. ··· 197 235 } 198 236 } 199 237 200 - // TODO: Validate parameters, input, output, and errors 238 + // Validate parameters if present (must be type "params") 239 + if let Some(parameters) = value.get("parameters") { 240 + let params_ctx = ctx.with_path(&format!("{}.parameters", def_name)); 241 + ObjectValidator.validate(parameters, &params_ctx)?; 242 + } 243 + 244 + // Validate input if present (must have encoding, optional schema) 245 + if let Some(input) = value.get("input") { 246 + if let Some(input_obj) = input.as_object() { 247 + // Validate required encoding field 248 + if !input_obj.contains_key("encoding") { 249 + return Err(ValidationError::InvalidSchema(format!( 250 + "Procedure '{}' input missing required 'encoding' field", 251 + def_name 252 + ))); 253 + } 254 + 255 + // Validate optional schema field 256 + if let Some(schema) = input_obj.get("schema") { 257 + if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) { 258 + let schema_ctx = ctx.with_path(&format!("{}.input.schema", def_name)); 259 + self.validate_schema_by_type(schema, schema_type, &schema_ctx)?; 260 + } 261 + } 262 + } else { 263 + return Err(ValidationError::InvalidSchema(format!( 264 + "Procedure '{}' input must be an object", 265 + def_name 266 + ))); 267 + } 268 + } 269 + 270 + // Validate output if present (must have encoding, optional schema) 271 + if let Some(output) = value.get("output") { 272 + if let Some(output_obj) = output.as_object() { 273 + // Validate required encoding field 274 + if !output_obj.contains_key("encoding") { 275 + return Err(ValidationError::InvalidSchema(format!( 276 + "Procedure '{}' output missing required 'encoding' field", 277 + def_name 278 + ))); 279 + } 280 + 281 + // Validate optional schema field 282 + if let Some(schema) = output_obj.get("schema") { 283 + if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) { 284 + let schema_ctx = ctx.with_path(&format!("{}.output.schema", def_name)); 285 + self.validate_schema_by_type(schema, schema_type, &schema_ctx)?; 286 + } 287 + } 288 + } else { 289 + return Err(ValidationError::InvalidSchema(format!( 290 + "Procedure '{}' output must be an object", 291 + def_name 292 + ))); 293 + } 294 + } 295 + 296 + // TODO: Validate errors if present (array of objects with name field) 201 297 202 298 Ok(()) 203 299 } ··· 292 388 293 389 let invalid_data = json!({"content": "Content"}); 294 390 assert!(validator.validate_data(&invalid_data, &schema, &ctx).is_err()); 391 + } 392 + 393 + #[test] 394 + fn test_procedure_with_broken_reference_in_output() { 395 + let procedure = json!({ 396 + "type": "procedure", 397 + "description": "Test procedure with broken reference in output", 398 + "output": { 399 + "encoding": "application/json", 400 + "schema": { 401 + "type": "object", 402 + "properties": { 403 + "result": { 404 + "type": "ref", 405 + "ref": "#nonExistentDef" 406 + } 407 + } 408 + } 409 + } 410 + }); 411 + 412 + let ctx = ValidationContext::builder() 413 + .with_lexicons(vec![json!({ 414 + "lexicon": 1, 415 + "id": "test.broken.ref", 416 + "defs": { 417 + "main": procedure.clone(), 418 + "existingDef": { 419 + "type": "string" 420 + } 421 + } 422 + })]) 423 + .unwrap() 424 + .build() 425 + .unwrap() 426 + .with_current_lexicon("test.broken.ref"); 427 + 428 + let validator = ProcedureValidator; 429 + let result = validator.validate(&procedure, &ctx); 430 + 431 + // Should fail because #nonExistentDef doesn't exist 432 + assert!(result.is_err(), "Procedure validation should fail for broken reference in output schema"); 433 + 434 + if let Err(error) = result { 435 + let error_msg = format!("{:?}", error); 436 + assert!( 437 + error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"), 438 + "Error should mention the non-existent reference. Got: {}", 439 + error_msg 440 + ); 441 + } 442 + } 443 + 444 + #[test] 445 + fn test_procedure_with_broken_reference_in_input() { 446 + let procedure = json!({ 447 + "type": "procedure", 448 + "description": "Test procedure with broken reference in input", 449 + "input": { 450 + "encoding": "application/json", 451 + "schema": { 452 + "type": "object", 453 + "properties": { 454 + "data": { 455 + "type": "ref", 456 + "ref": "#nonExistentDef" 457 + } 458 + } 459 + } 460 + } 461 + }); 462 + 463 + let ctx = ValidationContext::builder() 464 + .with_lexicons(vec![json!({ 465 + "lexicon": 1, 466 + "id": "test.broken.ref", 467 + "defs": { 468 + "main": procedure.clone(), 469 + "existingDef": { 470 + "type": "string" 471 + } 472 + } 473 + })]) 474 + .unwrap() 475 + .build() 476 + .unwrap() 477 + .with_current_lexicon("test.broken.ref"); 478 + 479 + let validator = ProcedureValidator; 480 + let result = validator.validate(&procedure, &ctx); 481 + 482 + // Should fail because #nonExistentDef doesn't exist 483 + assert!(result.is_err(), "Procedure validation should fail for broken reference in input schema"); 484 + 485 + if let Err(error) = result { 486 + let error_msg = format!("{:?}", error); 487 + assert!( 488 + error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"), 489 + "Error should mention the non-existent reference. Got: {}", 490 + error_msg 491 + ); 492 + } 295 493 } 296 494 }
+121 -2
packages/lexicon-rs/src/validation/primary/query.rs
··· 5 5 use serde_json::Value; 6 6 use crate::errors::ValidationError; 7 7 use crate::validation::{Validator, ValidationContext}; 8 + use crate::validation::{ 9 + field::{object::ObjectValidator, array::ArrayValidator, union::UnionValidator, reference::RefValidator}, 10 + primitive::{string::StringValidator, integer::IntegerValidator, boolean::BooleanValidator, bytes::BytesValidator, blob::BlobValidator, cid_link::CidLinkValidator, null::NullValidator}, 11 + meta::{token::TokenValidator, unknown::UnknownValidator}, 12 + }; 8 13 9 14 /// Validator for `query` type Lexicon definitions 10 15 /// ··· 165 170 } 166 171 } 167 172 168 - // TODO: Validate parameters if present (must be type "params") 169 - // TODO: Validate output if present (must have encoding, optional schema) 173 + // Validate parameters if present (must be type "params") 174 + if let Some(parameters) = value.get("parameters") { 175 + // Parameters should be validated as a "params" type schema 176 + let params_ctx = ctx.with_path(&format!("{}.parameters", def_name)); 177 + ObjectValidator.validate(parameters, &params_ctx)?; 178 + } 179 + 180 + // Validate output if present (must have encoding, optional schema) 181 + if let Some(output) = value.get("output") { 182 + let _output_ctx = ctx.with_path(&format!("{}.output", def_name)); 183 + 184 + if let Some(output_obj) = output.as_object() { 185 + // Validate required encoding field 186 + if !output_obj.contains_key("encoding") { 187 + return Err(ValidationError::InvalidSchema(format!( 188 + "Query '{}' output missing required 'encoding' field", 189 + def_name 190 + ))); 191 + } 192 + 193 + // Validate optional schema field 194 + if let Some(schema) = output_obj.get("schema") { 195 + // Recursively validate the schema using the appropriate validator 196 + if let Some(schema_type) = schema.get("type").and_then(|t| t.as_str()) { 197 + let schema_ctx = ctx.with_path(&format!("{}.output.schema", def_name)); 198 + 199 + let validation_result = match schema_type { 200 + // Field types 201 + "object" => ObjectValidator.validate(schema, &schema_ctx), 202 + "array" => ArrayValidator.validate(schema, &schema_ctx), 203 + "union" => UnionValidator.validate(schema, &schema_ctx), 204 + "ref" => RefValidator.validate(schema, &schema_ctx), 205 + 206 + // Primitive types 207 + "string" => StringValidator.validate(schema, &schema_ctx), 208 + "integer" => IntegerValidator.validate(schema, &schema_ctx), 209 + "boolean" => BooleanValidator.validate(schema, &schema_ctx), 210 + "bytes" => BytesValidator.validate(schema, &schema_ctx), 211 + "blob" => BlobValidator.validate(schema, &schema_ctx), 212 + "cid-link" => CidLinkValidator.validate(schema, &schema_ctx), 213 + "null" => NullValidator.validate(schema, &schema_ctx), 214 + 215 + // Meta types 216 + "token" => TokenValidator.validate(schema, &schema_ctx), 217 + "unknown" => UnknownValidator.validate(schema, &schema_ctx), 218 + 219 + _ => Err(ValidationError::InvalidSchema(format!( 220 + "Unknown schema type '{}' in query output", schema_type 221 + ))), 222 + }; 223 + 224 + validation_result?; 225 + } 226 + } 227 + } else { 228 + return Err(ValidationError::InvalidSchema(format!( 229 + "Query '{}' output must be an object", 230 + def_name 231 + ))); 232 + } 233 + } 234 + 170 235 // TODO: Validate errors if present (array of objects with name field) 171 236 172 237 Ok(()) ··· 452 517 453 518 let empty_params = json!({}); 454 519 assert!(validator.validate_data(&empty_params, &schema, &ctx).is_ok()); 520 + } 521 + 522 + #[test] 523 + fn test_query_with_broken_reference_in_output() { 524 + let query = json!({ 525 + "type": "query", 526 + "description": "Test query with broken reference in output", 527 + "output": { 528 + "encoding": "application/json", 529 + "schema": { 530 + "type": "object", 531 + "properties": { 532 + "items": { 533 + "type": "array", 534 + "items": { 535 + "type": "ref", 536 + "ref": "#nonExistentDef" 537 + } 538 + } 539 + } 540 + } 541 + } 542 + }); 543 + 544 + let ctx = ValidationContext::builder() 545 + .with_lexicons(vec![json!({ 546 + "lexicon": 1, 547 + "id": "test.broken.ref", 548 + "defs": { 549 + "main": query.clone(), 550 + "existingDef": { 551 + "type": "string" 552 + } 553 + } 554 + })]) 555 + .unwrap() 556 + .build() 557 + .unwrap() 558 + .with_current_lexicon("test.broken.ref"); 559 + 560 + let validator = QueryValidator; 561 + let result = validator.validate(&query, &ctx); 562 + 563 + // Should fail because #nonExistentDef doesn't exist 564 + assert!(result.is_err(), "Query validation should fail for broken reference in output schema"); 565 + 566 + if let Err(error) = result { 567 + let error_msg = format!("{:?}", error); 568 + assert!( 569 + error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"), 570 + "Error should mention the non-existent reference. Got: {}", 571 + error_msg 572 + ); 573 + } 455 574 } 456 575 }
+64
packages/lexicon-rs/src/validation/primary/subscription.rs
··· 5 5 use serde_json::Value; 6 6 use crate::errors::ValidationError; 7 7 use crate::validation::{Validator, ValidationContext}; 8 + use crate::validation::{ 9 + field::{object::ObjectValidator, union::UnionValidator}, 10 + }; 8 11 9 12 /// Validator for `subscription` type Lexicon definitions 10 13 /// ··· 164 167 } 165 168 } 166 169 170 + // Validate parameters if present 171 + if let Some(parameters) = value.get("parameters") { 172 + let params_ctx = ctx.with_path(&format!("{}.parameters", def_name)); 173 + ObjectValidator.validate(parameters, &params_ctx)?; 174 + } 175 + 167 176 // Validate message schema must be union if present 168 177 if let Some(message) = value.get("message") { 169 178 if let Some(schema) = message.get("schema") { ··· 173 182 def_name 174 183 ))); 175 184 } 185 + 186 + // Recursively validate the union schema 187 + let schema_ctx = ctx.with_path(&format!("{}.message.schema", def_name)); 188 + UnionValidator.validate(schema, &schema_ctx)?; 176 189 } else { 177 190 return Err(ValidationError::InvalidSchema(format!( 178 191 "Subscription '{}' message must have a schema field", ··· 466 479 467 480 let validator = SubscriptionValidator; 468 481 assert!(validator.validate(&subscription, &ctx).is_ok()); 482 + } 483 + 484 + #[test] 485 + fn test_subscription_with_broken_reference_in_union() { 486 + let subscription = json!({ 487 + "type": "subscription", 488 + "description": "Test subscription with broken reference in union", 489 + "message": { 490 + "schema": { 491 + "type": "union", 492 + "refs": [ 493 + "#nonExistentDef", 494 + "#existingDef" 495 + ] 496 + } 497 + } 498 + }); 499 + 500 + let ctx = ValidationContext::builder() 501 + .with_lexicons(vec![json!({ 502 + "lexicon": 1, 503 + "id": "test.broken.ref", 504 + "defs": { 505 + "main": subscription.clone(), 506 + "existingDef": { 507 + "type": "object", 508 + "properties": { 509 + "type": {"type": "string"} 510 + } 511 + } 512 + } 513 + })]) 514 + .unwrap() 515 + .build() 516 + .unwrap() 517 + .with_current_lexicon("test.broken.ref"); 518 + 519 + let validator = SubscriptionValidator; 520 + let result = validator.validate(&subscription, &ctx); 521 + 522 + // Should fail because #nonExistentDef doesn't exist 523 + assert!(result.is_err(), "Subscription validation should fail for broken reference in union"); 524 + 525 + if let Err(error) = result { 526 + let error_msg = format!("{:?}", error); 527 + assert!( 528 + error_msg.contains("non-existent") || error_msg.contains("nonExistentDef"), 529 + "Error should mention the non-existent reference. Got: {}", 530 + error_msg 531 + ); 532 + } 469 533 } 470 534 }
+2 -2
packages/lexicon-rs/src/validation/primitive/string.rs
··· 47 47 /// * `Ok(StringFormat)` if the format is valid 48 48 /// * `Err(ValidationError)` if the format is unknown 49 49 fn validate_format_constraint(def_name: &str, format_str: &str) -> Result<StringFormat, ValidationError> { 50 - StringFormat::from_str(format_str).ok_or_else(|| { 50 + format_str.parse::<StringFormat>().map_err(|_| { 51 51 ValidationError::InvalidSchema(format!( 52 52 "String '{}' has unknown format '{}'. Valid formats: datetime, uri, at-uri, did, handle, at-identifier, nsid, cid, language, tid, record-key", 53 53 def_name, format_str ··· 377 377 // Validate format constraints 378 378 if let Some(format_value) = schema.get("format") { 379 379 if let Some(format_str) = format_value.as_str() { 380 - if let Some(format) = StringFormat::from_str(format_str) { 380 + if let Ok(format) = format_str.parse::<StringFormat>() { 381 381 self.validate_string_format(data_str, format, ctx)?; 382 382 } 383 383 }
+3 -7
packages/lexicon-rs/src/validation/resolution.rs
··· 35 35 )); 36 36 } 37 37 38 - let (lexicon_id, def_name) = if reference.starts_with('#') { 38 + let (lexicon_id, def_name) = if let Some(def_name) = reference.strip_prefix('#') { 39 39 // Local reference within current lexicon 40 - let def_name = &reference[1..]; 41 40 if def_name.is_empty() { 42 41 return Err(ValidationError::InvalidSchema( 43 42 "Local reference cannot be just '#'".to_string() ··· 245 244 .map(|r| { 246 245 if r.starts_with('#') { 247 246 format!("{}{}", lexicon_id, r) 248 - } else if !r.contains('#') { 249 - r // Already a full reference to main 250 247 } else { 251 248 r // Already a full reference 252 249 } ··· 262 259 let mut rec_stack = HashSet::new(); 263 260 264 261 for def_id in dependency_graph.keys() { 265 - if !visited.contains(def_id) { 266 - if has_cycle_dfs(def_id, &dependency_graph, &mut visited, &mut rec_stack) { 262 + if !visited.contains(def_id) 263 + && has_cycle_dfs(def_id, &dependency_graph, &mut visited, &mut rec_stack) { 267 264 return Err(ValidationError::InvalidSchema(format!( 268 265 "Circular dependency detected involving definition: {}", def_id 269 266 ))); 270 267 } 271 - } 272 268 } 273 269 274 270 Ok(())
packages/lexicon-rs/test_ref_validation

This is a binary file and will not be displayed.

+52
packages/lexicon-rs/test_ref_validation.rs
··· 1 + use serde_json::json; 2 + use slices_lexicon::validate; 3 + 4 + fn main() { 5 + let broken_lexicon = json!({ 6 + "lexicon": 1, 7 + "id": "network.slices.slice.getSyncSummary", 8 + "defs": { 9 + "main": { 10 + "type": "query", 11 + "description": "Get a summary", 12 + "output": { 13 + "encoding": "application/json", 14 + "schema": { 15 + "type": "object", 16 + "required": ["collectionsSummary"], 17 + "properties": { 18 + "collectionsSummary": { 19 + "type": "array", 20 + "items": { 21 + "type": "ref", 22 + "ref": "#collectionSummar" 23 + } 24 + } 25 + } 26 + } 27 + } 28 + }, 29 + "collectionSummary": { 30 + "type": "object", 31 + "required": ["collection"], 32 + "properties": { 33 + "collection": { 34 + "type": "string" 35 + } 36 + } 37 + } 38 + } 39 + }); 40 + 41 + let lexicons = vec![broken_lexicon]; 42 + 43 + match validate(lexicons) { 44 + Ok(()) => println!("✅ Validation passed (should NOT happen with broken ref)"), 45 + Err(errors) => { 46 + println!("❌ Validation failed (expected):"); 47 + for (lexicon_id, error_list) in errors { 48 + println!(" {}: {:?}", lexicon_id, error_list); 49 + } 50 + } 51 + } 52 + }
+76 -22
packages/lexicon/wasm/README.md
··· 1 - # slices-lexicon 1 + # lexicon-rs 2 + 3 + Rust implementation of AT Protocol lexicon validation. 4 + 5 + ## Overview 6 + 7 + This validation engine can be used in any project that needs AT Protocol lexicon validation. It provides high-performance, spec-compliant validation of AT Protocol lexicon documents and data records. It can also be compiled to WebAssembly for use in JavaScript/TypeScript environments. 8 + 9 + ## Architecture 2 10 3 - AT Protocol lexicon validation library for Rust, compiled to WebAssembly. 11 + This package serves as the core validation engine and is typically consumed by higher-level packages: 12 + 13 + - **`@slices/lexicon`** - TypeScript/Deno package with ergonomic APIs 14 + - **`lexicon-intellisense`** - VS Code extension for lexicon development 15 + - **Slices CLI** - Command-line tooling for lexicon management 4 16 5 17 ## Features 6 18 7 - - 🚀 Fast lexicon validation written in Rust 8 - - 🌐 WebAssembly support for browser and Node.js environments 9 - - 📦 Zero JavaScript dependencies 10 - - 🔒 Type-safe validation with comprehensive error messages 11 - - ✅ Full AT Protocol lexicon specification support 19 + - High-performance lexicon validation implemented in Rust 20 + - Comprehensive error reporting with detailed context 21 + - Full AT Protocol lexicon specification compliance 22 + - Support for all lexicon types: records, queries, procedures, subscriptions 23 + - Optional WebAssembly compilation for JavaScript/TypeScript environments 24 + - Zero JavaScript runtime dependencies when using WebAssembly 25 + 26 + ## Direct Usage 27 + 28 + ### Rust Library 12 29 13 - ## Installation 30 + Add to your `Cargo.toml`: 14 31 15 32 ```toml 16 33 [dependencies] 17 34 slices-lexicon = "0.1" 18 35 ``` 19 36 20 - ## Usage 21 - 22 - ### In Rust 37 + Basic validation: 23 38 24 39 ```rust 25 - use slices_lexicon::{LexiconValidator, ValidationError}; 40 + use slices_lexicon::{validate, validate_record, is_valid_nsid}; 26 41 use serde_json::json; 27 42 43 + // Define lexicon documents 28 44 let lexicons = vec![ 29 45 json!({ 30 46 "id": "com.example.post", 47 + "lexicon": 1, 31 48 "defs": { 32 49 "main": { 33 50 "type": "record", 51 + "key": "tid", 34 52 "record": { 35 53 "type": "object", 54 + "required": ["text"], 36 55 "properties": { 37 - "text": { "type": "string" } 56 + "text": { "type": "string", "maxLength": 300 } 38 57 } 39 58 } 40 59 } ··· 42 61 }) 43 62 ]; 44 63 45 - let validator = LexiconValidator::new(lexicons)?; 64 + // Validate lexicon documents 65 + match validate(lexicons.clone()) { 66 + Ok(()) => println!("All lexicons are valid"), 67 + Err(errors) => { 68 + for (lexicon_id, error_list) in errors { 69 + println!("Errors in {}: {:?}", lexicon_id, error_list); 70 + } 71 + } 72 + } 73 + 74 + // Validate a data record against the schema 46 75 let record = json!({ "text": "Hello, world!" }); 76 + validate_record(lexicons, "com.example.post", record)?; 47 77 48 - validator.validate_record("com.example.post", &record)?; 78 + // Validate NSID format 79 + let is_valid = is_valid_nsid("com.example.post"); 80 + println!("NSID valid: {}", is_valid); 49 81 ``` 50 82 51 - ### As WebAssembly 83 + ### WebAssembly 52 84 53 - Build with wasm-pack: 85 + Build the WASM module: 54 86 55 87 ```bash 56 - wasm-pack build --target web 88 + wasm-pack build --target web --features wasm 57 89 ``` 58 90 59 - Then use in JavaScript/TypeScript: 91 + Use in JavaScript environments: 60 92 61 93 ```javascript 62 - import init, { WasmLexiconValidator } from './pkg/slices_lexicon.js'; 94 + import init, { 95 + WasmLexiconValidator, 96 + validate_lexicons_and_get_errors, 97 + is_valid_nsid 98 + } from './pkg/slices_lexicon.js'; 63 99 64 100 await init(); 65 101 66 - const validator = new WasmLexiconValidator(JSON.stringify(lexicons)); 67 - validator.validate_record("com.example.post", JSON.stringify(record)); 102 + // Validate lexicons 103 + const lexicons = [{ 104 + id: "com.example.post", 105 + lexicon: 1, 106 + defs: { /* ... */ } 107 + }]; 108 + 109 + const errors = validate_lexicons_and_get_errors(JSON.stringify(lexicons)); 110 + console.log('Validation errors:', JSON.parse(errors)); 111 + 112 + // Validate NSID format 113 + const isValid = is_valid_nsid("com.example.post"); 68 114 ``` 115 + 116 + ## JavaScript/TypeScript Usage 117 + 118 + If you're using JavaScript or TypeScript, use the higher-level packages instead of consuming this library directly: 119 + 120 + - **TypeScript/JavaScript**: Use `@slices/lexicon` for ergonomic APIs with automatic resource management 121 + - **VS Code Development**: Install the `lexicon-intellisense` extension 122 + - **CLI Tools**: Use the Slices CLI for lexicon management tasks 69 123 70 124 ## Supported Types 71 125
packages/lexicon/wasm/slices_lexicon_bg.wasm

This is a binary file and will not be displayed.