use std::cell::RefCell;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Direction {
Forward,
Reverse,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Severity {
Loss,
Approximated,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LossKind {
FlatteningAmbiguous,
FlatteningUnknownBlock,
ItemFlatteningDamage,
ComponentDropped,
EntityMergeAmbiguous,
RenameAmbiguous,
FingerprintCollapse,
UnsupportedInTarget,
Other,
}
impl LossKind {
pub fn as_str(self) -> &'static str {
match self {
LossKind::FlatteningAmbiguous => "flattening_ambiguous",
LossKind::FlatteningUnknownBlock => "flattening_unknown_block",
LossKind::ItemFlatteningDamage => "item_flattening_damage",
LossKind::ComponentDropped => "component_dropped",
LossKind::EntityMergeAmbiguous => "entity_merge_ambiguous",
LossKind::RenameAmbiguous => "rename_ambiguous",
LossKind::FingerprintCollapse => "fingerprint_collapse",
LossKind::UnsupportedInTarget => "unsupported_in_target",
LossKind::Other => "other",
}
}
}
#[derive(Clone, Debug)]
pub struct LossEntry {
pub version: i32,
pub kind: LossKind,
pub severity: Severity,
pub path: String,
pub detail: String,
}
#[derive(Clone, Debug, Default)]
pub struct LossReport {
pub entries: Vec<LossEntry>,
}
impl LossReport {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn loss_count(&self) -> usize {
self.entries
.iter()
.filter(|e| e.severity == Severity::Loss)
.count()
}
pub fn to_json(&self) -> String {
let entries: Vec<serde_json::Value> = self
.entries
.iter()
.map(|e| {
serde_json::json!({
"version": e.version,
"kind": e.kind.as_str(),
"severity": match e.severity {
Severity::Loss => "loss",
Severity::Approximated => "approximated",
},
"path": e.path,
"detail": e.detail,
})
})
.collect();
serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string())
}
pub fn summary(&self) -> String {
if self.entries.is_empty() {
return String::new();
}
let mut out = format!(
"{} conversion issue(s) ({} data loss):\n",
self.entries.len(),
self.loss_count()
);
for e in &self.entries {
let tag = match e.severity {
Severity::Loss => "LOSS",
Severity::Approximated => "APPROX",
};
out.push_str(&format!(
" [{tag}] v{} {} — {}\n",
e.version, e.path, e.detail
));
}
out
}
}
struct Ctx {
direction: Direction,
collecting: bool,
path: Vec<String>,
losses: Vec<LossEntry>,
}
impl Default for Ctx {
fn default() -> Self {
Self {
direction: Direction::Forward,
collecting: false,
path: Vec::new(),
losses: Vec::new(),
}
}
}
thread_local! {
static CTX: RefCell<Ctx> = RefCell::new(Ctx::default());
}
#[inline]
pub fn direction() -> Direction {
CTX.with(|c| c.borrow().direction)
}
#[inline]
pub fn is_reverse() -> bool {
direction() == Direction::Reverse
}
pub struct PathGuard {
_priv: (),
}
#[inline]
pub fn path_scope(segment: impl Into<String>) -> PathGuard {
CTX.with(|c| c.borrow_mut().path.push(segment.into()));
PathGuard { _priv: () }
}
impl Drop for PathGuard {
fn drop(&mut self) {
CTX.with(|c| {
c.borrow_mut().path.pop();
});
}
}
pub fn current_path() -> String {
CTX.with(|c| c.borrow().path.join(" > "))
}
pub fn report_loss(version: i32, kind: LossKind, severity: Severity, detail: impl Into<String>) {
CTX.with(|c| {
let mut c = c.borrow_mut();
if !c.collecting {
return;
}
let path = c.path.join(" > ");
c.losses.push(LossEntry {
version,
kind,
severity,
path,
detail: detail.into(),
});
});
}
struct SessionGuard {
prev_direction: Direction,
prev_collecting: bool,
outermost: bool,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
CTX.with(|c| {
let mut c = c.borrow_mut();
c.direction = self.prev_direction;
c.collecting = self.prev_collecting;
if self.outermost {
c.path.clear();
}
});
}
}
pub fn run_reverse<R>(f: impl FnOnce() -> R) -> (R, LossReport) {
let outermost = CTX.with(|c| {
let mut c = c.borrow_mut();
let was_collecting = c.collecting;
if !was_collecting {
c.path.clear();
c.losses.clear();
}
c.direction = Direction::Reverse;
!was_collecting
});
let _guard = {
SessionGuard {
prev_direction: if outermost {
Direction::Forward
} else {
Direction::Reverse
},
prev_collecting: !outermost,
outermost,
}
};
CTX.with(|c| c.borrow_mut().collecting = true);
let result = f();
let report = if outermost {
CTX.with(|c| LossReport {
entries: std::mem::take(&mut c.borrow_mut().losses),
})
} else {
LossReport::default()
};
(result, report)
}