Skip to main content

postproject_core/
metadata.rs

1//! Standards-aware, typed metadata values.
2
3use url::Url;
4
5use crate::{Error, ErrorKind, ObjectRef, Result, Timestamp};
6
7/// Maximum UTF-8 byte length of a vocabulary identifier.
8pub const MAX_VOCABULARY_ID_BYTES: usize = 512;
9/// Maximum UTF-8 byte length of a vocabulary-local property identifier.
10pub const MAX_PROPERTY_ID_BYTES: usize = 255;
11/// Maximum UTF-8 byte length of one text value.
12pub const MAX_METADATA_TEXT_BYTES: usize = 1024 * 1024;
13/// Maximum UTF-8 byte length of one URI value.
14pub const MAX_METADATA_URI_BYTES: usize = 4096;
15/// Maximum byte length of one binary value.
16pub const MAX_METADATA_BINARY_BYTES: usize = 15 * 1024 * 1024;
17/// Maximum byte length of a language tag.
18pub const MAX_LANGUAGE_TAG_BYTES: usize = 64;
19/// Maximum number of direct children in a list or structured value.
20pub const MAX_METADATA_COLLECTION_ITEMS: usize = 4096;
21/// Maximum number of fractional decimal digits.
22pub const MAX_METADATA_DECIMAL_SCALE: u32 = 1024;
23/// Maximum nesting depth of lists and structured values, including the root.
24pub const MAX_METADATA_NESTING_DEPTH: usize = 32;
25/// Maximum aggregate payload size of one metadata value.
26pub const MAX_METADATA_TOTAL_BYTES: usize = 15 * 1024 * 1024;
27
28/// An extensible identifier for a metadata vocabulary or namespace.
29///
30/// Core preserves the caller's exact identifier. A vocabulary registry may
31/// interpret it, but a registry is not required to store unknown metadata.
32#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct VocabularyId(String);
34
35impl VocabularyId {
36    /// Creates a bounded vocabulary identifier.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`ErrorKind::InvalidArgument`] for empty, oversized, or
41    /// whitespace/control-containing input.
42    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    /// Returns the exact identifier supplied by the caller.
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// Consumes the identifier and returns its string.
55    #[must_use]
56    pub fn into_string(self) -> String {
57        self.0
58    }
59}
60
61/// A property name local to a metadata vocabulary.
62#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct PropertyId(String);
64
65impl PropertyId {
66    /// Creates a bounded vocabulary-local property identifier.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`ErrorKind::InvalidArgument`] for empty, oversized, or
71    /// whitespace/control-containing input.
72    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    /// Returns the exact identifier supplied by the caller.
79    #[must_use]
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83
84    /// Consumes the identifier and returns its string.
85    #[must_use]
86    pub fn into_string(self) -> String {
87        self.0
88    }
89}
90
91/// A globally meaningful property formed from a vocabulary and local name.
92#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub struct MetadataProperty {
94    vocabulary: VocabularyId,
95    property: PropertyId,
96}
97
98impl MetadataProperty {
99    /// Creates a metadata property identity.
100    #[must_use]
101    pub const fn new(vocabulary: VocabularyId, property: PropertyId) -> Self {
102        Self {
103            vocabulary,
104            property,
105        }
106    }
107
108    /// Returns the vocabulary or namespace identity.
109    #[must_use]
110    pub const fn vocabulary(&self) -> &VocabularyId {
111        &self.vocabulary
112    }
113
114    /// Returns the vocabulary-local property identity.
115    #[must_use]
116    pub const fn property(&self) -> &PropertyId {
117        &self.property
118    }
119}
120
121/// An exact base-ten decimal represented as `coefficient * 10^-scale`.
122#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
123pub struct DecimalValue {
124    coefficient: i128,
125    scale: u32,
126}
127
128impl DecimalValue {
129    /// Creates a decimal and removes insignificant trailing fractional zeros.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`ErrorKind::InvalidArgument`] when the normalized scale
134    /// exceeds [`MAX_METADATA_DECIMAL_SCALE`].
135    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    /// Returns the signed base-ten coefficient.
150    #[must_use]
151    pub const fn coefficient(self) -> i128 {
152        self.coefficient
153    }
154
155    /// Returns the number of fractional decimal digits.
156    #[must_use]
157    pub const fn scale(self) -> u32 {
158        self.scale
159    }
160}
161
162/// An exact normalized rational number.
163#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
164pub struct RationalValue {
165    numerator: i64,
166    denominator: u64,
167}
168
169impl RationalValue {
170    /// Creates a rational value reduced to lowest terms.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`ErrorKind::InvalidArgument`] when `denominator` is zero.
175    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    /// Returns the normalized numerator.
194    #[must_use]
195    pub const fn numerator(self) -> i64 {
196        self.numerator
197    }
198
199    /// Returns the positive normalized denominator.
200    #[must_use]
201    pub const fn denominator(self) -> u64 {
202        self.denominator
203    }
204}
205
206/// The inspectable type of a [`MetadataValue`].
207#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
208#[non_exhaustive]
209pub enum MetadataValueKind {
210    /// Unqualified UTF-8 text.
211    String,
212    /// UTF-8 text with a language tag.
213    LangString,
214    /// Signed 64-bit integer.
215    I64,
216    /// Unsigned 64-bit integer.
217    U64,
218    /// Exact base-ten decimal.
219    Decimal,
220    /// Boolean.
221    Bool,
222    /// Signed Unix-microsecond timestamp.
223    Timestamp,
224    /// URI text validated without normalization.
225    Uri,
226    /// Opaque bytes.
227    Bytes,
228    /// Exact rational number.
229    Rational,
230    /// Ordered nested values.
231    List,
232    /// Ordered named fields.
233    Struct,
234    /// Reference to another `PostProject` object.
235    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/// A bounded, recursively typed metadata value.
256///
257/// Constructors preserve ordering and exact text while enforcing limits. The
258/// private representation prevents callers from bypassing those invariants.
259#[derive(Clone, Debug, Eq, Hash, PartialEq)]
260pub struct MetadataValue {
261    inner: MetadataValueInner,
262    depth: usize,
263    payload_bytes: usize,
264}
265
266impl MetadataValue {
267    /// Creates an unqualified text value.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`ErrorKind::InvalidArgument`] when the text exceeds its byte
272    /// limit or contains NUL.
273    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    /// Creates language-tagged text with a syntax-safe BCP 47-shaped tag.
281    ///
282    /// # Errors
283    ///
284    /// Returns [`ErrorKind::InvalidArgument`] when the text is invalid or the
285    /// language tag is empty, oversized, or not syntactically safe.
286    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    /// Creates a signed integer value.
299    #[must_use]
300    pub const fn i64(value: i64) -> Self {
301        Self::leaf(MetadataValueInner::I64(value), size_of::<i64>())
302    }
303
304    /// Creates an unsigned integer value.
305    #[must_use]
306    pub const fn u64(value: u64) -> Self {
307        Self::leaf(MetadataValueInner::U64(value), size_of::<u64>())
308    }
309
310    /// Creates an exact decimal value.
311    #[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    /// Creates a boolean value.
320    #[must_use]
321    pub const fn boolean(value: bool) -> Self {
322        Self::leaf(MetadataValueInner::Bool(value), 1)
323    }
324
325    /// Creates a timestamp value.
326    #[must_use]
327    pub const fn timestamp(value: Timestamp) -> Self {
328        Self::leaf(MetadataValueInner::Timestamp(value), size_of::<i64>())
329    }
330
331    /// Creates a URI value while preserving its exact spelling.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`ErrorKind::InvalidArgument`] when the URI is invalid,
336    /// oversized, or contains NUL.
337    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    /// Creates an opaque binary value.
351    ///
352    /// # Errors
353    ///
354    /// Returns [`ErrorKind::InvalidArgument`] when the value exceeds
355    /// [`MAX_METADATA_BINARY_BYTES`].
356    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    /// Creates an exact rational value.
368    #[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    /// Creates an ordered list.
377    ///
378    /// # Errors
379    ///
380    /// Returns [`ErrorKind::InvalidArgument`] when the item count, aggregate
381    /// payload size, or nesting depth exceeds its documented limit.
382    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    /// Creates an ordered structured value.
399    ///
400    /// # Errors
401    ///
402    /// Returns [`ErrorKind::InvalidArgument`] when the field count, aggregate
403    /// payload size, or nesting depth exceeds its documented limit.
404    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    /// Creates a reference to another `PostProject` object.
425    #[must_use]
426    pub const fn reference(value: ObjectRef) -> Self {
427        Self::leaf(MetadataValueInner::Reference(value), 17)
428    }
429
430    /// Returns the value's inspectable type.
431    #[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    /// Returns unqualified text, or `None` for another type.
451    #[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    /// Returns language-tagged text and its tag, or `None` for another type.
460    #[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    /// Returns a signed integer, or `None` for another type.
469    #[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    /// Returns an unsigned integer, or `None` for another type.
478    #[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    /// Returns an exact decimal, or `None` for another type.
487    #[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    /// Returns a boolean, or `None` for another type.
496    #[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    /// Returns a timestamp, or `None` for another type.
505    #[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    /// Returns URI text, or `None` for another type.
514    #[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    /// Returns opaque bytes, or `None` for another type.
523    #[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    /// Returns an exact rational, or `None` for another type.
532    #[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    /// Returns ordered list items, or `None` for another type.
541    #[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    /// Returns ordered structured fields, or `None` for another type.
550    #[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    /// Returns an object reference, or `None` for another type.
559    #[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    /// Returns this value's nesting depth, including the root value.
568    #[must_use]
569    pub const fn nesting_depth(&self) -> usize {
570        self.depth
571    }
572
573    /// Returns the bounded aggregate payload size used for validation.
574    #[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/// One named member of an ordered structured metadata value.
589#[derive(Clone, Debug, Eq, Hash, PartialEq)]
590pub struct MetadataField {
591    name: PropertyId,
592    value: MetadataValue,
593}
594
595impl MetadataField {
596    /// Creates a structured metadata field.
597    #[must_use]
598    pub const fn new(name: PropertyId, value: MetadataValue) -> Self {
599        Self { name, value }
600    }
601
602    /// Returns the vocabulary-local field name.
603    #[must_use]
604    pub const fn name(&self) -> &PropertyId {
605        &self.name
606    }
607
608    /// Returns the field value.
609    #[must_use]
610    pub const fn value(&self) -> &MetadataValue {
611        &self.value
612    }
613}
614
615/// One independently repeatable assertion about a target object.
616#[derive(Clone, Debug, Eq, Hash, PartialEq)]
617pub struct MetadataAssertion {
618    property: MetadataProperty,
619    value: MetadataValue,
620}
621
622/// A metadata assertion together with the object carrying it.
623#[derive(Clone, Debug, Eq, Hash, PartialEq)]
624pub struct MetadataMatch {
625    target: ObjectRef,
626    assertion: MetadataAssertion,
627}
628
629impl MetadataMatch {
630    /// Creates a property-query result.
631    #[must_use]
632    pub const fn new(target: ObjectRef, assertion: MetadataAssertion) -> Self {
633        Self { target, assertion }
634    }
635
636    /// Returns the object carrying the assertion.
637    #[must_use]
638    pub const fn target(&self) -> ObjectRef {
639        self.target
640    }
641
642    /// Returns the matching assertion.
643    #[must_use]
644    pub const fn assertion(&self) -> &MetadataAssertion {
645        &self.assertion
646    }
647}
648
649impl MetadataAssertion {
650    /// Creates an assertion. Repetition is represented by multiple assertions.
651    #[must_use]
652    pub const fn new(property: MetadataProperty, value: MetadataValue) -> Self {
653        Self { property, value }
654    }
655
656    /// Returns the asserted property.
657    #[must_use]
658    pub const fn property(&self) -> &MetadataProperty {
659        &self.property
660    }
661
662    /// Returns the asserted value.
663    #[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}