//! Compatibility reporting only: parsing/encoding remain authoritative validators. use mb3_dat::{Version, World}; use std::collections::HashSet; #[derive(Clone, Copy, Debug)] pub enum Target { Txt, Obj, Dat(Version), } impl Target { pub fn name(self) -> &'static str { match self { Self::Txt => "txt", Self::Obj => "obj", Self::Dat(Version::V201501280) => "dat-201501280", Self::Dat(Version::V201512200) => "dat-201512200", } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Issue { pub code: &'static str, pub count: usize, pub lossy: bool, } #[derive(Clone, Debug)] pub struct Report { pub target: Target, pub issues: Vec, } impl Report { pub fn blocked(&self) -> bool { self.issues.iter().any(|i| !i.lossy) } pub fn compatible(&self) -> bool { self.issues.is_empty() } pub fn json(&self) -> String { let issues = self .issues .iter() .map(|i| { format!( "{{\"code\":\"{}\",\"count\":{},\"lossy\":{}}}", i.code, i.count, i.lossy ) }) .collect::>() .join(","); format!( "{{\"schema\":1,\"target\":\"{}\",\"compatible\":{},\"issues\":[{}]}}", self.target.name(), self.compatible(), issues ) } } pub fn analyze(world: &World, target: Target) -> Report { let mut issues = Vec::new(); let planes = || world.brushes.iter().flat_map(|b| &b.planes); match target { Target::Obj => { if !world.computed { issues.push(Issue { code: "computed_required", count: 1, lossy: false, }); } let n = planes().count().saturating_add(world.patches.len()); if n != 0 { issues.push(Issue { code: "editable_geometry", count: n, lossy: true, }); } let n = world .string_properties .len() .saturating_add(world.uv_properties.len()) .saturating_add( planes() .map(|p| p.strings.len().saturating_add(p.uv.len())) .fold(0usize, usize::saturating_add), ); if n != 0 { issues.push(Issue { code: "properties", count: n, lossy: true, }); } let n = world .patches .iter() .filter_map(|p| p.computed.as_ref()) .map(|p| p.normals.len()) .fold(0usize, usize::saturating_add); if n != 0 { issues.push(Issue { code: "stored_normals", count: n, lossy: true, }); } } Target::Dat(version) => { let n = world .string_properties .iter() .chain(&world.uv_properties) .filter(|s| !s.is_ascii()) .count() .saturating_add( planes() .flat_map(|p| &p.strings) .filter(|p| !p.value.is_ascii()) .count(), ); if n != 0 { issues.push(Issue { lossy: false, code: "non_ascii", count: n, }); } if version == Version::V201501280 { let n = world .string_properties .len() .saturating_add(world.uv_properties.len()) .saturating_add( planes() .map(|p| p.strings.len().saturating_add(p.uv.len())) .fold(0usize, usize::saturating_add), ); if n != 0 { issues.push(Issue { lossy: false, code: "legacy_properties", count: n, }); } } } Target::Txt => { let mut n = 0usize; for names in [&world.string_properties, &world.uv_properties] { let mut seen = HashSet::new(); for name in names { if !seen.insert(name) { n = n.saturating_add(1); } } } if n != 0 { issues.push(Issue { lossy: false, code: "duplicate_names", count: n, }); } } } Report { target, issues } } #[cfg(test)] mod tests { use super::*; #[test] fn grouped_reports_and_no_mutation() { let mut w = World { string_properties: vec!["é".into(), "é".into()], ..World::default() }; let original = w.clone(); assert_eq!(analyze(&w, Target::Txt).issues[0].count, 1); let r = analyze(&w, Target::Dat(Version::V201501280)); assert_eq!(r.issues.len(), 2); assert!(!r.compatible()); assert!(r.json().contains("\"schema\":1")); assert_eq!(w, original); w.string_properties.clear(); assert!(analyze(&w, Target::Txt).compatible()); } }