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}