Skip to main content

dotloom_io/
report.rs

1//! Conversion (import/export) loss reports.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7/// What kind of information was lost or changed.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub enum LossKind {
11    /// Constraints are not representable in the target format.
12    Constraints,
13    /// Plugin entities exported as plain geometry (parameters lost).
14    PluginGeometry,
15    /// Associative dimensions exported as static geometry.
16    Dimensions,
17    /// Geometry approximated (curves flattened, ellipses as Béziers, ...).
18    Approximated,
19    /// Elements or entities that are not supported and were skipped.
20    Unsupported,
21    /// Styling simplified (CSS, gradients, line types, ...).
22    Style,
23    /// Text formatting simplified.
24    Text,
25    /// External references were ignored (never fetched).
26    ExternalReference,
27    /// Unit or scale assumption.
28    Units,
29    /// Read-only entities exported from their fallback representation.
30    Fallback,
31}
32
33/// One loss line with an occurrence count.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct Loss {
37    /// Category.
38    pub kind: LossKind,
39    /// Detail (element or entity type).
40    pub what: String,
41    /// Occurrences.
42    pub count: usize,
43}
44
45/// Report attached to every import and export.
46#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct ConversionReport {
49    /// Entities read or written.
50    pub entities: usize,
51    /// Losses grouped by kind and detail.
52    pub losses: Vec<Loss>,
53    /// Informational notes (versions, units, ...).
54    pub notes: Vec<String>,
55}
56
57/// Accumulates losses.
58#[derive(Debug, Default)]
59pub(crate) struct Recorder {
60    counts: BTreeMap<(LossKind, String), usize>,
61    pub notes: Vec<String>,
62    pub entities: usize,
63}
64
65impl Recorder {
66    pub fn add(&mut self, kind: LossKind, what: impl Into<String>) {
67        *self.counts.entry((kind, what.into())).or_insert(0) += 1;
68    }
69
70    pub fn finish(self) -> ConversionReport {
71        ConversionReport {
72            entities: self.entities,
73            losses: self.counts.into_iter().map(|((kind, what), count)| Loss { kind, what, count }).collect(),
74            notes: self.notes,
75        }
76    }
77}
78
79impl ConversionReport {
80    /// Whether nothing was lost.
81    #[must_use]
82    pub fn is_lossless(&self) -> bool {
83        self.losses.is_empty()
84    }
85
86    /// Total count for a kind.
87    #[must_use]
88    pub fn count(&self, kind: LossKind) -> usize {
89        self.losses.iter().filter(|l| l.kind == kind).map(|l| l.count).sum()
90    }
91}