A better Rust ATProto crate
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}