1use roxmltree::{Document, Node, ParsingOptions};
9
10use crate::{Error, XSI_NS};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Difference {
15 pub path: String,
17 pub message: String,
18}
19
20impl std::fmt::Display for Difference {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "{}: {}", self.path, self.message)
23 }
24}
25
26pub fn compare(expected: &str, actual: &str) -> Result<Vec<Difference>, Error> {
28 let expected = parse(expected, "expected")?;
29 let actual = parse(actual, "actual")?;
30 let mut differences = Vec::new();
31 let a = expected.root_element();
32 let b = actual.root_element();
33 let path = format!("/{}", a.tag_name().name());
34 compare_elements(a, b, &path, &mut differences);
35 Ok(differences)
36}
37
38pub fn compare_fragments(
41 expected: &str,
42 actual: &str,
43 namespaces: &[(&str, &str)],
44) -> Result<Vec<Difference>, Error> {
45 let (expected, _) = crate::read::wrap_fragment(expected, namespaces);
46 let (actual, _) = crate::read::wrap_fragment(actual, namespaces);
47 let expected = parse(&expected, "expected")?;
48 let actual = parse(&actual, "actual")?;
49 let mut differences = Vec::new();
50 match (first_element(&expected), first_element(&actual)) {
51 (Some(a), Some(b)) => {
52 let path = format!("/{}", a.tag_name().name());
53 compare_elements(a, b, &path, &mut differences);
54 }
55 _ => differences.push(Difference {
56 path: "/".to_owned(),
57 message: "a fragment has no element".to_owned(),
58 }),
59 }
60 Ok(differences)
61}
62
63fn first_element<'a, 'input>(doc: &'a Document<'input>) -> Option<Node<'a, 'input>> {
64 doc.root_element().children().find(|n| n.is_element())
65}
66
67fn parse<'input>(text: &'input str, which: &str) -> Result<Document<'input>, Error> {
68 let options = ParsingOptions {
69 allow_dtd: false,
70 ..ParsingOptions::default()
71 };
72 Document::parse_with_options(text, options).map_err(|e| Error {
73 message: format!("{which}: not well-formed XML: {e}"),
74 position: None,
75 })
76}
77
78type Name = (Option<String>, String);
79
80fn name_of(node: Node<'_, '_>) -> Name {
81 (
82 node.tag_name().namespace().map(str::to_owned),
83 node.tag_name().name().to_owned(),
84 )
85}
86
87fn show(name: &Name) -> String {
88 match &name.0 {
89 Some(ns) => format!("{{{ns}}}{}", name.1),
90 None => name.1.clone(),
91 }
92}
93
94fn attributes(node: Node<'_, '_>) -> Vec<(Name, String)> {
96 let mut out: Vec<(Name, String)> = node
97 .attributes()
98 .filter(|a| !(a.namespace() == Some(XSI_NS) && a.name() == "schemaLocation"))
99 .map(|a| {
100 let name = (a.namespace().map(str::to_owned), a.name().to_owned());
101 let value = if a.namespace() == Some(XSI_NS) && a.name() == "type" {
102 let (prefix, local) = match a.value().split_once(':') {
103 Some((p, l)) => (Some(p), l),
104 None => (None, a.value()),
105 };
106 match node.lookup_namespace_uri(prefix) {
107 Some(ns) => format!("{{{ns}}}{local}"),
108 None => local.to_owned(),
109 }
110 } else {
111 a.value().to_owned()
112 };
113 (name, value)
114 })
115 .collect();
116 out.sort();
117 out
118}
119
120fn content<'a, 'input>(node: Node<'a, 'input>) -> (Vec<Node<'a, 'input>>, String) {
124 let mut elements = Vec::new();
125 let mut texts: Vec<&str> = Vec::new();
126 let has_elements = node.children().any(|c| c.is_element());
127 let mut whole = String::new();
128 for child in node.children() {
129 if child.is_element() {
130 elements.push(child);
131 } else if child.is_text() {
132 let text = child.text().unwrap_or_default();
133 whole.push_str(text);
134 if !text.trim().is_empty() {
135 texts.push(text.trim());
136 }
137 }
138 }
139 let text = if has_elements {
140 texts.join(" ")
141 } else if whole.trim().is_empty() {
142 String::new()
143 } else {
144 whole
145 };
146 (elements, text)
147}
148
149fn compare_elements(a: Node<'_, '_>, b: Node<'_, '_>, path: &str, out: &mut Vec<Difference>) {
150 let (name_a, name_b) = (name_of(a), name_of(b));
151 if name_a != name_b {
152 out.push(Difference {
153 path: path.to_owned(),
154 message: format!("element `{}` became `{}`", show(&name_a), show(&name_b)),
155 });
156 return;
157 }
158 let (attrs_a, attrs_b) = (attributes(a), attributes(b));
159 for (name, value) in &attrs_a {
160 match attrs_b.iter().find(|(n, _)| n == name) {
161 None => out.push(Difference {
162 path: path.to_owned(),
163 message: format!("attribute `{}=\"{value}\"` is missing", show(name)),
164 }),
165 Some((_, other)) if !same_value(value, other) => out.push(Difference {
166 path: path.to_owned(),
167 message: format!("attribute `{}`: `{value}` became `{other}`", show(name)),
168 }),
169 Some(_) => {}
170 }
171 }
172 for (name, value) in &attrs_b {
173 if !attrs_a.iter().any(|(n, _)| n == name) {
174 out.push(Difference {
175 path: path.to_owned(),
176 message: format!("attribute `{}=\"{value}\"` was added", show(name)),
177 });
178 }
179 }
180 let ((elements_a, text_a), (elements_b, text_b)) = (content(a), content(b));
181 if text_a != text_b {
182 out.push(Difference {
183 path: path.to_owned(),
184 message: match (text_a.is_empty(), text_b.is_empty()) {
185 (false, true) => format!("text `{}` is missing", clip(&text_a)),
186 (true, false) => format!("text `{}` was added", clip(&text_b)),
187 _ => format!("text `{}` became `{}`", clip(&text_a), clip(&text_b)),
188 },
189 });
190 }
191 let groups = |elements: &[Node<'_, '_>]| {
194 let mut groups: Vec<(Name, Vec<usize>)> = Vec::new();
195 for (index, element) in elements.iter().enumerate() {
196 let name = name_of(*element);
197 match groups.iter_mut().find(|(n, _)| *n == name) {
198 Some((_, indexes)) => indexes.push(index),
199 None => groups.push((name, vec![index])),
200 }
201 }
202 groups
203 };
204 let (groups_a, groups_b) = (groups(&elements_a), groups(&elements_b));
205 for (name, indexes) in &groups_a {
206 let others = groups_b
207 .iter()
208 .find(|(n, _)| n == name)
209 .map(|(_, i)| i.as_slice())
210 .unwrap_or_default();
211 for (n, &index) in indexes.iter().enumerate() {
212 match others.get(n) {
213 Some(&other) => {
214 let child_path = format!("{path}/{}[{}]", name.1, n + 1);
215 compare_elements(elements_a[index], elements_b[other], &child_path, out);
216 }
217 None => out.push(Difference {
218 path: path.to_owned(),
219 message: format!("element `{}` [{}] is missing", show(name), n + 1),
220 }),
221 }
222 }
223 for n in indexes.len()..others.len() {
224 out.push(Difference {
225 path: path.to_owned(),
226 message: format!("element `{}` [{}] was added", show(name), n + 1),
227 });
228 }
229 }
230 for (name, indexes) in &groups_b {
231 if !groups_a.iter().any(|(n, _)| n == name) {
232 out.push(Difference {
233 path: path.to_owned(),
234 message: format!(
235 "element `{}` was added ({} times)",
236 show(name),
237 indexes.len()
238 ),
239 });
240 }
241 }
242}
243
244fn same_value(a: &str, b: &str) -> bool {
247 if a == b {
248 return true;
249 }
250 let number = |s: &str| {
251 let s = s.trim();
252 let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
253 (!digits.is_empty() && digits.bytes().all(|c| c.is_ascii_digit() || c == b'.'))
254 .then(|| s.parse::<f64>().ok())
255 .flatten()
256 };
257 matches!((number(a), number(b)), (Some(x), Some(y)) if x == y)
258}
259
260fn clip(text: &str) -> String {
261 let text = text.trim();
262 if text.chars().count() > 60 {
263 format!("{}…", text.chars().take(60).collect::<String>())
264 } else {
265 text.to_owned()
266 }
267}