#[cfg(feature = "xml")]
use std::fmt::Write as _;
use std::path::Path;
#[cfg(feature = "xml")]
use quick_xml::events::Event;
#[cfg(feature = "xml")]
use quick_xml::Reader;
#[cfg(feature = "xml")]
use serde_json::Map;
use serde_json::Value;
use crate::core::errors::RustyQLibError;
pub const ARRAY_ITEM: &str = "item";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
#[cfg(feature = "xml")]
Xml,
}
impl Format {
pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Format> {
match path
.as_ref()
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.as_deref()
{
#[cfg(feature = "xml")]
Some("xml") => Some(Format::Xml),
Some("json") => Some(Format::Json),
_ => None,
}
}
pub fn detect(content: &str) -> Format {
match content.trim_start().chars().next() {
#[cfg(feature = "xml")]
Some('<') => Format::Xml,
_ => Format::Json,
}
}
pub fn extension(&self) -> &'static str {
match self {
Format::Json => "json",
#[cfg(feature = "xml")]
Format::Xml => "xml",
}
}
}
pub fn parse_value(content: &str, format: Format) -> Result<Value, RustyQLibError> {
match format {
Format::Json => serde_json::from_str(content).map_err(|e| RustyQLibError::ParseError(format!("invalid JSON: {e}"))),
#[cfg(feature = "xml")]
Format::Xml => xml_to_value(content),
}
}
pub fn parse<T: serde::de::DeserializeOwned>(content: &str, format: Format) -> Result<T, RustyQLibError> {
let value = parse_value(content, format)?;
serde_json::from_value(value).map_err(|e| RustyQLibError::ParseError(format!("document does not match the schema: {e}")))
}
#[cfg(feature = "xml")]
pub fn xml_to_value(xml: &str) -> Result<Value, RustyQLibError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().expand_empty_elements = false;
let mut stack: Vec<Node> = Vec::new();
let mut root: Option<(String, Value)> = None;
loop {
match reader.read_event() {
Ok(Event::Start(e)) => stack.push(Node::start(&e)?),
Ok(Event::Empty(e)) => {
let node = Node::start(&e)?;
let (name, value) = node.finish();
attach(&mut stack, &mut root, name, value)?;
}
Ok(Event::Text(e)) => {
if let Some(node) = stack.last_mut() {
let text = e
.decode()
.map_err(|err| RustyQLibError::ParseError(format!("invalid text content: {err}")))?;
node.text.push_str(text.as_ref());
}
}
Ok(Event::CData(e)) => {
if let Some(node) = stack.last_mut() {
let text = String::from_utf8(e.into_inner().into_owned())
.map_err(|err| RustyQLibError::ParseError(format!("invalid CDATA: {err}")))?;
node.text.push_str(&text);
}
}
Ok(Event::GeneralRef(e)) => {
if let Some(node) = stack.last_mut() {
node.text.push_str(&resolve_entity(&e)?);
}
}
Ok(Event::End(_)) => {
let node = stack.pop().ok_or_else(|| RustyQLibError::ParseError("unbalanced closing tag".to_string()))?;
let (name, value) = node.finish();
attach(&mut stack, &mut root, name, value)?;
}
Ok(Event::Eof) => break,
Ok(_) => {} Err(e) => return Err(RustyQLibError::ParseError(format!("malformed XML at byte {}: {e}", reader.buffer_position()))),
}
}
if !stack.is_empty() {
return Err(RustyQLibError::ParseError("unbalanced XML: unclosed elements".to_string()));
}
match root {
Some((_, value)) => Ok(value),
None => Err(RustyQLibError::ParseError("empty XML document".to_string())),
}
}
#[cfg(feature = "xml")]
struct Node {
name: String,
attrs: Vec<(String, Value)>,
children: Vec<(String, Value)>,
text: String,
}
#[cfg(feature = "xml")]
impl Node {
fn start(e: &quick_xml::events::BytesStart) -> Result<Node, RustyQLibError> {
let name = String::from_utf8(e.name().as_ref().to_vec())
.map_err(|err| RustyQLibError::ParseError(format!("invalid element name: {err}")))?;
let mut attrs = Vec::new();
for attr in e.attributes() {
let attr = attr.map_err(|err| RustyQLibError::ParseError(format!("invalid attribute in <{name}>: {err}")))?;
let key = String::from_utf8(attr.key.as_ref().to_vec())
.map_err(|err| RustyQLibError::ParseError(format!("invalid attribute name: {err}")))?;
let raw = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.map_err(|err| RustyQLibError::ParseError(format!("invalid attribute value in <{name}>: {err}")))?;
attrs.push((key, infer_scalar(raw.as_ref())));
}
Ok(Node { name, attrs, children: Vec::new(), text: String::new() })
}
fn finish(self) -> (String, Value) {
let Node { name, attrs, children, text } = self;
if !children.is_empty() && children.iter().all(|(n, _)| n == ARRAY_ITEM) {
let items = children.into_iter().map(|(_, v)| v).collect();
return (name, Value::Array(items));
}
if children.is_empty() && attrs.is_empty() {
let trimmed = text.trim();
return (name, infer_scalar(trimmed));
}
let mut map = Map::new();
for (key, value) in attrs {
map.insert(key, value);
}
for (key, value) in children {
match map.get_mut(&key) {
Some(Value::Array(existing)) => existing.push(value),
Some(slot) => {
let previous = slot.take();
*slot = Value::Array(vec![previous, value]);
}
None => {
map.insert(key, value);
}
}
}
(name, Value::Object(map))
}
}
#[cfg(feature = "xml")]
fn resolve_entity(e: &quick_xml::events::BytesRef) -> Result<String, RustyQLibError> {
if e.is_char_ref() {
return match e.resolve_char_ref() {
Ok(Some(c)) => Ok(c.to_string()),
Ok(None) => Err(RustyQLibError::ParseError("unresolvable character reference".to_string())),
Err(err) => Err(RustyQLibError::ParseError(format!("invalid character reference: {err}"))),
};
}
let name = e.decode().map_err(|err| RustyQLibError::ParseError(format!("invalid entity reference: {err}")))?;
match name.as_ref() {
"amp" => Ok("&".to_string()),
"lt" => Ok("<".to_string()),
"gt" => Ok(">".to_string()),
"quot" => Ok("\"".to_string()),
"apos" => Ok("'".to_string()),
other => Err(RustyQLibError::ParseError(format!(
"unknown entity '&{other};' (only the predefined XML entities are supported)"
))),
}
}
#[cfg(feature = "xml")]
fn attach(
stack: &mut [Node],
root: &mut Option<(String, Value)>,
name: String,
value: Value,
) -> Result<(), RustyQLibError> {
match stack.last_mut() {
Some(parent) => {
parent.children.push((name, value));
Ok(())
}
None => {
if root.is_some() {
return Err(RustyQLibError::ParseError("XML documents must have a single root element".to_string()));
}
*root = Some((name, value));
Ok(())
}
}
}
#[cfg(feature = "xml")]
fn infer_scalar(text: &str) -> Value {
let t = text.trim();
if t.is_empty() {
return Value::Null;
}
match t {
"true" => return Value::Bool(true),
"false" => return Value::Bool(false),
"null" => return Value::Null,
_ => {}
}
if let Ok(i) = t.parse::<i64>() {
return Value::Number(i.into());
}
if let Ok(f) = t.parse::<f64>() {
if f.is_finite() {
if let Some(n) = serde_json::Number::from_f64(f) {
return Value::Number(n);
}
}
}
Value::String(t.to_string())
}
pub fn render_results(results: &[Value], format: Format) -> String {
let mut array = Value::Array(results.to_vec());
strip_nulls(&mut array);
match format {
Format::Json => serde_json::to_string_pretty(&array).unwrap_or_else(|_| "[]".to_string()),
#[cfg(feature = "xml")]
Format::Xml => value_to_xml(&array, "results"),
}
}
#[cfg_attr(not(feature = "xml"), allow(unused_variables))]
pub fn render_value(value: &Value, format: Format, root: &str) -> String {
let mut value = value.clone();
strip_nulls(&mut value);
match format {
Format::Json => serde_json::to_string_pretty(&value).unwrap_or_default(),
#[cfg(feature = "xml")]
Format::Xml => value_to_xml(&value, root),
}
}
pub fn strip_nulls(value: &mut Value) {
match value {
Value::Object(map) => {
map.retain(|_, v| !v.is_null());
for v in map.values_mut() {
strip_nulls(v);
}
}
Value::Array(items) => {
for item in items {
strip_nulls(item);
}
}
_ => {}
}
}
#[cfg(feature = "xml")]
pub fn value_to_xml(value: &Value, root: &str) -> String {
let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
write_element(&mut out, root, value, 0);
out
}
#[cfg(feature = "xml")]
fn write_element(out: &mut String, name: &str, value: &Value, depth: usize) {
let pad = " ".repeat(depth);
match value {
Value::Null => {
let _ = writeln!(out, "{pad}<{name}/>");
}
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
let text = match value {
Value::String(s) => escape_text(s),
other => other.to_string(),
};
let _ = writeln!(out, "{pad}<{name}>{text}</{name}>");
}
Value::Array(items) => {
if items.is_empty() {
let _ = writeln!(out, "{pad}<{name}/>");
return;
}
let _ = writeln!(out, "{pad}<{name}>");
for item in items {
write_element(out, ARRAY_ITEM, item, depth + 1);
}
let _ = writeln!(out, "{pad}</{name}>");
}
Value::Object(map) => {
if map.is_empty() {
let _ = writeln!(out, "{pad}<{name}/>");
return;
}
let _ = writeln!(out, "{pad}<{name}>");
for (key, child) in map {
write_element(out, key, child, depth + 1);
}
let _ = writeln!(out, "{pad}</{name}>");
}
}
}
#[cfg(feature = "xml")]
fn escape_text(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn detects_format_from_content_and_path() {
assert_eq!(Format::detect(" { \"a\": 1 }"), Format::Json);
#[cfg(feature = "xml")]
{
assert_eq!(Format::detect("\n<?xml version=\"1.0\"?><a/>"), Format::Xml);
assert_eq!(Format::detect("<contracts/>"), Format::Xml);
assert_eq!(Format::from_path("in.xml"), Some(Format::Xml));
}
assert_eq!(Format::from_path("in.JSON"), Some(Format::Json));
assert_eq!(Format::from_path("in.txt"), None);
}
#[test]
#[cfg(feature = "xml")]
fn scalars_are_inferred_without_eating_dates_or_codes() {
assert_eq!(infer_scalar("100"), json!(100));
assert_eq!(infer_scalar(" 0.30 "), json!(0.30));
assert_eq!(infer_scalar("-1.5e-3"), json!(-0.0015));
assert_eq!(infer_scalar("true"), json!(true));
assert_eq!(infer_scalar(""), Value::Null);
assert_eq!(infer_scalar("2027-07-17"), json!("2027-07-17"));
assert_eq!(infer_scalar("C"), json!("C"));
assert_eq!(infer_scalar("down_out"), json!("down_out"));
assert_eq!(infer_scalar("Act365"), json!("Act365"));
}
#[test]
#[cfg(feature = "xml")]
fn elements_and_attributes_both_become_fields() {
let value = xml_to_value(
r#"<curve type="flat"><rate>0.05</rate><day_count>Act365</day_count></curve>"#,
)
.unwrap();
assert_eq!(value, json!({"type": "flat", "rate": 0.05, "day_count": "Act365"}));
}
#[test]
#[cfg(feature = "xml")]
fn item_children_make_arrays_including_single_element() {
let value = xml_to_value("<tenors><item>0.5</item><item>1.0</item></tenors>").unwrap();
assert_eq!(value, json!([0.5, 1.0]));
let single = xml_to_value("<tenors><item>0.5</item></tenors>").unwrap();
assert_eq!(single, json!([0.5]));
}
#[test]
#[cfg(feature = "xml")]
fn nested_arrays_round_trip() {
let xml = "<vols><item><item>0.32</item><item>0.30</item></item>\
<item><item>0.33</item><item>0.31</item></item></vols>";
assert_eq!(xml_to_value(xml).unwrap(), json!([[0.32, 0.30], [0.33, 0.31]]));
}
#[test]
#[cfg(feature = "xml")]
fn repeated_siblings_collapse_into_an_array() {
let value = xml_to_value("<root><tag>a</tag><tag>b</tag><other>c</other></root>").unwrap();
assert_eq!(value, json!({"tag": ["a", "b"], "other": "c"}));
}
#[test]
#[cfg(feature = "xml")]
fn empty_and_self_closing_elements_are_null() {
let value = xml_to_value("<root><a/><b></b><c>1</c></root>").unwrap();
assert_eq!(value, json!({"a": null, "b": null, "c": 1}));
}
#[test]
#[cfg(feature = "xml")]
fn entities_and_cdata_are_decoded() {
let value = xml_to_value("<root><a>A & B</a><b><![CDATA[x < y]]></b></root>").unwrap();
assert_eq!(value, json!({"a": "A & B", "b": "x < y"}));
}
#[test]
#[cfg(feature = "xml")]
fn declaration_and_comments_are_ignored() {
let value =
xml_to_value("<?xml version=\"1.0\"?><!-- note --><root><a>1</a></root>").unwrap();
assert_eq!(value, json!({"a": 1}));
}
#[test]
#[cfg(feature = "xml")]
fn malformed_documents_are_reported() {
assert!(xml_to_value("<root><a></root>").is_err());
assert!(xml_to_value("").is_err());
assert!(xml_to_value("not xml at all").is_err());
}
#[test]
#[cfg(feature = "xml")]
fn value_to_xml_round_trips_through_the_reader() {
let original = json!({
"asset": "EQ",
"contracts": [
{"action": "PV", "strike_price": 100.0, "flag": true, "missing": null},
{"action": "PV", "tenors": [0.5, "2028-07-16"], "nested": [[1.0, 2.0]]}
]
});
let xml = value_to_xml(&original, "root");
let back = xml_to_value(&xml).unwrap();
assert_eq!(back, original, "\nXML was:\n{xml}");
}
#[test]
#[cfg(feature = "xml")]
fn xml_special_characters_survive_a_round_trip() {
let original = json!({"name": "Smith & Co <\"AAA\">"});
let back = xml_to_value(&value_to_xml(&original, "root")).unwrap();
assert_eq!(back, original);
}
#[test]
fn null_fields_are_dropped_from_output() {
let mut v = json!({"a": 1, "b": null, "c": {"d": null, "e": 2}, "f": [{"g": null}]});
strip_nulls(&mut v);
assert_eq!(v, json!({"a": 1, "c": {"e": 2}, "f": [{}]}));
}
#[test]
#[cfg(feature = "xml")]
fn rendered_output_is_valid_in_both_formats() {
let results = vec![json!({"contract": {"action": "PV", "skip": null}, "output": {"pv": 1.5}})];
let as_json: Value = serde_json::from_str(&render_results(&results, Format::Json)).unwrap();
let as_xml = xml_to_value(&render_results(&results, Format::Xml)).unwrap();
assert_eq!(as_json, as_xml, "both formats must carry the same data");
assert_eq!(as_json[0]["output"]["pv"], json!(1.5));
assert!(as_json[0]["contract"].get("skip").is_none());
}
#[test]
#[cfg(feature = "xml")]
fn parse_dispatches_on_format() {
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Doc {
a: i32,
b: String,
}
let from_json: Doc = parse(r#"{"a": 1, "b": "x"}"#, Format::Json).unwrap();
let from_xml: Doc = parse("<doc><a>1</a><b>x</b></doc>", Format::Xml).unwrap();
assert_eq!(from_json, from_xml);
}
}