Skip to main content

sim_lib_doc_core/
fidelity.rs

1//! Fidelity reporting for file codecs and service placements.
2
3/// Uniform report for data preserved, dropped, or warned about at a boundary.
4#[derive(Clone, Debug, Default, PartialEq, Eq)]
5pub struct FidelityReport {
6    /// Backend that produced the report, such as `codec/ooxml-xlsx` or
7    /// `site/msgraph`.
8    pub backend: String,
9    /// Fields or records the boundary could not preserve.
10    pub dropped: Vec<LossNote>,
11    /// Backend-specific data preserved outside the portable document body.
12    pub preserved_extras: Vec<String>,
13    /// Non-fatal diagnostics that should remain visible to callers.
14    pub warnings: Vec<String>,
15}
16
17impl FidelityReport {
18    /// Builds an empty report for one backend.
19    #[must_use]
20    pub fn new(backend: impl Into<String>) -> Self {
21        Self {
22            backend: backend.into(),
23            ..Self::default()
24        }
25    }
26
27    /// Reports whether the boundary preserved the document without known loss.
28    #[must_use]
29    pub fn is_lossless(&self) -> bool {
30        self.dropped.is_empty()
31    }
32
33    /// Appends one dropped-field note.
34    #[must_use]
35    pub fn with_dropped(mut self, field: impl Into<String>, reason: impl Into<String>) -> Self {
36        self.dropped.push(LossNote::new(field, reason));
37        self
38    }
39
40    /// Appends one preserved-extra marker.
41    #[must_use]
42    pub fn with_preserved_extra(mut self, extra: impl Into<String>) -> Self {
43        self.preserved_extras.push(extra.into());
44        self
45    }
46
47    /// Appends one non-fatal warning.
48    #[must_use]
49    pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
50        self.warnings.push(warning.into());
51        self
52    }
53}
54
55/// One field or record lost at a file or service boundary.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct LossNote {
58    /// Portable field or backend record path that was dropped.
59    pub field: String,
60    /// Human-readable reason for the loss.
61    pub reason: String,
62}
63
64impl LossNote {
65    /// Builds a loss note.
66    #[must_use]
67    pub fn new(field: impl Into<String>, reason: impl Into<String>) -> Self {
68        Self {
69            field: field.into(),
70            reason: reason.into(),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn warnings_survive_report_building() {
81        let report = FidelityReport::new("codec/plain")
82            .with_preserved_extra("raw-styles")
83            .with_warning("formula cached value differed");
84
85        assert!(report.is_lossless());
86        assert_eq!(report.warnings, vec!["formula cached value differed"]);
87        assert_eq!(report.preserved_extras, vec!["raw-styles"]);
88    }
89
90    #[test]
91    fn dropped_report_is_not_lossless() {
92        let report = FidelityReport::new("codec/plain")
93            .with_dropped("sheet.hiddenRows", "backend does not expose hidden rows");
94
95        assert!(!report.is_lossless());
96        assert_eq!(report.dropped[0].field, "sheet.hiddenRows");
97    }
98}