1use faucet_core::FaucetError;
7use quick_xml::events::Event;
8use quick_xml::reader::Reader;
9use serde_json::{Map, Value, json};
10
11pub fn xml_to_json(xml: &str) -> Result<Value, FaucetError> {
16 let mut reader = Reader::from_str(xml);
17 let mut stack: Vec<(String, Map<String, Value>)> = vec![("$root".into(), Map::new())];
18
19 loop {
20 match reader.read_event() {
21 Ok(Event::Start(e)) => {
22 let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
23 let mut obj = Map::new();
24
25 for attr in e.attributes().flatten() {
27 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
28 let val = String::from_utf8_lossy(&attr.value).into_owned();
29 obj.insert(key, Value::String(val));
30 }
31
32 stack.push((name, obj));
33 }
34 Ok(Event::End(_)) => {
35 let (name, obj) = stack.pop().ok_or_else(|| {
36 FaucetError::Transform("malformed XML: unexpected end tag".into())
37 })?;
38
39 let value = if obj.len() == 1 && obj.contains_key("#text") {
40 obj.into_iter().next().unwrap().1
42 } else {
43 Value::Object(obj)
44 };
45
46 let parent = stack.last_mut().ok_or_else(|| {
47 FaucetError::Transform("malformed XML: no parent element".into())
48 })?;
49
50 match parent.1.get_mut(&name) {
52 Some(Value::Array(arr)) => arr.push(value),
53 Some(existing) => {
54 let prev = existing.clone();
55 *existing = Value::Array(vec![prev, value]);
56 }
57 None => {
58 parent.1.insert(name, value);
59 }
60 }
61 }
62 Ok(Event::Text(e)) => {
63 let text = e
64 .unescape()
65 .map_err(|err| FaucetError::Transform(format!("XML decode error: {err}")))?
66 .trim()
67 .to_string();
68
69 if !text.is_empty()
70 && let Some(current) = stack.last_mut()
71 {
72 match current.1.get_mut("#text") {
73 Some(Value::String(s)) => {
74 s.push(' ');
75 s.push_str(&text);
76 }
77 _ => {
78 current.1.insert("#text".into(), Value::String(text));
79 }
80 }
81 }
82 }
83 Ok(Event::CData(e)) => {
84 let text = e
90 .decode()
91 .map_err(|err| {
92 FaucetError::Transform(format!("XML CDATA decode error: {err}"))
93 })?
94 .trim()
95 .to_string();
96
97 if !text.is_empty()
98 && let Some(current) = stack.last_mut()
99 {
100 match current.1.get_mut("#text") {
101 Some(Value::String(s)) => {
102 s.push(' ');
103 s.push_str(&text);
104 }
105 _ => {
106 current.1.insert("#text".into(), Value::String(text));
107 }
108 }
109 }
110 }
111 Ok(Event::Empty(e)) => {
112 let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
113 let mut obj = Map::new();
114 for attr in e.attributes().flatten() {
115 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
116 let val = String::from_utf8_lossy(&attr.value).into_owned();
117 obj.insert(key, Value::String(val));
118 }
119 let value = if obj.is_empty() {
120 json!(null)
121 } else {
122 Value::Object(obj)
123 };
124
125 if let Some(parent) = stack.last_mut() {
126 match parent.1.get_mut(&name) {
127 Some(Value::Array(arr)) => arr.push(value),
128 Some(existing) => {
129 let prev = existing.clone();
130 *existing = Value::Array(vec![prev, value]);
131 }
132 None => {
133 parent.1.insert(name, value);
134 }
135 }
136 }
137 }
138 Ok(Event::Eof) => break,
139 Ok(_) => {} Err(e) => {
141 return Err(FaucetError::Transform(format!("XML parse error: {e}")));
142 }
143 }
144 }
145
146 let (_, root) = stack
147 .pop()
148 .ok_or_else(|| FaucetError::Transform("empty XML document".into()))?;
149
150 Ok(Value::Object(root))
151}
152
153pub fn stream_extract<F: FnMut(Value)>(
172 xml: &str,
173 records_element_path: Option<&str>,
174 mut on_record: F,
175) -> Result<(), FaucetError> {
176 let target_segments: Option<Vec<&str>> = records_element_path.map(|p| p.split('.').collect());
177
178 let mut reader = Reader::from_str(xml);
179
180 let mut path: Vec<String> = Vec::new();
182
183 let mut start_depth: Option<usize> = None;
188 let mut subtree: Vec<(String, Map<String, Value>)> = Vec::new();
189
190 let mut full_doc: Option<Vec<(String, Map<String, Value>)>> = if target_segments.is_none() {
194 Some(vec![("$root".into(), Map::new())])
195 } else {
196 None
197 };
198
199 fn path_matches(path: &[String], target: &[&str]) -> bool {
203 path.len() == target.len() && path.iter().zip(target).all(|(a, b)| a.as_str() == *b)
204 }
205
206 fn append_child(parent: &mut Map<String, Value>, name: String, value: Value) {
209 match parent.get_mut(&name) {
210 Some(Value::Array(arr)) => arr.push(value),
211 Some(existing) => {
212 let prev = existing.clone();
213 *existing = Value::Array(vec![prev, value]);
214 }
215 None => {
216 parent.insert(name, value);
217 }
218 }
219 }
220
221 loop {
222 match reader.read_event() {
223 Ok(Event::Start(e)) => {
224 let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
225 let mut obj = Map::new();
226 for attr in e.attributes().flatten() {
227 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
228 let val = String::from_utf8_lossy(&attr.value).into_owned();
229 obj.insert(key, Value::String(val));
230 }
231
232 path.push(name.clone());
233
234 if let Some(doc) = full_doc.as_mut() {
235 doc.push((name, obj));
236 } else if let Some(target) = target_segments.as_deref() {
237 if start_depth.is_some() {
238 subtree.push((name, obj));
239 } else if path_matches(&path, target) {
240 start_depth = Some(path.len() - 1);
243 subtree.push((name, obj));
244 }
245 }
248 }
249 Ok(Event::Empty(e)) => {
250 let name = String::from_utf8_lossy(e.name().as_ref()).into_owned();
251 let mut obj = Map::new();
252 for attr in e.attributes().flatten() {
253 let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref()));
254 let val = String::from_utf8_lossy(&attr.value).into_owned();
255 obj.insert(key, Value::String(val));
256 }
257 let value = if obj.is_empty() {
258 json!(null)
259 } else {
260 Value::Object(obj)
261 };
262
263 path.push(name.clone());
266 let matches_target = target_segments
267 .as_deref()
268 .map(|t| path_matches(&path, t))
269 .unwrap_or(false);
270 path.pop();
271
272 if let Some(doc) = full_doc.as_mut() {
273 if let Some(parent) = doc.last_mut() {
274 append_child(&mut parent.1, name, value);
275 }
276 } else if matches_target && start_depth.is_none() {
277 on_record(value);
279 } else if start_depth.is_some()
280 && let Some(parent) = subtree.last_mut()
281 {
282 append_child(&mut parent.1, name, value);
283 }
284 }
285 Ok(Event::End(_)) => {
286 let name = path.pop().ok_or_else(|| {
287 FaucetError::Transform("malformed XML: unexpected end tag".into())
288 })?;
289
290 if let Some(doc) = full_doc.as_mut() {
291 let (popped_name, obj) = doc.pop().ok_or_else(|| {
292 FaucetError::Transform("malformed XML: no element on stack".into())
293 })?;
294 debug_assert_eq!(popped_name, name);
295 let value = if obj.len() == 1 && obj.contains_key("#text") {
296 obj.into_iter().next().unwrap().1
297 } else {
298 Value::Object(obj)
299 };
300 let parent = doc.last_mut().ok_or_else(|| {
301 FaucetError::Transform("malformed XML: no parent element".into())
302 })?;
303 append_child(&mut parent.1, popped_name, value);
304 } else if let Some(depth) = start_depth {
305 let (popped_name, obj) = subtree.pop().ok_or_else(|| {
306 FaucetError::Transform("malformed XML: no element on subtree stack".into())
307 })?;
308 debug_assert_eq!(popped_name, name);
309 let value = if obj.len() == 1 && obj.contains_key("#text") {
310 obj.into_iter().next().unwrap().1
311 } else {
312 Value::Object(obj)
313 };
314
315 if subtree.is_empty() {
316 debug_assert_eq!(path.len(), depth);
319 start_depth = None;
320 on_record(value);
321 } else if let Some(parent) = subtree.last_mut() {
322 append_child(&mut parent.1, popped_name, value);
323 }
324 }
325 }
327 Ok(Event::Text(e)) => {
328 let text = e
329 .unescape()
330 .map_err(|err| FaucetError::Transform(format!("XML decode error: {err}")))?
331 .trim()
332 .to_string();
333 if text.is_empty() {
334 continue;
335 }
336
337 if let Some(doc) = full_doc.as_mut() {
338 if let Some(current) = doc.last_mut() {
339 match current.1.get_mut("#text") {
340 Some(Value::String(s)) => {
341 s.push(' ');
342 s.push_str(&text);
343 }
344 _ => {
345 current.1.insert("#text".into(), Value::String(text));
346 }
347 }
348 }
349 } else if start_depth.is_some()
350 && let Some(current) = subtree.last_mut()
351 {
352 match current.1.get_mut("#text") {
353 Some(Value::String(s)) => {
354 s.push(' ');
355 s.push_str(&text);
356 }
357 _ => {
358 current.1.insert("#text".into(), Value::String(text));
359 }
360 }
361 }
362 }
363 Ok(Event::CData(e)) => {
364 let text = e
368 .decode()
369 .map_err(|err| {
370 FaucetError::Transform(format!("XML CDATA decode error: {err}"))
371 })?
372 .trim()
373 .to_string();
374 if text.is_empty() {
375 continue;
376 }
377 if let Some(doc) = full_doc.as_mut() {
378 if let Some(current) = doc.last_mut() {
379 match current.1.get_mut("#text") {
380 Some(Value::String(s)) => {
381 s.push(' ');
382 s.push_str(&text);
383 }
384 _ => {
385 current.1.insert("#text".into(), Value::String(text));
386 }
387 }
388 }
389 } else if start_depth.is_some()
390 && let Some(current) = subtree.last_mut()
391 {
392 match current.1.get_mut("#text") {
393 Some(Value::String(s)) => {
394 s.push(' ');
395 s.push_str(&text);
396 }
397 _ => {
398 current.1.insert("#text".into(), Value::String(text));
399 }
400 }
401 }
402 }
403 Ok(Event::Eof) => break,
404 Ok(_) => {} Err(e) => {
406 return Err(FaucetError::Transform(format!("XML parse error: {e}")));
407 }
408 }
409 }
410
411 if let Some(mut doc) = full_doc {
412 let (_, root) = doc
413 .pop()
414 .ok_or_else(|| FaucetError::Transform("empty XML document".into()))?;
415 on_record(Value::Object(root));
416 }
417
418 Ok(())
419}
420
421pub fn extract_at_path(value: &Value, path: &str) -> Vec<Value> {
425 let segments: Vec<&str> = path.split('.').collect();
426 let mut current = value.clone();
427
428 for seg in &segments {
429 current = match current {
430 Value::Object(ref map) => match map.get(*seg) {
431 Some(v) => v.clone(),
432 None => return vec![],
433 },
434 _ => return vec![],
435 };
436 }
437
438 match current {
439 Value::Array(arr) => arr,
440 other => vec![other],
441 }
442}
443
444fn local_name(key: &str) -> &str {
447 match key.rsplit_once(':') {
448 Some((_, local)) => local,
449 None => key,
450 }
451}
452
453fn find_child_by_local<'a>(value: &'a Value, local: &str) -> Option<&'a Value> {
456 value
457 .as_object()?
458 .iter()
459 .find_map(|(k, v)| (local_name(k) == local).then_some(v))
460}
461
462fn fault_message(fault: &Value) -> String {
465 let fault = match fault {
467 Value::Array(items) => items.first().unwrap_or(fault),
468 other => other,
469 };
470 if let Some(fs) = find_child_by_local(fault, "faultstring") {
472 return value_to_text(fs);
473 }
474 if let Some(reason) = find_child_by_local(fault, "Reason") {
476 if let Some(text) = find_child_by_local(reason, "Text") {
477 return value_to_text(text);
478 }
479 return value_to_text(reason);
480 }
481 value_to_text(fault)
482}
483
484fn value_to_text(value: &Value) -> String {
487 match value {
488 Value::String(s) => s.clone(),
489 Value::Object(map) => map
490 .get("#text")
491 .and_then(Value::as_str)
492 .map(str::to_string)
493 .unwrap_or_else(|| value.to_string()),
494 other => other.to_string(),
495 }
496}
497
498pub fn detect_soap_fault(doc: &Value) -> Option<String> {
504 let envelope = find_child_by_local(doc, "Envelope")?;
505 let body = find_child_by_local(envelope, "Body")?;
506 let fault = find_child_by_local(body, "Fault")?;
507 Some(fault_message(fault))
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn simple_xml_to_json() {
516 let xml = r#"<root><name>Alice</name><age>30</age></root>"#;
517 let json = xml_to_json(xml).unwrap();
518 assert_eq!(json["root"]["name"], "Alice");
519 assert_eq!(json["root"]["age"], "30");
520 }
521
522 #[test]
523 fn repeated_elements_become_array() {
524 let xml = r#"<root><item>a</item><item>b</item><item>c</item></root>"#;
525 let json = xml_to_json(xml).unwrap();
526 let items = json["root"]["item"].as_array().unwrap();
527 assert_eq!(items.len(), 3);
528 assert_eq!(items[0], "a");
529 assert_eq!(items[1], "b");
530 }
531
532 #[test]
533 fn attributes_prefixed() {
534 let xml = r#"<user id="42"><name>Bob</name></user>"#;
535 let json = xml_to_json(xml).unwrap();
536 assert_eq!(json["user"]["@id"], "42");
537 assert_eq!(json["user"]["name"], "Bob");
538 }
539
540 #[test]
541 fn nested_elements() {
542 let xml = r#"<root><user><address><city>NYC</city></address></user></root>"#;
543 let json = xml_to_json(xml).unwrap();
544 assert_eq!(json["root"]["user"]["address"]["city"], "NYC");
545 }
546
547 #[test]
548 fn cdata_content_is_captured_not_dropped() {
549 let xml = r#"<root><body><![CDATA[<b>hi</b> & bye]]></body></root>"#;
552 let json = xml_to_json(xml).unwrap();
553 assert_eq!(json["root"]["body"], "<b>hi</b> & bye");
554 }
555
556 #[test]
557 fn cdata_content_captured_in_streaming_path() {
558 let xml = r#"<feed><item><html><![CDATA[<p>x</p>]]></html></item></feed>"#;
560 let recs = collect_stream_extract(xml, Some("feed.item"));
561 assert_eq!(recs.len(), 1);
562 assert_eq!(recs[0]["html"], "<p>x</p>");
563 }
564
565 #[test]
566 fn empty_elements() {
567 let xml = r#"<root><flag/></root>"#;
568 let json = xml_to_json(xml).unwrap();
569 assert!(json["root"]["flag"].is_null());
570 }
571
572 #[test]
573 fn empty_element_with_attr() {
574 let xml = r#"<root><flag enabled="true"/></root>"#;
575 let json = xml_to_json(xml).unwrap();
576 assert_eq!(json["root"]["flag"]["@enabled"], "true");
577 }
578
579 #[test]
580 fn extract_at_path_nested() {
581 let val = json!({"root": {"users": {"user": [{"id": 1}, {"id": 2}]}}});
582 let records = extract_at_path(&val, "root.users.user");
583 assert_eq!(records.len(), 2);
584 assert_eq!(records[0]["id"], 1);
585 }
586
587 #[test]
588 fn extract_at_path_single_element() {
589 let val = json!({"root": {"user": {"id": 1}}});
590 let records = extract_at_path(&val, "root.user");
591 assert_eq!(records.len(), 1);
592 assert_eq!(records[0]["id"], 1);
593 }
594
595 #[test]
596 fn extract_at_path_missing() {
597 let val = json!({"root": {}});
598 let records = extract_at_path(&val, "root.users.user");
599 assert!(records.is_empty());
600 }
601
602 #[test]
603 fn soap_envelope() {
604 let xml = r#"
605 <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
606 <soap:Body>
607 <GetUsersResponse>
608 <User><Name>Alice</Name></User>
609 <User><Name>Bob</Name></User>
610 </GetUsersResponse>
611 </soap:Body>
612 </soap:Envelope>"#;
613 let json = xml_to_json(xml).unwrap();
614 let users = extract_at_path(&json, "soap:Envelope.soap:Body.GetUsersResponse.User");
615 assert_eq!(users.len(), 2);
616 }
617
618 fn collect_stream_extract(xml: &str, path: Option<&str>) -> Vec<Value> {
619 let mut out = Vec::new();
620 stream_extract(xml, path, |v| out.push(v)).unwrap();
621 out
622 }
623
624 #[test]
625 fn stream_extract_matches_eager_path_extraction() {
626 let xml = r#"<root>
627 <user id="1"><name>Alice</name><age>30</age></user>
628 <user id="2"><name>Bob</name><age>25</age></user>
629 <user id="3"><name>Carol</name><age>40</age></user>
630 </root>"#;
631 let streamed = collect_stream_extract(xml, Some("root.user"));
632 let eager = extract_at_path(&xml_to_json(xml).unwrap(), "root.user");
633 assert_eq!(streamed, eager);
634 assert_eq!(streamed.len(), 3);
635 assert_eq!(streamed[0]["@id"], "1");
636 assert_eq!(streamed[0]["name"], "Alice");
637 assert_eq!(streamed[2]["name"], "Carol");
638 }
639
640 #[test]
641 fn stream_extract_handles_nested_children_and_attrs() {
642 let xml = r#"<root>
643 <order id="A"><line><sku>X</sku><qty>2</qty></line><line><sku>Y</sku><qty>5</qty></line></order>
644 <order id="B"><line><sku>Z</sku><qty>1</qty></line></order>
645 </root>"#;
646 let streamed = collect_stream_extract(xml, Some("root.order"));
647 let eager = extract_at_path(&xml_to_json(xml).unwrap(), "root.order");
648 assert_eq!(streamed, eager);
649 assert_eq!(streamed.len(), 2);
650 let lines = streamed[0]["line"].as_array().expect("repeated children");
651 assert_eq!(lines.len(), 2);
652 assert_eq!(lines[1]["sku"], "Y");
653 }
654
655 #[test]
656 fn stream_extract_no_path_returns_full_doc_once() {
657 let xml = r#"<root><a>1</a><b>2</b></root>"#;
658 let streamed = collect_stream_extract(xml, None);
659 let eager = xml_to_json(xml).unwrap();
660 assert_eq!(streamed.len(), 1);
661 assert_eq!(streamed[0], eager);
662 }
663
664 #[test]
665 fn stream_extract_no_matches_emits_nothing() {
666 let xml = r#"<root><a>1</a></root>"#;
667 let streamed = collect_stream_extract(xml, Some("root.missing"));
668 assert!(streamed.is_empty());
669 }
670
671 #[test]
672 fn stream_extract_self_closing_matched_element() {
673 let xml = r#"<root><item id="1"/><item id="2"/><item id="3"/></root>"#;
674 let streamed = collect_stream_extract(xml, Some("root.item"));
675 assert_eq!(streamed.len(), 3);
676 assert_eq!(streamed[0]["@id"], "1");
677 assert_eq!(streamed[2]["@id"], "3");
678 }
679
680 #[test]
681 fn stream_extract_preserves_soap_namespaces() {
682 let xml = r#"
683 <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
684 <soap:Body>
685 <GetUsersResponse>
686 <User><Name>Alice</Name></User>
687 <User><Name>Bob</Name></User>
688 </GetUsersResponse>
689 </soap:Body>
690 </soap:Envelope>"#;
691 let streamed =
692 collect_stream_extract(xml, Some("soap:Envelope.soap:Body.GetUsersResponse.User"));
693 let eager = extract_at_path(
694 &xml_to_json(xml).unwrap(),
695 "soap:Envelope.soap:Body.GetUsersResponse.User",
696 );
697 assert_eq!(streamed, eager);
698 assert_eq!(streamed.len(), 2);
699 assert_eq!(streamed[1]["Name"], "Bob");
700 }
701
702 #[test]
703 fn xml_to_json_mixed_text_and_children_keeps_both() {
704 let xml = r#"<root><p>hello<b>bold</b></p></root>"#;
707 let json = xml_to_json(xml).unwrap();
708 assert_eq!(json["root"]["p"]["#text"], "hello");
709 assert_eq!(json["root"]["p"]["b"], "bold");
710 }
711
712 #[test]
713 fn xml_to_json_text_split_by_entity_is_concatenated() {
714 let xml = r#"<root><msg>foo & bar</msg></root>"#;
718 let json = xml_to_json(xml).unwrap();
719 assert_eq!(json["root"]["msg"], "foo & bar");
720 }
721
722 #[test]
723 fn xml_to_json_text_then_cdata_concatenated() {
724 let xml = r#"<root><note>before <![CDATA[<raw>]]></note></root>"#;
727 let json = xml_to_json(xml).unwrap();
728 assert_eq!(json["root"]["note"], "before <raw>");
729 }
730
731 #[test]
732 fn xml_to_json_repeated_empty_elements_become_array() {
733 let xml = r#"<root><flag/><flag/></root>"#;
736 let json = xml_to_json(xml).unwrap();
737 let flags = json["root"]["flag"].as_array().expect("repeated empties");
738 assert_eq!(flags.len(), 2);
739 assert!(flags[0].is_null());
740 assert!(flags[1].is_null());
741 }
742
743 #[test]
744 fn xml_to_json_three_repeated_empty_elements_push_onto_array() {
745 let xml = r#"<root><flag a="1"/><flag a="2"/><flag a="3"/></root>"#;
748 let json = xml_to_json(xml).unwrap();
749 let flags = json["root"]["flag"].as_array().expect("repeated empties");
750 assert_eq!(flags.len(), 3);
751 assert_eq!(flags[2]["@a"], "3");
752 }
753
754 #[test]
755 fn xml_to_json_skips_comments_and_processing_instructions() {
756 let xml = r#"<?xml version="1.0"?><root><!-- a comment --><name>X</name></root>"#;
759 let json = xml_to_json(xml).unwrap();
760 assert_eq!(json["root"]["name"], "X");
761 }
762
763 #[test]
764 fn xml_to_json_malformed_returns_parse_error() {
765 let xml = r#"<root><a></b></root>"#;
768 let err = xml_to_json(xml).unwrap_err();
769 assert!(
770 matches!(&err, FaucetError::Transform(m) if m.contains("XML parse error")),
771 "got {err:?}"
772 );
773 }
774
775 #[test]
776 fn extract_at_path_descending_into_scalar_returns_empty() {
777 let val = json!({"root": {"name": "Alice"}});
780 let records = extract_at_path(&val, "root.name.first");
781 assert!(records.is_empty());
782 }
783
784 #[test]
785 fn extract_at_path_scalar_root_returns_empty() {
786 let val = json!("just a string");
788 let records = extract_at_path(&val, "anything");
789 assert!(records.is_empty());
790 }
791
792 #[test]
793 fn stream_extract_full_doc_mode_self_closing_and_repeats() {
794 let xml = r#"<root><flag/><flag/><name>Z</name></root>"#;
797 let streamed = collect_stream_extract(xml, None);
798 let eager = xml_to_json(xml).unwrap();
799 assert_eq!(streamed.len(), 1);
800 assert_eq!(streamed[0], eager);
801 let flags = streamed[0]["root"]["flag"]
802 .as_array()
803 .expect("repeated empties in full-doc mode");
804 assert_eq!(flags.len(), 2);
805 assert_eq!(streamed[0]["root"]["name"], "Z");
806 }
807
808 #[test]
809 fn stream_extract_full_doc_mode_mixed_text_and_cdata() {
810 let xml = r#"<root><note>hi & <![CDATA[<x>]]></note></root>"#;
813 let streamed = collect_stream_extract(xml, None);
814 let eager = xml_to_json(xml).unwrap();
815 assert_eq!(streamed.len(), 1);
816 assert_eq!(streamed[0], eager);
817 assert_eq!(streamed[0]["root"]["note"], "hi & <x>");
818 }
819
820 #[test]
821 fn stream_extract_subtree_self_closing_child_appends() {
822 let xml = r#"<root>
825 <user id="1"><active/><name>Alice</name></user>
826 <user id="2"><active/><name>Bob</name></user>
827 </root>"#;
828 let streamed = collect_stream_extract(xml, Some("root.user"));
829 let eager = extract_at_path(&xml_to_json(xml).unwrap(), "root.user");
830 assert_eq!(streamed, eager);
831 assert_eq!(streamed.len(), 2);
832 assert!(streamed[0]["active"].is_null());
833 assert_eq!(streamed[0]["name"], "Alice");
834 }
835
836 #[test]
837 fn stream_extract_subtree_text_split_by_entity_concatenated() {
838 let xml = r#"<root><item><msg>a & b</msg></item></root>"#;
841 let streamed = collect_stream_extract(xml, Some("root.item"));
842 assert_eq!(streamed.len(), 1);
843 assert_eq!(streamed[0]["msg"], "a & b");
844 }
845
846 #[test]
847 fn stream_extract_subtree_text_then_cdata_concatenated() {
848 let xml = r#"<root><item><note>start <![CDATA[<end>]]></note></item></root>"#;
851 let streamed = collect_stream_extract(xml, Some("root.item"));
852 assert_eq!(streamed.len(), 1);
853 assert_eq!(streamed[0]["note"], "start <end>");
854 }
855
856 #[test]
857 fn stream_extract_repeated_children_push_onto_existing_array() {
858 let xml = r#"<root><order><line>a</line><line>b</line><line>c</line></order></root>"#;
861 let streamed = collect_stream_extract(xml, Some("root.order"));
862 assert_eq!(streamed.len(), 1);
863 let lines = streamed[0]["line"].as_array().expect("repeated children");
864 assert_eq!(lines.len(), 3);
865 assert_eq!(lines[2], "c");
866 }
867
868 #[test]
869 fn stream_extract_malformed_returns_parse_error() {
870 let xml = r#"<root><a></b></root>"#;
872 let mut out = Vec::new();
873 let err = stream_extract(xml, Some("root.a"), |v| out.push(v)).unwrap_err();
874 assert!(
875 matches!(&err, FaucetError::Transform(m) if m.contains("XML parse error")),
876 "got {err:?}"
877 );
878 }
879
880 #[test]
881 fn local_name_strips_namespace_prefix() {
882 assert_eq!(local_name("soap:Body"), "Body");
883 assert_eq!(local_name("Body"), "Body");
884 assert_eq!(local_name("a:b:c"), "c");
885 }
886
887 #[test]
888 fn detect_soap_fault_soap11_prefixed() {
889 let xml = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
890 <soap:Body>
891 <soap:Fault>
892 <faultcode>soap:Server</faultcode>
893 <faultstring>Something went wrong</faultstring>
894 </soap:Fault>
895 </soap:Body>
896 </soap:Envelope>"#;
897 let doc = xml_to_json(xml).unwrap();
898 assert_eq!(
899 detect_soap_fault(&doc).as_deref(),
900 Some("Something went wrong")
901 );
902 }
903
904 #[test]
905 fn detect_soap_fault_soap11_default_namespace() {
906 let xml = r#"<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
908 <Body>
909 <Fault>
910 <faultcode>Server</faultcode>
911 <faultstring>boom</faultstring>
912 </Fault>
913 </Body>
914 </Envelope>"#;
915 let doc = xml_to_json(xml).unwrap();
916 assert_eq!(detect_soap_fault(&doc).as_deref(), Some("boom"));
917 }
918
919 #[test]
920 fn detect_soap_fault_soap12_reason_text() {
921 let xml = r#"<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
922 <env:Body>
923 <env:Fault>
924 <env:Code><env:Value>env:Receiver</env:Value></env:Code>
925 <env:Reason><env:Text xml:lang="en">server exploded</env:Text></env:Reason>
926 </env:Fault>
927 </env:Body>
928 </env:Envelope>"#;
929 let doc = xml_to_json(xml).unwrap();
930 assert_eq!(detect_soap_fault(&doc).as_deref(), Some("server exploded"));
931 }
932
933 #[test]
934 fn detect_soap_fault_returns_none_on_success_response() {
935 let xml = r#"<Envelope><Body>
936 <GetUsersResponse><User><Name>Alice</Name></User></GetUsersResponse>
937 </Body></Envelope>"#;
938 let doc = xml_to_json(xml).unwrap();
939 assert!(detect_soap_fault(&doc).is_none());
940 }
941
942 #[test]
943 fn detect_soap_fault_returns_none_when_no_envelope() {
944 let doc = xml_to_json("<root><item>a</item></root>").unwrap();
945 assert!(detect_soap_fault(&doc).is_none());
946 }
947
948 #[test]
949 fn stream_extract_skips_comments_outside_match() {
950 let xml = r#"<root><!-- c --><item><v>1</v></item><!-- d --><item><v>2</v></item></root>"#;
952 let streamed = collect_stream_extract(xml, Some("root.item"));
953 assert_eq!(streamed.len(), 2);
954 assert_eq!(streamed[0]["v"], "1");
955 assert_eq!(streamed[1]["v"], "2");
956 }
957}