Skip to main content

openbim_core/
detected.rs

1//! Version detection that reports its own evidence.
2
3/// How a document's version was determined.
4///
5/// # The trap this closes
6///
7/// Several openBIM schemas reuse one XML namespace across incompatible
8/// versions. IDS is the extreme case: every published version from 0.2 to
9/// 1.0 declares a byte-identical `targetNamespace`, and the differences are in
10/// attribute *names* and cardinality rather than element names. A reader that
11/// guesses wrong therefore does not fail — it silently yields a *different*
12/// specification.
13///
14/// So detection returns its evidence, and a file whose declared version
15/// disagrees with its actual shape produces [`Detected::Conflict`] rather than
16/// a silent pick.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Detected<V> {
19    /// The document stated its version and the content agreed.
20    Declared(V),
21    /// The version was inferred from document shape; nothing declared it.
22    Inferred(V),
23    /// The declared version and the observed shape disagree.
24    ///
25    /// Never resolve this silently. Which one is correct is a policy decision
26    /// belonging to the caller, not the parser.
27    Conflict {
28        /// What the document claimed.
29        declared: V,
30        /// What its shape indicates.
31        observed: V,
32    },
33}
34
35impl<V: Copy> Detected<V> {
36    /// The version to use when the caller has no opinion about conflicts.
37    ///
38    /// Returns `None` for [`Detected::Conflict`] — resolving that is the
39    /// caller's decision, and defaulting it here would reintroduce exactly the
40    /// silent-wrong-parse failure this type exists to surface.
41    ///
42    /// ```
43    /// use openbim_core::Detected;
44    /// assert_eq!(Detected::Declared(1u8).resolved(), Some(1));
45    /// assert_eq!(
46    ///     Detected::Conflict { declared: 1u8, observed: 2 }.resolved(),
47    ///     None
48    /// );
49    /// ```
50    #[must_use]
51    pub fn resolved(&self) -> Option<V> {
52        match self {
53            Detected::Declared(v) | Detected::Inferred(v) => Some(*v),
54            Detected::Conflict { .. } => None,
55        }
56    }
57
58    /// Whether detection found contradictory evidence.
59    #[must_use]
60    pub fn is_conflict(&self) -> bool {
61        matches!(self, Detected::Conflict { .. })
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn conflict_refuses_to_resolve_itself() {
71        assert_eq!(Detected::Declared(3u8).resolved(), Some(3));
72        assert_eq!(Detected::Inferred(3u8).resolved(), Some(3));
73
74        let conflict = Detected::Conflict {
75            declared: 2u8,
76            observed: 3,
77        };
78        assert_eq!(conflict.resolved(), None);
79        assert!(conflict.is_conflict());
80    }
81
82    #[test]
83    fn agreement_is_not_a_conflict() {
84        assert!(!Detected::Declared(1u8).is_conflict());
85        assert!(!Detected::Inferred(1u8).is_conflict());
86    }
87}