1use std::fmt::Write as _;
29use std::path::Path;
30
31use quick_xml::events::Event;
32use quick_xml::Reader;
33use serde_json::{Map, Value};
34
35pub const ARRAY_ITEM: &str = "item";
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Format {
40 Json,
41 Xml,
42}
43
44impl Format {
45 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Format> {
47 match path
48 .as_ref()
49 .extension()
50 .and_then(|e| e.to_str())
51 .map(|e| e.to_lowercase())
52 .as_deref()
53 {
54 Some("xml") => Some(Format::Xml),
55 Some("json") => Some(Format::Json),
56 _ => None,
57 }
58 }
59
60 pub fn detect(content: &str) -> Format {
62 match content.trim_start().chars().next() {
63 Some('<') => Format::Xml,
64 _ => Format::Json,
65 }
66 }
67
68 pub fn extension(&self) -> &'static str {
69 match self {
70 Format::Json => "json",
71 Format::Xml => "xml",
72 }
73 }
74}
75
76pub fn parse_value(content: &str, format: Format) -> Result<Value, String> {
80 match format {
81 Format::Json => serde_json::from_str(content).map_err(|e| format!("invalid JSON: {e}")),
82 Format::Xml => xml_to_value(content),
83 }
84}
85
86pub fn parse<T: serde::de::DeserializeOwned>(content: &str, format: Format) -> Result<T, String> {
88 let value = parse_value(content, format)?;
89 serde_json::from_value(value).map_err(|e| format!("document does not match the schema: {e}"))
90}
91
92pub fn xml_to_value(xml: &str) -> Result<Value, String> {
94 let mut reader = Reader::from_str(xml);
95 reader.config_mut().expand_empty_elements = false;
98
99 let mut stack: Vec<Node> = Vec::new();
101 let mut root: Option<(String, Value)> = None;
102
103 loop {
104 match reader.read_event() {
105 Ok(Event::Start(e)) => stack.push(Node::start(&e)?),
106 Ok(Event::Empty(e)) => {
107 let node = Node::start(&e)?;
108 let (name, value) = node.finish();
109 attach(&mut stack, &mut root, name, value)?;
110 }
111 Ok(Event::Text(e)) => {
112 if let Some(node) = stack.last_mut() {
113 let text = e
114 .decode()
115 .map_err(|err| format!("invalid text content: {err}"))?;
116 node.text.push_str(text.as_ref());
117 }
118 }
119 Ok(Event::CData(e)) => {
120 if let Some(node) = stack.last_mut() {
121 let text = String::from_utf8(e.into_inner().into_owned())
122 .map_err(|err| format!("invalid CDATA: {err}"))?;
123 node.text.push_str(&text);
124 }
125 }
126 Ok(Event::GeneralRef(e)) => {
128 if let Some(node) = stack.last_mut() {
129 node.text.push_str(&resolve_entity(&e)?);
130 }
131 }
132 Ok(Event::End(_)) => {
133 let node = stack.pop().ok_or_else(|| "unbalanced closing tag".to_string())?;
134 let (name, value) = node.finish();
135 attach(&mut stack, &mut root, name, value)?;
136 }
137 Ok(Event::Eof) => break,
138 Ok(_) => {} Err(e) => return Err(format!("malformed XML at byte {}: {e}", reader.buffer_position())),
140 }
141 }
142 if !stack.is_empty() {
143 return Err("unbalanced XML: unclosed elements".to_string());
144 }
145 match root {
146 Some((_, value)) => Ok(value),
148 None => Err("empty XML document".to_string()),
149 }
150}
151
152struct Node {
153 name: String,
154 attrs: Vec<(String, Value)>,
156 children: Vec<(String, Value)>,
158 text: String,
159}
160
161impl Node {
162 fn start(e: &quick_xml::events::BytesStart) -> Result<Node, String> {
163 let name = String::from_utf8(e.name().as_ref().to_vec())
164 .map_err(|err| format!("invalid element name: {err}"))?;
165 let mut attrs = Vec::new();
166 for attr in e.attributes() {
167 let attr = attr.map_err(|err| format!("invalid attribute in <{name}>: {err}"))?;
168 let key = String::from_utf8(attr.key.as_ref().to_vec())
169 .map_err(|err| format!("invalid attribute name: {err}"))?;
170 let raw = attr
171 .unescape_value()
172 .map_err(|err| format!("invalid attribute value in <{name}>: {err}"))?;
173 attrs.push((key, infer_scalar(raw.as_ref())));
174 }
175 Ok(Node { name, attrs, children: Vec::new(), text: String::new() })
176 }
177
178 fn finish(self) -> (String, Value) {
179 let Node { name, attrs, children, text } = self;
180
181 if !children.is_empty() && children.iter().all(|(n, _)| n == ARRAY_ITEM) {
183 let items = children.into_iter().map(|(_, v)| v).collect();
184 return (name, Value::Array(items));
185 }
186
187 if children.is_empty() && attrs.is_empty() {
188 let trimmed = text.trim();
189 return (name, infer_scalar(trimmed));
190 }
191
192 let mut map = Map::new();
193 for (key, value) in attrs {
194 map.insert(key, value);
195 }
196 for (key, value) in children {
198 match map.get_mut(&key) {
199 Some(Value::Array(existing)) => existing.push(value),
200 Some(slot) => {
201 let previous = slot.take();
202 *slot = Value::Array(vec![previous, value]);
203 }
204 None => {
205 map.insert(key, value);
206 }
207 }
208 }
209 (name, Value::Object(map))
210 }
211}
212
213fn resolve_entity(e: &quick_xml::events::BytesRef) -> Result<String, String> {
216 if e.is_char_ref() {
217 return match e.resolve_char_ref() {
218 Ok(Some(c)) => Ok(c.to_string()),
219 Ok(None) => Err("unresolvable character reference".to_string()),
220 Err(err) => Err(format!("invalid character reference: {err}")),
221 };
222 }
223 let name = e.decode().map_err(|err| format!("invalid entity reference: {err}"))?;
224 match name.as_ref() {
225 "amp" => Ok("&".to_string()),
226 "lt" => Ok("<".to_string()),
227 "gt" => Ok(">".to_string()),
228 "quot" => Ok("\"".to_string()),
229 "apos" => Ok("'".to_string()),
230 other => Err(format!(
231 "unknown entity '&{other};' (only the predefined XML entities are supported)"
232 )),
233 }
234}
235
236fn attach(
237 stack: &mut [Node],
238 root: &mut Option<(String, Value)>,
239 name: String,
240 value: Value,
241) -> Result<(), String> {
242 match stack.last_mut() {
243 Some(parent) => {
244 parent.children.push((name, value));
245 Ok(())
246 }
247 None => {
248 if root.is_some() {
249 return Err("XML documents must have a single root element".to_string());
250 }
251 *root = Some((name, value));
252 Ok(())
253 }
254 }
255}
256
257fn infer_scalar(text: &str) -> Value {
259 let t = text.trim();
260 if t.is_empty() {
261 return Value::Null;
262 }
263 match t {
264 "true" => return Value::Bool(true),
265 "false" => return Value::Bool(false),
266 "null" => return Value::Null,
267 _ => {}
268 }
269 if let Ok(i) = t.parse::<i64>() {
272 return Value::Number(i.into());
273 }
274 if let Ok(f) = t.parse::<f64>() {
275 if f.is_finite() {
276 if let Some(n) = serde_json::Number::from_f64(f) {
277 return Value::Number(n);
278 }
279 }
280 }
281 Value::String(t.to_string())
282}
283
284pub fn render_results(results: &[Value], format: Format) -> String {
291 let mut array = Value::Array(results.to_vec());
292 strip_nulls(&mut array);
293 match format {
294 Format::Json => serde_json::to_string_pretty(&array).unwrap_or_else(|_| "[]".to_string()),
295 Format::Xml => value_to_xml(&array, "results"),
296 }
297}
298
299pub fn render_value(value: &Value, format: Format, root: &str) -> String {
301 let mut value = value.clone();
302 strip_nulls(&mut value);
303 match format {
304 Format::Json => serde_json::to_string_pretty(&value).unwrap_or_default(),
305 Format::Xml => value_to_xml(&value, root),
306 }
307}
308
309pub fn strip_nulls(value: &mut Value) {
315 match value {
316 Value::Object(map) => {
317 map.retain(|_, v| !v.is_null());
318 for v in map.values_mut() {
319 strip_nulls(v);
320 }
321 }
322 Value::Array(items) => {
323 for item in items {
324 strip_nulls(item);
325 }
326 }
327 _ => {}
328 }
329}
330
331pub fn value_to_xml(value: &Value, root: &str) -> String {
335 let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
336 write_element(&mut out, root, value, 0);
337 out
338}
339
340fn write_element(out: &mut String, name: &str, value: &Value, depth: usize) {
341 let pad = " ".repeat(depth);
342 match value {
343 Value::Null => {
344 let _ = writeln!(out, "{pad}<{name}/>");
345 }
346 Value::Bool(_) | Value::Number(_) | Value::String(_) => {
347 let text = match value {
348 Value::String(s) => escape_text(s),
349 other => other.to_string(),
350 };
351 let _ = writeln!(out, "{pad}<{name}>{text}</{name}>");
352 }
353 Value::Array(items) => {
354 if items.is_empty() {
355 let _ = writeln!(out, "{pad}<{name}/>");
356 return;
357 }
358 let _ = writeln!(out, "{pad}<{name}>");
359 for item in items {
360 write_element(out, ARRAY_ITEM, item, depth + 1);
361 }
362 let _ = writeln!(out, "{pad}</{name}>");
363 }
364 Value::Object(map) => {
365 if map.is_empty() {
366 let _ = writeln!(out, "{pad}<{name}/>");
367 return;
368 }
369 let _ = writeln!(out, "{pad}<{name}>");
370 for (key, child) in map {
371 write_element(out, key, child, depth + 1);
372 }
373 let _ = writeln!(out, "{pad}</{name}>");
374 }
375 }
376}
377
378fn escape_text(text: &str) -> String {
379 let mut out = String::with_capacity(text.len());
380 for c in text.chars() {
381 match c {
382 '&' => out.push_str("&"),
383 '<' => out.push_str("<"),
384 '>' => out.push_str(">"),
385 '"' => out.push_str("""),
386 '\'' => out.push_str("'"),
387 _ => out.push(c),
388 }
389 }
390 out
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use serde_json::json;
397
398 #[test]
399 fn detects_format_from_content_and_path() {
400 assert_eq!(Format::detect(" { \"a\": 1 }"), Format::Json);
401 assert_eq!(Format::detect("\n<?xml version=\"1.0\"?><a/>"), Format::Xml);
402 assert_eq!(Format::detect("<contracts/>"), Format::Xml);
403 assert_eq!(Format::from_path("in.xml"), Some(Format::Xml));
404 assert_eq!(Format::from_path("in.JSON"), Some(Format::Json));
405 assert_eq!(Format::from_path("in.txt"), None);
406 }
407
408 #[test]
409 fn scalars_are_inferred_without_eating_dates_or_codes() {
410 assert_eq!(infer_scalar("100"), json!(100));
411 assert_eq!(infer_scalar(" 0.30 "), json!(0.30));
412 assert_eq!(infer_scalar("-1.5e-3"), json!(-0.0015));
413 assert_eq!(infer_scalar("true"), json!(true));
414 assert_eq!(infer_scalar(""), Value::Null);
415 assert_eq!(infer_scalar("2027-07-17"), json!("2027-07-17"));
417 assert_eq!(infer_scalar("C"), json!("C"));
418 assert_eq!(infer_scalar("down_out"), json!("down_out"));
419 assert_eq!(infer_scalar("Act365"), json!("Act365"));
420 }
421
422 #[test]
423 fn elements_and_attributes_both_become_fields() {
424 let value = xml_to_value(
425 r#"<curve type="flat"><rate>0.05</rate><day_count>Act365</day_count></curve>"#,
426 )
427 .unwrap();
428 assert_eq!(value, json!({"type": "flat", "rate": 0.05, "day_count": "Act365"}));
429 }
430
431 #[test]
432 fn item_children_make_arrays_including_single_element() {
433 let value = xml_to_value("<tenors><item>0.5</item><item>1.0</item></tenors>").unwrap();
434 assert_eq!(value, json!([0.5, 1.0]));
435 let single = xml_to_value("<tenors><item>0.5</item></tenors>").unwrap();
436 assert_eq!(single, json!([0.5]));
437 }
438
439 #[test]
440 fn nested_arrays_round_trip() {
441 let xml = "<vols><item><item>0.32</item><item>0.30</item></item>\
442 <item><item>0.33</item><item>0.31</item></item></vols>";
443 assert_eq!(xml_to_value(xml).unwrap(), json!([[0.32, 0.30], [0.33, 0.31]]));
444 }
445
446 #[test]
447 fn repeated_siblings_collapse_into_an_array() {
448 let value = xml_to_value("<root><tag>a</tag><tag>b</tag><other>c</other></root>").unwrap();
449 assert_eq!(value, json!({"tag": ["a", "b"], "other": "c"}));
450 }
451
452 #[test]
453 fn empty_and_self_closing_elements_are_null() {
454 let value = xml_to_value("<root><a/><b></b><c>1</c></root>").unwrap();
455 assert_eq!(value, json!({"a": null, "b": null, "c": 1}));
456 }
457
458 #[test]
459 fn entities_and_cdata_are_decoded() {
460 let value = xml_to_value("<root><a>A & B</a><b><![CDATA[x < y]]></b></root>").unwrap();
461 assert_eq!(value, json!({"a": "A & B", "b": "x < y"}));
462 }
463
464 #[test]
465 fn declaration_and_comments_are_ignored() {
466 let value =
467 xml_to_value("<?xml version=\"1.0\"?><!-- note --><root><a>1</a></root>").unwrap();
468 assert_eq!(value, json!({"a": 1}));
469 }
470
471 #[test]
472 fn malformed_documents_are_reported() {
473 assert!(xml_to_value("<root><a></root>").is_err());
474 assert!(xml_to_value("").is_err());
475 assert!(xml_to_value("not xml at all").is_err());
476 }
477
478 #[test]
479 fn value_to_xml_round_trips_through_the_reader() {
480 let original = json!({
481 "asset": "EQ",
482 "contracts": [
483 {"action": "PV", "strike_price": 100.0, "flag": true, "missing": null},
484 {"action": "PV", "tenors": [0.5, "2028-07-16"], "nested": [[1.0, 2.0]]}
485 ]
486 });
487 let xml = value_to_xml(&original, "root");
488 let back = xml_to_value(&xml).unwrap();
489 assert_eq!(back, original, "\nXML was:\n{xml}");
490 }
491
492 #[test]
493 fn xml_special_characters_survive_a_round_trip() {
494 let original = json!({"name": "Smith & Co <\"AAA\">"});
495 let back = xml_to_value(&value_to_xml(&original, "root")).unwrap();
496 assert_eq!(back, original);
497 }
498
499 #[test]
500 fn null_fields_are_dropped_from_output() {
501 let mut v = json!({"a": 1, "b": null, "c": {"d": null, "e": 2}, "f": [{"g": null}]});
502 strip_nulls(&mut v);
503 assert_eq!(v, json!({"a": 1, "c": {"e": 2}, "f": [{}]}));
504 }
505
506 #[test]
507 fn rendered_output_is_valid_in_both_formats() {
508 let results = vec![json!({"contract": {"action": "PV", "skip": null}, "output": {"pv": 1.5}})];
509 let as_json: Value = serde_json::from_str(&render_results(&results, Format::Json)).unwrap();
510 let as_xml = xml_to_value(&render_results(&results, Format::Xml)).unwrap();
511 assert_eq!(as_json, as_xml, "both formats must carry the same data");
512 assert_eq!(as_json[0]["output"]["pv"], json!(1.5));
513 assert!(as_json[0]["contract"].get("skip").is_none());
514 }
515
516 #[test]
517 fn parse_dispatches_on_format() {
518 #[derive(serde::Deserialize, PartialEq, Debug)]
519 struct Doc {
520 a: i32,
521 b: String,
522 }
523 let from_json: Doc = parse(r#"{"a": 1, "b": "x"}"#, Format::Json).unwrap();
524 let from_xml: Doc = parse("<doc><a>1</a><b>x</b></doc>", Format::Xml).unwrap();
525 assert_eq!(from_json, from_xml);
526 }
527}