use std::cell::RefCell;
use std::collections::BTreeSet;
use serde::de::{DeserializeOwned, Deserializer, EnumAccess, IgnoredAny, VariantAccess, Visitor};
use serde::Deserialize;
use crate::docx::error::Result;
pub fn from_xml<T: DeserializeOwned>(data: &[u8]) -> Result<T> {
Ok(quick_xml::de::from_reader(data)?)
}
#[derive(Clone, Debug, Default)]
pub(crate) struct UnknownChildren(Vec<Box<str>>);
impl UnknownChildren {
#[cfg(test)]
pub(crate) fn names(&self) -> Vec<&str> {
self.0.iter().map(|n| &**n).collect()
}
pub(crate) fn warn_once(&self, parent: &'static str) {
for name in self.take_unreported(parent) {
log::warn!("[parse] <{parent}> child <w:{name}> is not modelled and was ignored");
}
}
fn take_unreported(&self, parent: &'static str) -> Vec<Box<str>> {
if self.0.is_empty() {
return Vec::new();
}
SEEN_UNKNOWN_CHILDREN.with(|seen| {
let mut seen = seen.borrow_mut();
self.0
.iter()
.filter(|name| seen.insert((parent, (*name).clone())))
.cloned()
.collect()
})
}
}
thread_local! {
static SEEN_UNKNOWN_CHILDREN: RefCell<BTreeSet<(&'static str, Box<str>)>> =
const { RefCell::new(BTreeSet::new()) };
}
pub(crate) fn reset_unknown_child_log() {
SEEN_UNKNOWN_CHILDREN.with(|seen| seen.borrow_mut().clear());
}
impl<'de> Deserialize<'de> for UnknownChildren {
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
Vec::<UnknownElement>::deserialize(d).map(|v| Self(v.into_iter().map(|e| e.0).collect()))
}
}
struct UnknownElement(Box<str>);
impl<'de> Deserialize<'de> for UnknownElement {
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
struct ElementVisitor;
impl<'de> Visitor<'de> for ElementVisitor {
type Value = UnknownElement;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an XML element")
}
fn visit_enum<A: EnumAccess<'de>>(
self,
data: A,
) -> std::result::Result<Self::Value, A::Error> {
let (name, variant): (String, _) = data.variant()?;
variant.newtype_variant::<IgnoredAny>()?;
Ok(UnknownElement(name.into_boxed_str()))
}
}
d.deserialize_enum("UnknownElement", &[], ElementVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default, Deserialize)]
struct Bag {
#[serde(rename = "known", default)]
known: Vec<Known>,
#[serde(rename = "$value", default)]
unknown: UnknownChildren,
}
#[derive(Debug, Deserialize)]
struct Known {
#[serde(rename = "@val", default)]
#[allow(dead_code)]
val: Option<String>,
}
fn parse(xml: &str) -> Bag {
quick_xml::de::from_str(xml).unwrap()
}
#[test]
fn a_named_child_is_not_reported_as_unknown() {
let b = parse(r#"<bag><known val="7"/></bag>"#);
assert_eq!(b.known.len(), 1);
assert!(
b.unknown.names().is_empty(),
"a child the schema names must never be reported"
);
}
#[test]
fn an_unnamed_child_is_captured_by_name() {
let b = parse(r#"<bag><known val="7"/><hMerge val="restart"/></bag>"#);
assert_eq!(b.known.len(), 1, "the named child still parses");
assert_eq!(b.unknown.names(), ["hMerge"]);
}
#[test]
fn several_unnamed_children_are_captured_in_document_order() {
let b = parse(r#"<bag><hMerge/><bidiVisual/></bag>"#);
assert_eq!(b.unknown.names(), ["hMerge", "bidiVisual"]);
}
#[test]
fn an_unnamed_child_with_a_subtree_is_captured_by_name_alone() {
let b = parse(r#"<bag><foo w="1"><bar/>text</foo></bag>"#);
assert_eq!(b.unknown.names(), ["foo"]);
}
#[test]
fn an_empty_bag_reports_nothing() {
assert!(parse("<bag/>").unknown.names().is_empty());
}
#[test]
fn a_name_is_reported_once_per_document() {
reset_unknown_child_log();
let u = UnknownChildren(vec!["hMerge".into(), "hMerge".into(), "bidiVisual".into()]);
assert_eq!(
u.take_unreported("w:tcPr"),
vec!["hMerge".into(), "bidiVisual".into()] as Vec<Box<str>>,
"a repeat within one bag is still one report"
);
assert!(
u.take_unreported("w:tcPr").is_empty(),
"the next table with the same unmodelled child reports nothing"
);
}
#[test]
fn the_report_is_keyed_by_parent_as_well_as_child() {
reset_unknown_child_log();
let u = UnknownChildren(vec!["hMerge".into()]);
assert_eq!(u.take_unreported("w:tcPr").len(), 1);
assert_eq!(
u.take_unreported("w:tblPr").len(),
1,
"same name, different parent — still worth saying"
);
}
#[test]
fn the_next_document_reports_the_same_name_again() {
reset_unknown_child_log();
let u = UnknownChildren(vec!["hMerge".into()]);
assert_eq!(u.take_unreported("w:tcPr").len(), 1);
assert!(u.take_unreported("w:tcPr").is_empty());
reset_unknown_child_log();
assert_eq!(
u.take_unreported("w:tcPr").len(),
1,
"a new document starts from a clean log"
);
}
}