A better Rust ATProto crate
at main 1181 lines 39 kB view raw
1//! Tests for Data validation against lexicon schemas 2 3use super::*; 4use crate::{lexicon::*, schema::LexiconSchema}; 5use jacquard_common::{ 6 CowStr, 7 smol_str::ToSmolStr, 8 types::{string::AtprotoStr, value::Data}, 9}; 10use std::collections::BTreeMap; 11 12// Helper to create plain string Data 13fn data_string(s: &str) -> Data<'static> { 14 use smol_str::ToSmolStr; 15 Data::String(AtprotoStr::String(CowStr::Owned(s.to_smolstr()))) 16} 17 18// Test schema: Simple object with required string field 19struct SimpleSchema; 20 21impl LexiconSchema for SimpleSchema { 22 fn nsid() -> &'static str { 23 "test.simple" 24 } 25 26 fn def_name() -> &'static str { 27 "main" 28 } 29 30 fn lexicon_doc() -> LexiconDoc<'static> { 31 LexiconDoc { 32 lexicon: Lexicon::Lexicon1, 33 id: CowStr::new_static("test.simple"), 34 revision: None, 35 description: None, 36 defs: { 37 let mut defs = BTreeMap::new(); 38 defs.insert( 39 "main".into(), 40 LexUserType::Object(LexObject { 41 description: None, 42 required: Some(vec!["text".into()]), 43 nullable: None, 44 properties: { 45 let mut props = BTreeMap::new(); 46 props.insert( 47 "text".into(), 48 LexObjectProperty::String(LexString { 49 description: None, 50 format: None, 51 default: None, 52 min_length: None, 53 max_length: None, 54 min_graphemes: None, 55 max_graphemes: None, 56 r#enum: None, 57 r#const: None, 58 known_values: None, 59 }), 60 ); 61 props 62 }, 63 }), 64 ); 65 defs 66 }, 67 } 68 } 69} 70 71#[test] 72fn test_valid_simple_object() { 73 let validator = SchemaValidator::new(); 74 validator 75 .registry() 76 .insert("test.simple".to_smolstr(), SimpleSchema::lexicon_doc()); 77 78 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 79 "text".into(), 80 data_string("hello"), 81 )]))); 82 83 let result = validator.validate::<SimpleSchema>(&data).unwrap(); 84 assert!( 85 result.is_valid(), 86 "Expected valid, got: {:?}", 87 result.structural_errors() 88 ); 89} 90 91#[test] 92fn test_missing_required_field() { 93 let validator = SchemaValidator::new(); 94 validator 95 .registry() 96 .insert("test.simple".to_smolstr(), SimpleSchema::lexicon_doc()); 97 98 // Empty object - missing required 'text' field 99 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::new())); 100 101 let result = validator.validate::<SimpleSchema>(&data).unwrap(); 102 assert!(!result.is_valid()); 103 104 let errors = result.structural_errors(); 105 assert_eq!(errors.len(), 1); 106 assert!(matches!( 107 &errors[0], 108 StructuralError::MissingRequiredField { field, .. } if field.as_str() == "text" 109 )); 110} 111 112#[test] 113fn test_type_mismatch() { 114 let validator = SchemaValidator::new(); 115 validator 116 .registry() 117 .insert("test.simple".to_smolstr(), SimpleSchema::lexicon_doc()); 118 119 // 'text' field is integer instead of string 120 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 121 "text".into(), 122 Data::Integer(42), 123 )]))); 124 125 let result = validator.validate::<SimpleSchema>(&data).unwrap(); 126 assert!(!result.is_valid()); 127 128 let errors = result.structural_errors(); 129 assert_eq!(errors.len(), 1); 130 match &errors[0] { 131 StructuralError::TypeMismatch { 132 expected, actual, .. 133 } => { 134 assert!(matches!( 135 expected, 136 jacquard_common::types::DataModelType::String(_) 137 )); 138 assert!(matches!( 139 actual, 140 jacquard_common::types::DataModelType::Integer 141 )); 142 } 143 _ => panic!("Expected TypeMismatch error"), 144 } 145} 146 147// Test schema: Union with $type discriminator 148struct UnionSchema; 149 150impl LexiconSchema for UnionSchema { 151 fn nsid() -> &'static str { 152 "test.union" 153 } 154 155 fn lexicon_doc() -> LexiconDoc<'static> { 156 LexiconDoc { 157 lexicon: Lexicon::Lexicon1, 158 id: CowStr::new_static("test.union"), 159 revision: None, 160 description: None, 161 defs: { 162 let mut defs = BTreeMap::new(); 163 defs.insert( 164 "main".into(), 165 LexUserType::Object(LexObject { 166 description: None, 167 required: Some(vec!["content".into()]), 168 nullable: None, 169 properties: { 170 let mut props = BTreeMap::new(); 171 props.insert( 172 "content".into(), 173 LexObjectProperty::Union(LexRefUnion { 174 description: None, 175 refs: vec!["#text".into(), "#image".into()], 176 closed: Some(true), 177 }), 178 ); 179 props 180 }, 181 }), 182 ); 183 defs.insert( 184 "text".into(), 185 LexUserType::Object(LexObject { 186 description: None, 187 required: Some(vec!["value".into()]), 188 nullable: None, 189 properties: { 190 let mut props = BTreeMap::new(); 191 props.insert( 192 "value".into(), 193 LexObjectProperty::String(LexString { 194 description: None, 195 format: None, 196 default: None, 197 min_length: None, 198 max_length: None, 199 min_graphemes: None, 200 max_graphemes: None, 201 r#enum: None, 202 r#const: None, 203 known_values: None, 204 }), 205 ); 206 props 207 }, 208 }), 209 ); 210 defs.insert( 211 "image".into(), 212 LexUserType::Object(LexObject { 213 description: None, 214 required: Some(vec!["url".into()]), 215 nullable: None, 216 properties: { 217 let mut props = BTreeMap::new(); 218 props.insert( 219 "url".into(), 220 LexObjectProperty::String(LexString { 221 description: None, 222 format: None, 223 default: None, 224 min_length: None, 225 max_length: None, 226 min_graphemes: None, 227 max_graphemes: None, 228 r#enum: None, 229 r#const: None, 230 known_values: None, 231 }), 232 ); 233 props 234 }, 235 }), 236 ); 237 defs 238 }, 239 } 240 } 241} 242 243#[test] 244fn test_union_missing_discriminator() { 245 let validator = SchemaValidator::new(); 246 validator 247 .registry() 248 .insert("test.union".to_smolstr(), UnionSchema::lexicon_doc()); 249 250 // Union object without $type field 251 let content = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 252 "value".into(), 253 data_string("hello"), 254 )]))); 255 256 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 257 "content".into(), 258 content, 259 )]))); 260 261 let result = validator.validate::<UnionSchema>(&data).unwrap(); 262 assert!(!result.is_valid()); 263 264 let errors = result.structural_errors(); 265 assert!( 266 errors 267 .iter() 268 .any(|e| matches!(e, StructuralError::MissingUnionDiscriminator { .. })) 269 ); 270} 271 272#[test] 273fn test_union_invalid_type() { 274 let validator = SchemaValidator::new(); 275 validator 276 .registry() 277 .insert("test.union".to_smolstr(), UnionSchema::lexicon_doc()); 278 279 // Union with $type that doesn't match any variant 280 let content = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([ 281 ("$type".into(), data_string("test.union#unknown")), 282 ("value".into(), data_string("hello")), 283 ]))); 284 285 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 286 "content".into(), 287 content, 288 )]))); 289 290 let result = validator.validate::<UnionSchema>(&data).unwrap(); 291 assert!(!result.is_valid()); 292 293 let errors = result.structural_errors(); 294 assert!( 295 errors 296 .iter() 297 .any(|e| matches!(e, StructuralError::UnionNoMatch { .. })) 298 ); 299} 300 301#[test] 302fn test_union_valid_variant() { 303 let validator = SchemaValidator::new(); 304 validator 305 .registry() 306 .insert("test.union".to_smolstr(), UnionSchema::lexicon_doc()); 307 308 // Valid text variant 309 let content = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([ 310 ("$type".into(), data_string("test.union#text")), 311 ("value".into(), data_string("hello")), 312 ]))); 313 314 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 315 "content".into(), 316 content, 317 )]))); 318 319 let result = validator.validate::<UnionSchema>(&data).unwrap(); 320 assert!( 321 result.is_valid(), 322 "Expected valid, got: {:?}", 323 result.structural_errors() 324 ); 325} 326 327// Test schema: Array validation 328struct ArraySchema; 329 330impl LexiconSchema for ArraySchema { 331 fn nsid() -> &'static str { 332 "test.array" 333 } 334 335 fn lexicon_doc() -> LexiconDoc<'static> { 336 LexiconDoc { 337 lexicon: Lexicon::Lexicon1, 338 id: CowStr::new_static("test.array"), 339 revision: None, 340 description: None, 341 defs: { 342 let mut defs = BTreeMap::new(); 343 defs.insert( 344 "main".into(), 345 LexUserType::Object(LexObject { 346 description: None, 347 required: Some(vec!["items".into()]), 348 nullable: None, 349 properties: { 350 let mut props = BTreeMap::new(); 351 props.insert( 352 "items".into(), 353 LexObjectProperty::Array(LexArray { 354 description: None, 355 items: LexArrayItem::String(LexString { 356 description: None, 357 format: None, 358 default: None, 359 min_length: None, 360 max_length: None, 361 min_graphemes: None, 362 max_graphemes: None, 363 r#enum: None, 364 r#const: None, 365 known_values: None, 366 }), 367 min_length: None, 368 max_length: None, 369 }), 370 ); 371 props 372 }, 373 }), 374 ); 375 defs 376 }, 377 } 378 } 379} 380 381#[test] 382fn test_array_valid_items() { 383 let validator = SchemaValidator::new(); 384 validator 385 .registry() 386 .insert("test.array".to_smolstr(), ArraySchema::lexicon_doc()); 387 388 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 389 "items".into(), 390 Data::Array(jacquard_common::types::value::Array(vec![ 391 data_string("one"), 392 data_string("two"), 393 data_string("three"), 394 ])), 395 )]))); 396 397 let result = validator.validate::<ArraySchema>(&data).unwrap(); 398 assert!( 399 result.is_valid(), 400 "Expected valid, got: {:?}", 401 result.structural_errors() 402 ); 403} 404 405#[test] 406fn test_array_invalid_item_type() { 407 let validator = SchemaValidator::new(); 408 validator 409 .registry() 410 .insert("test.array".to_smolstr(), ArraySchema::lexicon_doc()); 411 412 // Second item is integer instead of string 413 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 414 "items".into(), 415 Data::Array(jacquard_common::types::value::Array(vec![ 416 data_string("one"), 417 Data::Integer(42), 418 data_string("three"), 419 ])), 420 )]))); 421 422 let result = validator.validate::<ArraySchema>(&data).unwrap(); 423 assert!(!result.is_valid()); 424 425 let errors = result.structural_errors(); 426 assert!(errors.iter().any(|e| { 427 matches!(e, StructuralError::TypeMismatch { expected, actual, .. } 428 if matches!(expected, jacquard_common::types::DataModelType::String(_)) 429 && matches!(actual, jacquard_common::types::DataModelType::Integer)) 430 })); 431} 432 433#[test] 434fn test_nested_objects() { 435 // Test schema with nested object 436 struct NestedSchema; 437 impl LexiconSchema for NestedSchema { 438 fn nsid() -> &'static str { 439 "test.nested" 440 } 441 442 fn lexicon_doc() -> LexiconDoc<'static> { 443 LexiconDoc { 444 lexicon: Lexicon::Lexicon1, 445 id: CowStr::new_static("test.nested"), 446 revision: None, 447 description: None, 448 defs: { 449 let mut defs = BTreeMap::new(); 450 defs.insert( 451 "main".into(), 452 LexUserType::Object(LexObject { 453 description: None, 454 required: Some(vec!["meta".into()]), 455 nullable: None, 456 properties: { 457 let mut props = BTreeMap::new(); 458 props.insert( 459 "meta".into(), 460 LexObjectProperty::Object(LexObject { 461 description: None, 462 required: Some(vec!["title".into()]), 463 nullable: None, 464 properties: { 465 let mut meta_props = BTreeMap::new(); 466 meta_props.insert( 467 "title".into(), 468 LexObjectProperty::String(LexString { 469 description: None, 470 format: None, 471 default: None, 472 min_length: None, 473 max_length: None, 474 min_graphemes: None, 475 max_graphemes: None, 476 r#enum: None, 477 r#const: None, 478 known_values: None, 479 }), 480 ); 481 meta_props 482 }, 483 }), 484 ); 485 props 486 }, 487 }), 488 ); 489 defs 490 }, 491 } 492 } 493 } 494 495 let validator = SchemaValidator::new(); 496 validator 497 .registry() 498 .insert("test.nested".to_smolstr(), NestedSchema::lexicon_doc()); 499 500 // Nested object missing required field 501 let meta = Data::Object(jacquard_common::types::value::Object(BTreeMap::new())); 502 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 503 "meta".into(), 504 meta, 505 )]))); 506 507 let result = validator.validate::<NestedSchema>(&data).unwrap(); 508 assert!(!result.is_valid()); 509 510 let errors = result.structural_errors(); 511 assert!(errors.iter().any(|e| matches!( 512 e, 513 StructuralError::MissingRequiredField { field, .. } if field.as_str() == "title" 514 ))); 515} 516 517// ============================================================================ 518// CONSTRAINT VALIDATION TESTS (Phase 4) 519// ============================================================================ 520 521// Schema with string constraints 522struct StringConstraintSchema; 523 524impl LexiconSchema for StringConstraintSchema { 525 fn nsid() -> &'static str { 526 "test.string.constraints" 527 } 528 529 fn lexicon_doc() -> LexiconDoc<'static> { 530 LexiconDoc { 531 lexicon: Lexicon::Lexicon1, 532 id: CowStr::new_static("test.string.constraints"), 533 revision: None, 534 description: None, 535 defs: { 536 let mut defs = BTreeMap::new(); 537 defs.insert( 538 "main".into(), 539 LexUserType::Object(LexObject { 540 description: None, 541 required: Some(vec!["text".into()]), 542 nullable: None, 543 properties: { 544 let mut props = BTreeMap::new(); 545 props.insert( 546 "text".into(), 547 LexObjectProperty::String(LexString { 548 description: None, 549 format: None, 550 default: None, 551 min_length: Some(5), 552 max_length: Some(20), 553 min_graphemes: None, 554 max_graphemes: None, 555 r#enum: None, 556 r#const: None, 557 known_values: None, 558 }), 559 ); 560 props 561 }, 562 }), 563 ); 564 defs 565 }, 566 } 567 } 568} 569 570// Schema with grapheme constraints 571struct GraphemeConstraintSchema; 572 573impl LexiconSchema for GraphemeConstraintSchema { 574 fn nsid() -> &'static str { 575 "test.grapheme.constraints" 576 } 577 578 fn lexicon_doc() -> LexiconDoc<'static> { 579 LexiconDoc { 580 lexicon: Lexicon::Lexicon1, 581 id: CowStr::new_static("test.grapheme.constraints"), 582 revision: None, 583 description: None, 584 defs: { 585 let mut defs = BTreeMap::new(); 586 defs.insert( 587 "main".into(), 588 LexUserType::Object(LexObject { 589 description: None, 590 required: Some(vec!["text".into()]), 591 nullable: None, 592 properties: { 593 let mut props = BTreeMap::new(); 594 props.insert( 595 "text".into(), 596 LexObjectProperty::String(LexString { 597 description: None, 598 format: None, 599 default: None, 600 min_length: None, 601 max_length: None, 602 min_graphemes: Some(2), 603 max_graphemes: Some(5), 604 r#enum: None, 605 r#const: None, 606 known_values: None, 607 }), 608 ); 609 props 610 }, 611 }), 612 ); 613 defs 614 }, 615 } 616 } 617} 618 619// Schema with integer constraints 620struct IntegerConstraintSchema; 621 622impl LexiconSchema for IntegerConstraintSchema { 623 fn nsid() -> &'static str { 624 "test.integer.constraints" 625 } 626 627 fn lexicon_doc() -> LexiconDoc<'static> { 628 LexiconDoc { 629 lexicon: Lexicon::Lexicon1, 630 id: CowStr::new_static("test.integer.constraints"), 631 revision: None, 632 description: None, 633 defs: { 634 let mut defs = BTreeMap::new(); 635 defs.insert( 636 "main".into(), 637 LexUserType::Object(LexObject { 638 description: None, 639 required: Some(vec!["value".into()]), 640 nullable: None, 641 properties: { 642 let mut props = BTreeMap::new(); 643 props.insert( 644 "value".into(), 645 LexObjectProperty::Integer(LexInteger { 646 description: None, 647 default: None, 648 minimum: Some(0), 649 maximum: Some(100), 650 r#enum: None, 651 r#const: None, 652 }), 653 ); 654 props 655 }, 656 }), 657 ); 658 defs 659 }, 660 } 661 } 662} 663 664// Schema with array length constraints 665struct ArrayConstraintSchema; 666 667impl LexiconSchema for ArrayConstraintSchema { 668 fn nsid() -> &'static str { 669 "test.array.constraints" 670 } 671 672 fn lexicon_doc() -> LexiconDoc<'static> { 673 LexiconDoc { 674 lexicon: Lexicon::Lexicon1, 675 id: CowStr::new_static("test.array.constraints"), 676 revision: None, 677 description: None, 678 defs: { 679 let mut defs = BTreeMap::new(); 680 defs.insert( 681 "main".into(), 682 LexUserType::Object(LexObject { 683 description: None, 684 required: Some(vec!["items".into()]), 685 nullable: None, 686 properties: { 687 let mut props = BTreeMap::new(); 688 props.insert( 689 "items".into(), 690 LexObjectProperty::Array(LexArray { 691 description: None, 692 items: LexArrayItem::String(LexString { 693 description: None, 694 format: None, 695 default: None, 696 min_length: None, 697 max_length: None, 698 min_graphemes: None, 699 max_graphemes: None, 700 r#enum: None, 701 r#const: None, 702 known_values: None, 703 }), 704 min_length: Some(2), 705 max_length: Some(5), 706 }), 707 ); 708 props 709 }, 710 }), 711 ); 712 defs 713 }, 714 } 715 } 716} 717 718#[test] 719fn test_constraint_validation_is_lazy() { 720 let validator = SchemaValidator::new(); 721 validator.registry().insert( 722 "test.string.constraints".to_smolstr(), 723 StringConstraintSchema::lexicon_doc(), 724 ); 725 726 // String too long (21 chars, max is 20) 727 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 728 "text".into(), 729 data_string("this string is too long!"), 730 )]))); 731 732 let result = validator.validate::<StringConstraintSchema>(&data).unwrap(); 733 734 // Structurally valid - type is correct, required field present 735 assert!(result.is_structurally_valid()); 736 737 // But overall invalid due to constraint violation 738 assert!(!result.is_valid()); 739} 740 741#[test] 742fn test_string_max_length() { 743 let validator = SchemaValidator::new(); 744 validator.registry().insert( 745 "test.string.constraints".to_smolstr(), 746 StringConstraintSchema::lexicon_doc(), 747 ); 748 749 // String exceeding max_length (25 chars, max is 20) 750 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 751 "text".into(), 752 data_string("this string is way too long"), 753 )]))); 754 755 let result = validator.validate::<StringConstraintSchema>(&data).unwrap(); 756 757 assert!(!result.is_valid()); 758 assert!(result.is_structurally_valid()); 759 assert!(result.has_constraint_violations()); 760 761 let constraint_errors = result.constraint_errors(); 762 assert_eq!(constraint_errors.len(), 1); 763 assert!(matches!( 764 &constraint_errors[0], 765 ConstraintError::MaxLength { 766 max: 20, 767 actual: 27, 768 .. 769 } 770 )); 771} 772 773#[test] 774fn test_string_min_length() { 775 let validator = SchemaValidator::new(); 776 validator.registry().insert( 777 "test.string.constraints".to_smolstr(), 778 StringConstraintSchema::lexicon_doc(), 779 ); 780 781 // String below min_length (3 chars, min is 5) 782 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 783 "text".into(), 784 data_string("hi"), 785 )]))); 786 787 let result = validator.validate::<StringConstraintSchema>(&data).unwrap(); 788 789 assert!(!result.is_valid()); 790 assert!(result.is_structurally_valid()); 791 792 let constraint_errors = result.constraint_errors(); 793 assert_eq!(constraint_errors.len(), 1); 794 assert!(matches!( 795 &constraint_errors[0], 796 ConstraintError::MinLength { 797 min: 5, 798 actual: 2, 799 .. 800 } 801 )); 802} 803 804#[test] 805fn test_string_max_graphemes() { 806 let validator = SchemaValidator::new(); 807 validator.registry().insert( 808 "test.grapheme.constraints".to_smolstr(), 809 GraphemeConstraintSchema::lexicon_doc(), 810 ); 811 812 // 6 emoji graphemes (max is 5) 813 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 814 "text".into(), 815 data_string("👍👍👍👍👍👍"), 816 )]))); 817 818 let result = validator 819 .validate::<GraphemeConstraintSchema>(&data) 820 .unwrap(); 821 822 assert!(!result.is_valid()); 823 assert!(result.is_structurally_valid()); 824 825 let constraint_errors = result.constraint_errors(); 826 assert_eq!(constraint_errors.len(), 1); 827 assert!(matches!( 828 &constraint_errors[0], 829 ConstraintError::MaxGraphemes { 830 max: 5, 831 actual: 6, 832 .. 833 } 834 )); 835} 836 837#[test] 838fn test_string_min_graphemes() { 839 let validator = SchemaValidator::new(); 840 validator.registry().insert( 841 "test.grapheme.constraints".to_smolstr(), 842 GraphemeConstraintSchema::lexicon_doc(), 843 ); 844 845 // 1 emoji grapheme (min is 2) 846 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 847 "text".into(), 848 data_string("👍"), 849 )]))); 850 851 let result = validator 852 .validate::<GraphemeConstraintSchema>(&data) 853 .unwrap(); 854 855 assert!(!result.is_valid()); 856 assert!(result.is_structurally_valid()); 857 858 let constraint_errors = result.constraint_errors(); 859 assert_eq!(constraint_errors.len(), 1); 860 assert!(matches!( 861 &constraint_errors[0], 862 ConstraintError::MinGraphemes { 863 min: 2, 864 actual: 1, 865 .. 866 } 867 )); 868} 869 870#[test] 871fn test_string_within_constraints() { 872 let validator = SchemaValidator::new(); 873 validator.registry().insert( 874 "test.string.constraints".to_smolstr(), 875 StringConstraintSchema::lexicon_doc(), 876 ); 877 878 // Valid string (10 chars, within 5-20 range) 879 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 880 "text".into(), 881 data_string("valid text"), 882 )]))); 883 884 let result = validator.validate::<StringConstraintSchema>(&data).unwrap(); 885 886 assert!(result.is_valid()); 887 assert!(result.is_structurally_valid()); 888 assert!(!result.has_constraint_violations()); 889} 890 891#[test] 892fn test_integer_maximum() { 893 let validator = SchemaValidator::new(); 894 validator.registry().insert( 895 "test.integer.constraints".to_smolstr(), 896 IntegerConstraintSchema::lexicon_doc(), 897 ); 898 899 // Integer exceeding maximum (150 > 100) 900 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 901 "value".into(), 902 Data::Integer(150), 903 )]))); 904 905 let result = validator 906 .validate::<IntegerConstraintSchema>(&data) 907 .unwrap(); 908 909 assert!(!result.is_valid()); 910 assert!(result.is_structurally_valid()); 911 912 let constraint_errors = result.constraint_errors(); 913 assert_eq!(constraint_errors.len(), 1); 914 assert!(matches!( 915 &constraint_errors[0], 916 ConstraintError::Maximum { 917 max: 100, 918 actual: 150, 919 .. 920 } 921 )); 922} 923 924#[test] 925fn test_integer_minimum() { 926 let validator = SchemaValidator::new(); 927 validator.registry().insert( 928 "test.integer.constraints".to_smolstr(), 929 IntegerConstraintSchema::lexicon_doc(), 930 ); 931 932 // Integer below minimum (-5 < 0) 933 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 934 "value".into(), 935 Data::Integer(-5), 936 )]))); 937 938 let result = validator 939 .validate::<IntegerConstraintSchema>(&data) 940 .unwrap(); 941 942 assert!(!result.is_valid()); 943 assert!(result.is_structurally_valid()); 944 945 let constraint_errors = result.constraint_errors(); 946 assert_eq!(constraint_errors.len(), 1); 947 assert!(matches!( 948 &constraint_errors[0], 949 ConstraintError::Minimum { 950 min: 0, 951 actual: -5, 952 .. 953 } 954 )); 955} 956 957#[test] 958fn test_integer_within_constraints() { 959 let validator = SchemaValidator::new(); 960 validator.registry().insert( 961 "test.integer.constraints".to_smolstr(), 962 IntegerConstraintSchema::lexicon_doc(), 963 ); 964 965 // Valid integer (50 is within 0-100 range) 966 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 967 "value".into(), 968 Data::Integer(50), 969 )]))); 970 971 let result = validator 972 .validate::<IntegerConstraintSchema>(&data) 973 .unwrap(); 974 975 assert!(result.is_valid()); 976 assert!(result.is_structurally_valid()); 977 assert!(!result.has_constraint_violations()); 978} 979 980#[test] 981fn test_array_max_length() { 982 let validator = SchemaValidator::new(); 983 validator.registry().insert( 984 "test.array.constraints".to_smolstr(), 985 ArrayConstraintSchema::lexicon_doc(), 986 ); 987 988 // Array with too many items (6 items, max is 5) 989 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 990 "items".into(), 991 Data::Array(jacquard_common::types::value::Array(vec![ 992 data_string("one"), 993 data_string("two"), 994 data_string("three"), 995 data_string("four"), 996 data_string("five"), 997 data_string("six"), 998 ])), 999 )]))); 1000 1001 let result = validator.validate::<ArrayConstraintSchema>(&data).unwrap(); 1002 1003 assert!(!result.is_valid()); 1004 assert!(result.is_structurally_valid()); 1005 1006 let constraint_errors = result.constraint_errors(); 1007 assert_eq!(constraint_errors.len(), 1); 1008 assert!(matches!( 1009 &constraint_errors[0], 1010 ConstraintError::MaxLength { 1011 max: 5, 1012 actual: 6, 1013 .. 1014 } 1015 )); 1016} 1017 1018#[test] 1019fn test_array_min_length() { 1020 let validator = SchemaValidator::new(); 1021 validator.registry().insert( 1022 "test.array.constraints".to_smolstr(), 1023 ArrayConstraintSchema::lexicon_doc(), 1024 ); 1025 1026 // Array with too few items (1 item, min is 2) 1027 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 1028 "items".into(), 1029 Data::Array(jacquard_common::types::value::Array(vec![data_string( 1030 "one", 1031 )])), 1032 )]))); 1033 1034 let result = validator.validate::<ArrayConstraintSchema>(&data).unwrap(); 1035 1036 assert!(!result.is_valid()); 1037 assert!(result.is_structurally_valid()); 1038 1039 let constraint_errors = result.constraint_errors(); 1040 assert_eq!(constraint_errors.len(), 1); 1041 assert!(matches!( 1042 &constraint_errors[0], 1043 ConstraintError::MinLength { 1044 min: 2, 1045 actual: 1, 1046 .. 1047 } 1048 )); 1049} 1050 1051#[test] 1052fn test_array_within_constraints() { 1053 let validator = SchemaValidator::new(); 1054 validator.registry().insert( 1055 "test.array.constraints".to_smolstr(), 1056 ArrayConstraintSchema::lexicon_doc(), 1057 ); 1058 1059 // Valid array (3 items, within 2-5 range) 1060 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 1061 "items".into(), 1062 Data::Array(jacquard_common::types::value::Array(vec![ 1063 data_string("one"), 1064 data_string("two"), 1065 data_string("three"), 1066 ])), 1067 )]))); 1068 1069 let result = validator.validate::<ArrayConstraintSchema>(&data).unwrap(); 1070 1071 assert!(result.is_valid()); 1072 assert!(result.is_structurally_valid()); 1073 assert!(!result.has_constraint_violations()); 1074} 1075 1076#[test] 1077fn test_structurally_invalid_skips_constraints() { 1078 let validator = SchemaValidator::new(); 1079 validator.registry().insert( 1080 "test.string.constraints".to_smolstr(), 1081 StringConstraintSchema::lexicon_doc(), 1082 ); 1083 1084 // Structurally invalid: integer instead of string 1085 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 1086 "text".into(), 1087 Data::Integer(42), 1088 )]))); 1089 1090 let result = validator.validate::<StringConstraintSchema>(&data).unwrap(); 1091 1092 assert!(!result.is_valid()); 1093 assert!(!result.is_structurally_valid()); 1094 1095 // Structural errors should be present 1096 assert_eq!(result.structural_errors().len(), 1); 1097 1098 // Constraint checking should be skipped or return empty 1099 // (implementation detail: may or may not compute constraints for structurally invalid data) 1100} 1101 1102#[test] 1103fn test_structurally_valid_with_constraint_errors() { 1104 let validator = SchemaValidator::new(); 1105 validator.registry().insert( 1106 "test.string.constraints".to_smolstr(), 1107 StringConstraintSchema::lexicon_doc(), 1108 ); 1109 1110 // Structurally valid but violates constraints 1111 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 1112 "text".into(), 1113 data_string("too long string here!!!"), 1114 )]))); 1115 1116 let result = validator.validate::<StringConstraintSchema>(&data).unwrap(); 1117 1118 assert!(!result.is_valid()); 1119 assert!(result.is_structurally_valid()); 1120 assert!(result.has_constraint_violations()); 1121 1122 // Both structural and constraint errors should be separate 1123 assert_eq!(result.structural_errors().len(), 0); 1124 assert!(result.constraint_errors().len() > 0); 1125} 1126 1127#[test] 1128fn test_validate_structural_only() { 1129 let validator = SchemaValidator::new(); 1130 validator.registry().insert( 1131 "test.string.constraints".to_smolstr(), 1132 StringConstraintSchema::lexicon_doc(), 1133 ); 1134 1135 // String too long (violates constraints) 1136 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 1137 "text".into(), 1138 data_string("this string is way too long"), 1139 )]))); 1140 1141 // Use structural validation only 1142 let result = validator.validate_structural::<StringConstraintSchema>(&data); 1143 1144 // Structurally valid - type is correct, required field present 1145 assert!(result.is_structurally_valid()); 1146 1147 // No constraint errors computed 1148 assert_eq!(result.constraint_errors().len(), 0); 1149 1150 // Result should be StructuralOnly variant 1151 match result { 1152 ValidationResult::StructuralOnly { .. } => {} 1153 ValidationResult::Complete { .. } => panic!("Expected StructuralOnly variant"), 1154 } 1155} 1156 1157#[test] 1158fn test_validate_structural_only_with_errors() { 1159 let validator = SchemaValidator::new(); 1160 validator.registry().insert( 1161 "test.string.constraints".to_smolstr(), 1162 StringConstraintSchema::lexicon_doc(), 1163 ); 1164 1165 // Structurally invalid: integer instead of string 1166 let data = Data::Object(jacquard_common::types::value::Object(BTreeMap::from([( 1167 "text".into(), 1168 Data::Integer(42), 1169 )]))); 1170 1171 let result = validator.validate_structural::<StringConstraintSchema>(&data); 1172 1173 // Not structurally valid 1174 assert!(!result.is_structurally_valid()); 1175 1176 // Structural errors should be present 1177 assert_eq!(result.structural_errors().len(), 1); 1178 1179 // No constraint errors 1180 assert_eq!(result.constraint_errors().len(), 0); 1181}