1use url::Url;
4
5use crate::{Error, ErrorKind, ObjectRef, Result, Timestamp};
6
7pub const MAX_VOCABULARY_ID_BYTES: usize = 512;
9pub const MAX_PROPERTY_ID_BYTES: usize = 255;
11pub const MAX_METADATA_TEXT_BYTES: usize = 1024 * 1024;
13pub const MAX_METADATA_URI_BYTES: usize = 4096;
15pub const MAX_METADATA_BINARY_BYTES: usize = 15 * 1024 * 1024;
17pub const MAX_LANGUAGE_TAG_BYTES: usize = 64;
19pub const MAX_METADATA_COLLECTION_ITEMS: usize = 4096;
21pub const MAX_METADATA_DECIMAL_SCALE: u32 = 1024;
23pub const MAX_METADATA_NESTING_DEPTH: usize = 32;
25pub const MAX_METADATA_TOTAL_BYTES: usize = 15 * 1024 * 1024;
27
28#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct VocabularyId(String);
34
35impl VocabularyId {
36 pub fn new(value: impl Into<String>) -> Result<Self> {
43 let value = value.into();
44 validate_identifier("metadata vocabulary", &value, MAX_VOCABULARY_ID_BYTES)?;
45 Ok(Self(value))
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53
54 #[must_use]
56 pub fn into_string(self) -> String {
57 self.0
58 }
59}
60
61#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct PropertyId(String);
64
65impl PropertyId {
66 pub fn new(value: impl Into<String>) -> Result<Self> {
73 let value = value.into();
74 validate_identifier("metadata property", &value, MAX_PROPERTY_ID_BYTES)?;
75 Ok(Self(value))
76 }
77
78 #[must_use]
80 pub fn as_str(&self) -> &str {
81 &self.0
82 }
83
84 #[must_use]
86 pub fn into_string(self) -> String {
87 self.0
88 }
89}
90
91#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub struct MetadataProperty {
94 vocabulary: VocabularyId,
95 property: PropertyId,
96}
97
98impl MetadataProperty {
99 #[must_use]
101 pub const fn new(vocabulary: VocabularyId, property: PropertyId) -> Self {
102 Self {
103 vocabulary,
104 property,
105 }
106 }
107
108 #[must_use]
110 pub const fn vocabulary(&self) -> &VocabularyId {
111 &self.vocabulary
112 }
113
114 #[must_use]
116 pub const fn property(&self) -> &PropertyId {
117 &self.property
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
123pub struct DecimalValue {
124 coefficient: i128,
125 scale: u32,
126}
127
128impl DecimalValue {
129 pub fn new(mut coefficient: i128, mut scale: u32) -> Result<Self> {
136 while scale > 0 && coefficient % 10 == 0 {
137 coefficient /= 10;
138 scale -= 1;
139 }
140 if scale > MAX_METADATA_DECIMAL_SCALE {
141 return Err(limit_error(
142 "metadata decimal scale",
143 MAX_METADATA_DECIMAL_SCALE as usize,
144 ));
145 }
146 Ok(Self { coefficient, scale })
147 }
148
149 #[must_use]
151 pub const fn coefficient(self) -> i128 {
152 self.coefficient
153 }
154
155 #[must_use]
157 pub const fn scale(self) -> u32 {
158 self.scale
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
164pub struct RationalValue {
165 numerator: i64,
166 denominator: u64,
167}
168
169impl RationalValue {
170 pub fn new(numerator: i64, denominator: u64) -> Result<Self> {
176 if denominator == 0 {
177 return Err(Error::new(
178 ErrorKind::InvalidArgument,
179 "metadata rational denominator must not be zero",
180 ));
181 }
182 let divisor = greatest_common_divisor(numerator.unsigned_abs(), denominator);
183 let normalized_numerator = match i64::try_from(divisor) {
184 Ok(divisor) => numerator / divisor,
185 Err(_) => -1,
186 };
187 Ok(Self {
188 numerator: normalized_numerator,
189 denominator: denominator / divisor,
190 })
191 }
192
193 #[must_use]
195 pub const fn numerator(self) -> i64 {
196 self.numerator
197 }
198
199 #[must_use]
201 pub const fn denominator(self) -> u64 {
202 self.denominator
203 }
204}
205
206#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
208#[non_exhaustive]
209pub enum MetadataValueKind {
210 String,
212 LangString,
214 I64,
216 U64,
218 Decimal,
220 Bool,
222 Timestamp,
224 Uri,
226 Bytes,
228 Rational,
230 List,
232 Struct,
234 Reference,
236}
237
238#[derive(Clone, Debug, Eq, Hash, PartialEq)]
239enum MetadataValueInner {
240 String(String),
241 LangString { value: String, language: String },
242 I64(i64),
243 U64(u64),
244 Decimal(DecimalValue),
245 Bool(bool),
246 Timestamp(Timestamp),
247 Uri(String),
248 Bytes(Vec<u8>),
249 Rational(RationalValue),
250 List(Vec<MetadataValue>),
251 Struct(Vec<MetadataField>),
252 Reference(ObjectRef),
253}
254
255#[derive(Clone, Debug, Eq, Hash, PartialEq)]
260pub struct MetadataValue {
261 inner: MetadataValueInner,
262 depth: usize,
263 payload_bytes: usize,
264}
265
266impl MetadataValue {
267 pub fn string(value: impl Into<String>) -> Result<Self> {
274 let value = value.into();
275 validate_text("metadata string", &value, MAX_METADATA_TEXT_BYTES)?;
276 let payload_bytes = value.len();
277 Ok(Self::leaf(MetadataValueInner::String(value), payload_bytes))
278 }
279
280 pub fn language_string(value: impl Into<String>, language: impl Into<String>) -> Result<Self> {
287 let value = value.into();
288 let language = language.into();
289 validate_text("metadata language string", &value, MAX_METADATA_TEXT_BYTES)?;
290 validate_language_tag(&language)?;
291 let payload_bytes = checked_payload_sum([value.len(), language.len()])?;
292 Ok(Self::leaf(
293 MetadataValueInner::LangString { value, language },
294 payload_bytes,
295 ))
296 }
297
298 #[must_use]
300 pub const fn i64(value: i64) -> Self {
301 Self::leaf(MetadataValueInner::I64(value), size_of::<i64>())
302 }
303
304 #[must_use]
306 pub const fn u64(value: u64) -> Self {
307 Self::leaf(MetadataValueInner::U64(value), size_of::<u64>())
308 }
309
310 #[must_use]
312 pub const fn decimal(value: DecimalValue) -> Self {
313 Self::leaf(
314 MetadataValueInner::Decimal(value),
315 size_of::<i128>() + size_of::<u32>(),
316 )
317 }
318
319 #[must_use]
321 pub const fn boolean(value: bool) -> Self {
322 Self::leaf(MetadataValueInner::Bool(value), 1)
323 }
324
325 #[must_use]
327 pub const fn timestamp(value: Timestamp) -> Self {
328 Self::leaf(MetadataValueInner::Timestamp(value), size_of::<i64>())
329 }
330
331 pub fn uri(value: impl Into<String>) -> Result<Self> {
338 let value = value.into();
339 validate_text("metadata URI", &value, MAX_METADATA_URI_BYTES)?;
340 Url::parse(&value).map_err(|error| {
341 Error::new(
342 ErrorKind::InvalidArgument,
343 format!("metadata URI is invalid: {error}"),
344 )
345 })?;
346 let payload_bytes = value.len();
347 Ok(Self::leaf(MetadataValueInner::Uri(value), payload_bytes))
348 }
349
350 pub fn bytes(value: Vec<u8>) -> Result<Self> {
357 if value.len() > MAX_METADATA_BINARY_BYTES {
358 return Err(limit_error(
359 "metadata binary value",
360 MAX_METADATA_BINARY_BYTES,
361 ));
362 }
363 let payload_bytes = value.len();
364 Ok(Self::leaf(MetadataValueInner::Bytes(value), payload_bytes))
365 }
366
367 #[must_use]
369 pub const fn rational(value: RationalValue) -> Self {
370 Self::leaf(
371 MetadataValueInner::Rational(value),
372 size_of::<i64>() + size_of::<u64>(),
373 )
374 }
375
376 pub fn list(values: Vec<Self>) -> Result<Self> {
383 if values.len() > MAX_METADATA_COLLECTION_ITEMS {
384 return Err(limit_error(
385 "metadata list item count",
386 MAX_METADATA_COLLECTION_ITEMS,
387 ));
388 }
389 let depth = composite_depth(values.iter().map(|value| value.depth))?;
390 let payload_bytes = checked_payload_sum(values.iter().map(|value| value.payload_bytes))?;
391 Ok(Self {
392 inner: MetadataValueInner::List(values),
393 depth,
394 payload_bytes,
395 })
396 }
397
398 pub fn structure(fields: Vec<MetadataField>) -> Result<Self> {
405 if fields.len() > MAX_METADATA_COLLECTION_ITEMS {
406 return Err(limit_error(
407 "metadata structure field count",
408 MAX_METADATA_COLLECTION_ITEMS,
409 ));
410 }
411 let depth = composite_depth(fields.iter().map(|field| field.value.depth))?;
412 let payload_bytes = checked_payload_sum(
413 fields
414 .iter()
415 .flat_map(|field| [field.name.as_str().len(), field.value.payload_bytes]),
416 )?;
417 Ok(Self {
418 inner: MetadataValueInner::Struct(fields),
419 depth,
420 payload_bytes,
421 })
422 }
423
424 #[must_use]
426 pub const fn reference(value: ObjectRef) -> Self {
427 Self::leaf(MetadataValueInner::Reference(value), 17)
428 }
429
430 #[must_use]
432 pub const fn kind(&self) -> MetadataValueKind {
433 match self.inner {
434 MetadataValueInner::String(_) => MetadataValueKind::String,
435 MetadataValueInner::LangString { .. } => MetadataValueKind::LangString,
436 MetadataValueInner::I64(_) => MetadataValueKind::I64,
437 MetadataValueInner::U64(_) => MetadataValueKind::U64,
438 MetadataValueInner::Decimal(_) => MetadataValueKind::Decimal,
439 MetadataValueInner::Bool(_) => MetadataValueKind::Bool,
440 MetadataValueInner::Timestamp(_) => MetadataValueKind::Timestamp,
441 MetadataValueInner::Uri(_) => MetadataValueKind::Uri,
442 MetadataValueInner::Bytes(_) => MetadataValueKind::Bytes,
443 MetadataValueInner::Rational(_) => MetadataValueKind::Rational,
444 MetadataValueInner::List(_) => MetadataValueKind::List,
445 MetadataValueInner::Struct(_) => MetadataValueKind::Struct,
446 MetadataValueInner::Reference(_) => MetadataValueKind::Reference,
447 }
448 }
449
450 #[must_use]
452 pub fn as_string(&self) -> Option<&str> {
453 match &self.inner {
454 MetadataValueInner::String(value) => Some(value),
455 _ => None,
456 }
457 }
458
459 #[must_use]
461 pub fn as_language_string(&self) -> Option<(&str, &str)> {
462 match &self.inner {
463 MetadataValueInner::LangString { value, language } => Some((value, language)),
464 _ => None,
465 }
466 }
467
468 #[must_use]
470 pub const fn as_i64(&self) -> Option<i64> {
471 match self.inner {
472 MetadataValueInner::I64(value) => Some(value),
473 _ => None,
474 }
475 }
476
477 #[must_use]
479 pub const fn as_u64(&self) -> Option<u64> {
480 match self.inner {
481 MetadataValueInner::U64(value) => Some(value),
482 _ => None,
483 }
484 }
485
486 #[must_use]
488 pub const fn as_decimal(&self) -> Option<DecimalValue> {
489 match self.inner {
490 MetadataValueInner::Decimal(value) => Some(value),
491 _ => None,
492 }
493 }
494
495 #[must_use]
497 pub const fn as_bool(&self) -> Option<bool> {
498 match self.inner {
499 MetadataValueInner::Bool(value) => Some(value),
500 _ => None,
501 }
502 }
503
504 #[must_use]
506 pub const fn as_timestamp(&self) -> Option<Timestamp> {
507 match self.inner {
508 MetadataValueInner::Timestamp(value) => Some(value),
509 _ => None,
510 }
511 }
512
513 #[must_use]
515 pub fn as_uri(&self) -> Option<&str> {
516 match &self.inner {
517 MetadataValueInner::Uri(value) => Some(value),
518 _ => None,
519 }
520 }
521
522 #[must_use]
524 pub fn as_bytes(&self) -> Option<&[u8]> {
525 match &self.inner {
526 MetadataValueInner::Bytes(value) => Some(value),
527 _ => None,
528 }
529 }
530
531 #[must_use]
533 pub const fn as_rational(&self) -> Option<RationalValue> {
534 match self.inner {
535 MetadataValueInner::Rational(value) => Some(value),
536 _ => None,
537 }
538 }
539
540 #[must_use]
542 pub fn as_list(&self) -> Option<&[Self]> {
543 match &self.inner {
544 MetadataValueInner::List(values) => Some(values),
545 _ => None,
546 }
547 }
548
549 #[must_use]
551 pub fn as_structure(&self) -> Option<&[MetadataField]> {
552 match &self.inner {
553 MetadataValueInner::Struct(fields) => Some(fields),
554 _ => None,
555 }
556 }
557
558 #[must_use]
560 pub const fn as_reference(&self) -> Option<ObjectRef> {
561 match self.inner {
562 MetadataValueInner::Reference(value) => Some(value),
563 _ => None,
564 }
565 }
566
567 #[must_use]
569 pub const fn nesting_depth(&self) -> usize {
570 self.depth
571 }
572
573 #[must_use]
575 pub const fn payload_bytes(&self) -> usize {
576 self.payload_bytes
577 }
578
579 const fn leaf(inner: MetadataValueInner, payload_bytes: usize) -> Self {
580 Self {
581 inner,
582 depth: 1,
583 payload_bytes,
584 }
585 }
586}
587
588#[derive(Clone, Debug, Eq, Hash, PartialEq)]
590pub struct MetadataField {
591 name: PropertyId,
592 value: MetadataValue,
593}
594
595impl MetadataField {
596 #[must_use]
598 pub const fn new(name: PropertyId, value: MetadataValue) -> Self {
599 Self { name, value }
600 }
601
602 #[must_use]
604 pub const fn name(&self) -> &PropertyId {
605 &self.name
606 }
607
608 #[must_use]
610 pub const fn value(&self) -> &MetadataValue {
611 &self.value
612 }
613}
614
615#[derive(Clone, Debug, Eq, Hash, PartialEq)]
617pub struct MetadataAssertion {
618 property: MetadataProperty,
619 value: MetadataValue,
620}
621
622#[derive(Clone, Debug, Eq, Hash, PartialEq)]
624pub struct MetadataMatch {
625 target: ObjectRef,
626 assertion: MetadataAssertion,
627}
628
629impl MetadataMatch {
630 #[must_use]
632 pub const fn new(target: ObjectRef, assertion: MetadataAssertion) -> Self {
633 Self { target, assertion }
634 }
635
636 #[must_use]
638 pub const fn target(&self) -> ObjectRef {
639 self.target
640 }
641
642 #[must_use]
644 pub const fn assertion(&self) -> &MetadataAssertion {
645 &self.assertion
646 }
647}
648
649impl MetadataAssertion {
650 #[must_use]
652 pub const fn new(property: MetadataProperty, value: MetadataValue) -> Self {
653 Self { property, value }
654 }
655
656 #[must_use]
658 pub const fn property(&self) -> &MetadataProperty {
659 &self.property
660 }
661
662 #[must_use]
664 pub const fn value(&self) -> &MetadataValue {
665 &self.value
666 }
667}
668
669fn validate_identifier(label: &str, value: &str, maximum: usize) -> Result<()> {
670 if value.is_empty() || value.len() > maximum {
671 return Err(Error::new(
672 ErrorKind::InvalidArgument,
673 format!("{label} must contain 1-{maximum} UTF-8 bytes"),
674 ));
675 }
676 if value.chars().any(char::is_whitespace) || value.chars().any(char::is_control) {
677 return Err(Error::new(
678 ErrorKind::InvalidArgument,
679 format!("{label} must not contain whitespace or control characters"),
680 ));
681 }
682 Ok(())
683}
684
685fn validate_text(label: &str, value: &str, maximum: usize) -> Result<()> {
686 if value.len() > maximum || value.contains('\0') {
687 return Err(Error::new(
688 ErrorKind::InvalidArgument,
689 format!("{label} must contain at most {maximum} UTF-8 bytes without NUL"),
690 ));
691 }
692 Ok(())
693}
694
695fn validate_language_tag(value: &str) -> Result<()> {
696 if value.is_empty()
697 || value.len() > MAX_LANGUAGE_TAG_BYTES
698 || !value.is_ascii()
699 || value.starts_with('-')
700 || value.ends_with('-')
701 || value
702 .bytes()
703 .any(|byte| !byte.is_ascii_alphanumeric() && byte != b'-')
704 {
705 return Err(Error::new(
706 ErrorKind::InvalidArgument,
707 "metadata language tag must be 1-64 ASCII letters, digits, or separated '-' characters",
708 ));
709 }
710 if value.split('-').any(str::is_empty) {
711 return Err(Error::new(
712 ErrorKind::InvalidArgument,
713 "metadata language tag must not contain empty subtags",
714 ));
715 }
716 Ok(())
717}
718
719fn composite_depth(child_depths: impl Iterator<Item = usize>) -> Result<usize> {
720 let depth = child_depths.max().unwrap_or(0).saturating_add(1);
721 if depth > MAX_METADATA_NESTING_DEPTH {
722 return Err(limit_error(
723 "metadata value nesting depth",
724 MAX_METADATA_NESTING_DEPTH,
725 ));
726 }
727 Ok(depth)
728}
729
730fn checked_payload_sum(values: impl IntoIterator<Item = usize>) -> Result<usize> {
731 let mut total = 0_usize;
732 for value in values {
733 total = total
734 .checked_add(value)
735 .ok_or_else(|| limit_error("metadata aggregate payload", MAX_METADATA_TOTAL_BYTES))?;
736 if total > MAX_METADATA_TOTAL_BYTES {
737 return Err(limit_error(
738 "metadata aggregate payload",
739 MAX_METADATA_TOTAL_BYTES,
740 ));
741 }
742 }
743 Ok(total)
744}
745
746fn limit_error(label: &str, maximum: usize) -> Error {
747 Error::new(
748 ErrorKind::InvalidArgument,
749 format!("{label} exceeds the supported limit of {maximum}"),
750 )
751}
752
753const fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 {
754 while right != 0 {
755 let remainder = left % right;
756 left = right;
757 right = remainder;
758 }
759 left
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765 use crate::{AssetId, RepresentationId};
766
767 fn property(name: &str) -> MetadataProperty {
768 MetadataProperty::new(
769 VocabularyId::new("https://example.com/vocabulary").expect("valid vocabulary"),
770 PropertyId::new(name).expect("valid property"),
771 )
772 }
773
774 #[test]
775 fn unknown_vocabulary_and_property_round_trip_exactly() {
776 let property = MetadataProperty::new(
777 VocabularyId::new("com.Example.Custom/1").expect("valid vocabulary"),
778 PropertyId::new("CameraSerialNumber").expect("valid property"),
779 );
780
781 assert_eq!(property.vocabulary().as_str(), "com.Example.Custom/1");
782 assert_eq!(property.property().as_str(), "CameraSerialNumber");
783 }
784
785 #[test]
786 fn primitive_values_are_typed_and_exact() {
787 let string = MetadataValue::string("Interview A").expect("valid string");
788 let lang = MetadataValue::language_string("Colour", "en-GB").expect("valid language");
789 let uri = MetadataValue::uri("urn:example:Asset%201").expect("valid URI");
790 let bytes = MetadataValue::bytes(vec![0, 1, 255]).expect("valid bytes");
791 let decimal = MetadataValue::decimal(DecimalValue::new(12_340, 3).unwrap());
792 let rational = MetadataValue::rational(RationalValue::new(48_000, 2_000).unwrap());
793
794 assert_eq!(string.kind(), MetadataValueKind::String);
795 assert_eq!(string.as_string(), Some("Interview A"));
796 assert_eq!(lang.as_language_string(), Some(("Colour", "en-GB")));
797 assert_eq!(uri.as_uri(), Some("urn:example:Asset%201"));
798 assert_eq!(bytes.as_bytes(), Some(&[0, 1, 255][..]));
799 assert_eq!(
800 decimal.as_decimal(),
801 Some(DecimalValue::new(1_234, 2).unwrap())
802 );
803 assert_eq!(
804 rational.as_rational(),
805 Some(RationalValue::new(24, 1).unwrap())
806 );
807 assert_eq!(MetadataValue::i64(-4).as_i64(), Some(-4));
808 assert_eq!(MetadataValue::u64(4).as_u64(), Some(4));
809 assert_eq!(MetadataValue::boolean(true).as_bool(), Some(true));
810 assert_eq!(
811 MetadataValue::timestamp(Timestamp::from_unix_micros(42)).as_timestamp(),
812 Some(Timestamp::from_unix_micros(42))
813 );
814 }
815
816 #[test]
817 fn repeated_assertions_are_distinct_from_a_list_value() {
818 let assertions = [
819 MetadataAssertion::new(property("keyword"), MetadataValue::string("one").unwrap()),
820 MetadataAssertion::new(property("keyword"), MetadataValue::string("two").unwrap()),
821 ];
822 let list = MetadataValue::list(vec![
823 MetadataValue::string("one").unwrap(),
824 MetadataValue::string("two").unwrap(),
825 ])
826 .unwrap();
827
828 assert_eq!(assertions.len(), 2);
829 assert_eq!(list.as_list().expect("list").len(), 2);
830 assert_ne!(assertions[0].value(), &list);
831 }
832
833 #[test]
834 fn structured_values_preserve_field_order_and_references() {
835 let asset = ObjectRef::Asset(AssetId::from_bytes([7; 16]));
836 let representation = ObjectRef::Representation(RepresentationId::from_bytes([8; 16]));
837 let value = MetadataValue::structure(vec![
838 MetadataField::new(
839 PropertyId::new("asset").unwrap(),
840 MetadataValue::reference(asset),
841 ),
842 MetadataField::new(
843 PropertyId::new("representation").unwrap(),
844 MetadataValue::reference(representation),
845 ),
846 ])
847 .expect("valid structure");
848
849 let fields = value.as_structure().expect("structure");
850 assert_eq!(fields[0].name().as_str(), "asset");
851 assert_eq!(fields[0].value().as_reference(), Some(asset));
852 assert_eq!(fields[1].name().as_str(), "representation");
853 assert_eq!(fields[1].value().as_reference(), Some(representation));
854 }
855
856 #[test]
857 fn invalid_identifiers_language_tags_and_uris_are_rejected() {
858 assert_eq!(
859 VocabularyId::new("contains whitespace")
860 .expect_err("invalid vocabulary")
861 .kind(),
862 ErrorKind::InvalidArgument
863 );
864 assert_eq!(
865 PropertyId::new("").expect_err("empty property").kind(),
866 ErrorKind::InvalidArgument
867 );
868 for language in ["", "-en", "en--GB", "en_uk", "dé"] {
869 assert_eq!(
870 MetadataValue::language_string("value", language)
871 .expect_err("invalid language")
872 .kind(),
873 ErrorKind::InvalidArgument
874 );
875 }
876 assert_eq!(
877 MetadataValue::uri("not a uri")
878 .expect_err("invalid URI")
879 .kind(),
880 ErrorKind::InvalidArgument
881 );
882 }
883
884 #[test]
885 fn recursive_values_enforce_depth_and_payload_limits() {
886 let mut value = MetadataValue::string("leaf").unwrap();
887 for _ in 1..MAX_METADATA_NESTING_DEPTH {
888 value = MetadataValue::list(vec![value]).expect("depth remains valid");
889 }
890 assert_eq!(value.nesting_depth(), MAX_METADATA_NESTING_DEPTH);
891 assert_eq!(
892 MetadataValue::list(vec![value])
893 .expect_err("excess depth must fail")
894 .kind(),
895 ErrorKind::InvalidArgument
896 );
897
898 let chunk = MetadataValue::bytes(vec![0; MAX_METADATA_TOTAL_BYTES / 2 + 1]).unwrap();
899 assert_eq!(
900 MetadataValue::list(vec![chunk.clone(), chunk])
901 .expect_err("excess aggregate payload must fail")
902 .kind(),
903 ErrorKind::InvalidArgument
904 );
905 }
906
907 #[test]
908 fn rational_normalization_handles_signed_boundaries() {
909 assert_eq!(
910 RationalValue::new(i64::MIN, 1_u64 << 63).unwrap(),
911 RationalValue::new(-1, 1).unwrap()
912 );
913 assert_eq!(
914 RationalValue::new(1, 0)
915 .expect_err("zero denominator")
916 .kind(),
917 ErrorKind::InvalidArgument
918 );
919 }
920
921 #[test]
922 fn decimal_scale_is_bounded_after_normalization() {
923 assert_eq!(
924 DecimalValue::new(1, MAX_METADATA_DECIMAL_SCALE + 1)
925 .expect_err("oversized scale")
926 .kind(),
927 ErrorKind::InvalidArgument
928 );
929 assert_eq!(
930 DecimalValue::new(10, MAX_METADATA_DECIMAL_SCALE + 1).unwrap(),
931 DecimalValue::new(1, MAX_METADATA_DECIMAL_SCALE).unwrap()
932 );
933 }
934}