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