1use crate::error::FaucetError;
15use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
16use serde_json::{Map, Value};
17
18pub fn decode(bytes: &[u8], record_element: &str) -> Result<Vec<Value>, FaucetError> {
25 let doc = to_json(bytes)?;
26 let mut out = Vec::new();
27 collect(&doc, record_element, &mut out);
28 if !out.is_empty() {
29 return Ok(out);
30 }
31 match root_children(&doc) {
33 Some(children) => Ok(children),
34 None if is_empty_document(&doc) => Ok(Vec::new()),
38 None => Err(FaucetError::Source(format!(
39 "xml: no <{record_element}> elements, and the document root has no repeated child to \
40 use instead — set `xml.record_element` to the element that delimits one record"
41 ))),
42 }
43}
44
45fn collect(v: &Value, key: &str, out: &mut Vec<Value>) {
47 match v {
48 Value::Object(map) => {
49 for (k, child) in map {
50 if k == key {
51 match child {
52 Value::Array(items) => out.extend(items.iter().cloned()),
53 other => out.push(other.clone()),
54 }
55 } else {
56 collect(child, key, out);
57 }
58 }
59 }
60 Value::Array(items) => {
61 for i in items {
62 collect(i, key, out);
63 }
64 }
65 _ => {}
66 }
67}
68
69fn is_empty_document(doc: &Value) -> bool {
71 let Some(root) = doc.as_object().and_then(|m| m.values().next()) else {
72 return true;
73 };
74 match root {
75 Value::String(s) => s.trim().is_empty(),
76 Value::Object(map) => map.is_empty(),
77 _ => false,
78 }
79}
80
81fn root_children(doc: &Value) -> Option<Vec<Value>> {
85 let root = doc.as_object()?.values().next()?;
86 let map = root.as_object()?;
87 let real: Vec<(&String, &Value)> = map
88 .iter()
89 .filter(|(k, _)| !k.starts_with('@') && *k != "#text")
90 .collect();
91 match real.as_slice() {
92 [(_, Value::Array(items))] => Some(items.to_vec()),
93 [(_, single)] => Some(vec![(*single).clone()]),
94 _ => None,
95 }
96}
97
98fn err<E: std::fmt::Display>(e: E) -> FaucetError {
100 FaucetError::Sink(format!("xml: {e}"))
101}
102
103pub fn encode(
108 records: &[Value],
109 root_element: &str,
110 record_element: &str,
111) -> Result<Vec<u8>, FaucetError> {
112 let mut w = quick_xml::Writer::new(Vec::new());
113 w.write_event(Event::Start(BytesStart::new(root_element)))
114 .map_err(err)?;
115 for r in records {
116 w.write_event(Event::Start(BytesStart::new(record_element)))
117 .map_err(err)?;
118 write_value(&mut w, r)?;
119 w.write_event(Event::End(BytesEnd::new(record_element)))
120 .map_err(err)?;
121 }
122 w.write_event(Event::End(BytesEnd::new(root_element)))
123 .map_err(err)?;
124 Ok(w.into_inner())
125}
126
127fn write_value(w: &mut quick_xml::Writer<Vec<u8>>, v: &Value) -> Result<(), FaucetError> {
128 match v {
129 Value::Object(map) => {
130 for (k, child) in map {
131 let name = sanitize(k);
132 match child {
133 Value::Array(items) => {
134 for item in items {
135 w.write_event(Event::Start(BytesStart::new(&name)))
136 .map_err(err)?;
137 write_value(w, item)?;
138 w.write_event(Event::End(BytesEnd::new(&name)))
139 .map_err(err)?;
140 }
141 }
142 other => {
143 w.write_event(Event::Start(BytesStart::new(&name)))
144 .map_err(err)?;
145 write_value(w, other)?;
146 w.write_event(Event::End(BytesEnd::new(&name)))
147 .map_err(err)?;
148 }
149 }
150 }
151 Ok(())
152 }
153 Value::Null => Ok(()),
154 other => w
155 .write_event(Event::Text(BytesText::new(&super::cell_text(other))))
156 .map_err(err),
157 }
158}
159
160fn sanitize(key: &str) -> String {
166 let mut out = String::with_capacity(key.len());
167 for (i, c) in key.chars().enumerate() {
168 let ok = c.is_alphanumeric() || c == '_' || c == '-' || c == '.';
169 let ok = ok && !(i == 0 && (c.is_numeric() || c == '-' || c == '.'));
170 out.push(if ok { c } else { '_' });
171 }
172 if out.is_empty() { "field".into() } else { out }
173}
174
175type Frame = (Map<String, Value>, String);
177
178fn unbalanced(detail: &str) -> FaucetError {
179 FaucetError::Source(format!(
180 "xml: malformed XML: {detail}; the document has more closing than opening tags"
181 ))
182}
183
184fn top<'a>(stack: &'a mut [Frame], detail: &str) -> Result<&'a mut Frame, FaucetError> {
185 stack.last_mut().ok_or_else(|| unbalanced(detail))
186}
187
188fn pop(stack: &mut Vec<Frame>, detail: &str) -> Result<Frame, FaucetError> {
189 stack.pop().ok_or_else(|| unbalanced(detail))
190}
191
192fn attrs(e: &BytesStart) -> Map<String, Value> {
193 let mut m = Map::new();
194 for a in e.attributes().flatten() {
195 let k = local(a.key.as_ref());
196 if let Ok(v) = a.unescape_value() {
197 m.insert(format!("@{k}"), Value::String(v.to_string()));
198 }
199 }
200 m
201}
202
203fn local(name: &[u8]) -> String {
204 String::from_utf8_lossy(name)
205 .rsplit(':')
206 .next()
207 .unwrap_or_default()
208 .to_string()
209}
210
211fn insert_child(parent: &mut Map<String, Value>, key: String, val: Value) {
212 match parent.get_mut(&key) {
213 Some(Value::Array(arr)) => arr.push(val),
214 Some(existing) => {
215 let prev = existing.take();
216 parent.insert(key, Value::Array(vec![prev, val]));
217 }
218 None => {
219 parent.insert(key, val);
220 }
221 }
222}
223
224fn finish(obj: Map<String, Value>, text: String) -> Value {
225 let trimmed = text.trim();
226 if obj.is_empty() {
227 Value::String(trimmed.to_string())
228 } else {
229 let mut obj = obj;
230 if !trimmed.is_empty() {
231 obj.insert("#text".to_string(), Value::String(trimmed.to_string()));
232 }
233 Value::Object(obj)
234 }
235}
236
237pub fn to_json(bytes: &[u8]) -> Result<Value, FaucetError> {
239 let text = std::str::from_utf8(bytes)
240 .map_err(|e| FaucetError::Source(format!("xml: not UTF-8: {e}")))?;
241 let mut reader = quick_xml::Reader::from_str(text);
242 let mut stack: Vec<Frame> = vec![(Map::new(), String::new())];
243 let mut names: Vec<String> = Vec::new();
244
245 loop {
246 match reader
247 .read_event()
248 .map_err(|e| FaucetError::Source(format!("xml: {e}")))?
249 {
250 Event::Eof => break,
251 Event::Start(e) => {
252 names.push(local(e.name().as_ref()));
253 stack.push((attrs(&e), String::new()));
254 }
255 Event::Empty(e) => {
256 let name = local(e.name().as_ref());
257 let val = finish(attrs(&e), String::new());
258 let t = top(&mut stack, "element after the document root closed")?;
259 insert_child(&mut t.0, name, val);
260 }
261 Event::Text(t) => {
262 let s = t
263 .unescape()
264 .map_err(|e| FaucetError::Source(format!("xml: {e}")))?
265 .to_string();
266 top(&mut stack, "text after the document root closed")?
267 .1
268 .push_str(&s);
269 }
270 Event::CData(t) => {
271 let s = String::from_utf8_lossy(t.as_ref()).to_string();
272 top(&mut stack, "CDATA after the document root closed")?
273 .1
274 .push_str(&s);
275 }
276 Event::End(_) => {
277 let (obj, text) = pop(&mut stack, "unmatched closing tag")?;
278 let name = names
279 .pop()
280 .ok_or_else(|| unbalanced("unmatched closing tag"))?;
281 let val = finish(obj, text);
282 let parent = top(&mut stack, "unmatched closing tag")?;
283 insert_child(&mut parent.0, name, val);
284 }
285 _ => {}
286 }
287 }
288 let (root, _) = stack
289 .pop()
290 .ok_or_else(|| unbalanced("document root closed twice"))?;
291 Ok(Value::Object(root))
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use serde_json::json;
298
299 #[test]
300 fn a_named_record_element_selects_the_records() {
301 let xml = br#"<rows><row><id>1</id><n>a</n></row><row><id>2</id><n>b</n></row></rows>"#;
302 assert_eq!(
303 decode(xml, "row").expect("decode"),
304 vec![json!({"id": "1", "n": "a"}), json!({"id": "2", "n": "b"})]
305 );
306 }
307
308 #[test]
309 fn a_single_record_is_still_a_list_of_one() {
310 let xml = br#"<rows><row><id>1</id></row></rows>"#;
311 assert_eq!(
312 decode(xml, "row").expect("decode"),
313 vec![json!({"id": "1"})]
314 );
315 }
316
317 #[test]
318 fn the_record_element_is_found_at_any_depth() {
319 let xml = br#"<env><body><rows><row><id>1</id></row></rows></body></env>"#;
320 assert_eq!(
321 decode(xml, "row").expect("decode"),
322 vec![json!({"id": "1"})]
323 );
324 }
325
326 #[test]
327 fn without_a_match_the_roots_children_are_used() {
328 let xml = br#"<rows><row><id>1</id></row><row><id>2</id></row></rows>"#;
331 assert_eq!(decode(xml, "record").expect("decode").len(), 2);
332 }
333
334 #[test]
335 fn a_self_closing_element_and_cdata_decode() {
336 let v = to_json(br#"<r><e/><c><![CDATA[raw <>&]]></c><!-- ignored --></r>"#).expect("json");
338 assert_eq!(v["r"]["e"], json!(""));
339 assert_eq!(v["r"]["c"], json!("raw <>&"));
340 }
341
342 #[test]
343 fn three_repeated_elements_accumulate_into_one_array() {
344 let recs = decode(br#"<rs><r>a</r><r>b</r><r>c</r></rs>"#, "r").expect("decode");
347 assert_eq!(recs, vec![json!("a"), json!("b"), json!("c")]);
348 }
349
350 #[test]
351 fn a_root_wrapping_a_single_child_still_yields_one_record() {
352 let recs = decode(br#"<rows><row><id>1</id></row></rows>"#, "nope").expect("decode");
354 assert_eq!(recs, vec![json!({"id": "1"})]);
355 }
356
357 #[test]
358 fn emptiness_is_judged_on_the_root_shape() {
359 assert!(is_empty_document(&json!({})));
360 assert!(is_empty_document(&json!({"r": ""})));
361 assert!(is_empty_document(&json!({"r": " "})));
362 assert!(is_empty_document(&json!({"r": {}})));
363 assert!(!is_empty_document(&json!({"r": "text"})));
364 assert!(!is_empty_document(&json!({"r": {"a": 1}})));
365 assert!(!is_empty_document(&json!({"r": [1]})));
366 }
367
368 #[test]
369 fn the_writer_error_helper_is_prefixed() {
370 assert_eq!(err("boom").to_string(), "Sink error: xml: boom");
371 }
372
373 #[test]
374 fn an_empty_document_reads_back_as_zero_records_not_an_error() {
375 let bytes = encode(&[], "records", "record").expect("encode");
376 assert_eq!(
377 String::from_utf8(bytes.clone()).unwrap(),
378 "<records></records>"
379 );
380 assert!(decode(&bytes, "record").expect("decode").is_empty());
381 }
382
383 #[test]
384 fn an_ambiguous_root_is_an_error_naming_the_option() {
385 let xml = br#"<doc><a>1</a><b>2</b></doc>"#;
386 let err = decode(xml, "record").expect_err("ambiguous");
387 assert!(err.to_string().contains("xml.record_element"), "{err}");
388 }
389
390 #[test]
391 fn attributes_text_and_namespaces_map_compactly() {
392 let xml = br#"<r><x ns:k="v">t</x></r>"#;
393 let v = to_json(xml).expect("json");
394 assert_eq!(v["r"]["x"]["@k"], json!("v"));
395 assert_eq!(v["r"]["x"]["#text"], json!("t"));
396 }
397
398 #[test]
399 fn unbalanced_input_is_a_typed_error_not_a_panic() {
400 let err = to_json(b"<a></a></a>").expect_err("unbalanced");
405 assert!(matches!(err, FaucetError::Source(_)), "{err}");
406 assert!(unbalanced("x").to_string().contains("malformed XML"));
407 }
408
409 #[test]
410 fn encode_round_trips_through_decode() {
411 let recs = vec![json!({"id": "1", "n": "a"}), json!({"id": "2", "n": "b"})];
412 let bytes = encode(&recs, "records", "record").expect("encode");
413 assert_eq!(
414 String::from_utf8(bytes.clone()).unwrap(),
415 "<records><record><id>1</id><n>a</n></record><record><id>2</id><n>b</n></record></records>"
416 );
417 assert_eq!(decode(&bytes, "record").expect("decode"), recs);
418 }
419
420 #[test]
421 fn an_array_field_becomes_a_repeated_element() {
422 let bytes = encode(&[json!({"t": ["a", "b"]})], "rs", "r").expect("encode");
423 assert_eq!(
424 String::from_utf8(bytes).unwrap(),
425 "<rs><r><t>a</t><t>b</t></r></rs>"
426 );
427 }
428
429 #[test]
430 fn a_key_that_is_not_a_legal_element_name_is_substituted_not_rejected() {
431 let bytes = encode(&[json!({"a b/c": 1, "2x": 2})], "rs", "r").expect("encode");
432 let s = String::from_utf8(bytes).unwrap();
433 assert!(s.contains("<a_b_c>1</a_b_c>"), "{s}");
434 assert!(s.contains("<_x>2</_x>"), "{s}");
435 }
436
437 #[test]
438 fn a_null_field_writes_an_empty_element() {
439 let bytes = encode(&[json!({"a": null})], "rs", "r").expect("encode");
440 assert_eq!(String::from_utf8(bytes).unwrap(), "<rs><r><a></a></r></rs>");
441 }
442}