use std::error::Error;
use std::io::Cursor;
#[derive(Debug, Clone, PartialEq)]
pub enum CanonicalValue {
Null,
Bool(bool),
Number(serde_json::Number),
String(String),
Array(Vec<CanonicalValue>),
Object(Vec<(String, CanonicalValue)>),
}
type XmlStackFrame = (
String,
Vec<(String, String)>,
Vec<(String, CanonicalValue)>,
String,
);
impl CanonicalValue {
fn from_json_value(v: serde_json::Value) -> Self {
match v {
serde_json::Value::Null => Self::Null,
serde_json::Value::Bool(b) => Self::Bool(b),
serde_json::Value::Number(n) => Self::Number(n),
serde_json::Value::String(s) => Self::String(s),
serde_json::Value::Array(a) => {
Self::Array(a.into_iter().map(Self::from_json_value).collect())
}
serde_json::Value::Object(m) => Self::Object(
m.into_iter()
.map(|(k, v)| (k, Self::from_json_value(v)))
.collect(),
),
}
}
fn to_json_value(&self) -> serde_json::Value {
match self {
Self::Null => serde_json::Value::Null,
Self::Bool(b) => serde_json::Value::Bool(*b),
Self::Number(n) => serde_json::Value::Number(n.clone()),
Self::String(s) => serde_json::Value::String(s.clone()),
Self::Array(a) => {
serde_json::Value::Array(a.iter().map(|v| v.to_json_value()).collect())
}
Self::Object(o) => {
let mut map = serde_json::Map::new();
for (k, v) in o {
map.insert(k.clone(), v.to_json_value());
}
serde_json::Value::Object(map)
}
}
}
pub fn from_json(input: &str) -> Result<Self, Box<dyn Error>> {
Ok(Self::from_json_value(serde_json::from_str(input)?))
}
pub fn to_json(&self) -> Result<String, Box<dyn Error>> {
Ok(serde_json::to_string(&self.to_json_value())?)
}
fn from_delimited(input: &str, delim: u8) -> Result<Self, Box<dyn Error>> {
let input = input.strip_prefix('\u{feff}').unwrap_or(input);
let mut rdr = csv::ReaderBuilder::new()
.delimiter(delim)
.from_reader(Cursor::new(input));
let headers = rdr.headers()?.clone();
let mut seen = std::collections::HashSet::new();
for h in headers.iter() {
if !seen.insert(h) {
return Err(format!("duplicate header: {h}").into());
}
}
let mut rows = Vec::new();
for result in rdr.records() {
let record = result?;
let mut obj = Vec::with_capacity(headers.len());
for (h, v) in headers.iter().zip(record.iter()) {
obj.push((h.to_string(), Self::String(v.to_string())));
}
rows.push(Self::Object(obj));
}
Ok(Self::Array(rows))
}
fn to_delimited(&self, delim: u8) -> Result<String, Box<dyn Error>> {
let rows = self.as_tabular()?;
if rows.is_empty() {
return Ok(String::new());
}
let mut headers: Vec<String> = Vec::new();
for row in &rows {
let obj = row.as_object()?;
for (k, _) in obj {
if !headers.contains(k) {
headers.push(k.clone());
}
}
}
let mut wtr = csv::WriterBuilder::new()
.delimiter(delim)
.from_writer(vec![]);
wtr.write_record(&headers)?;
for row in &rows {
let obj = row.as_object()?;
let mut record = Vec::with_capacity(headers.len());
for h in &headers {
let cell = match obj.iter().find(|(k, _)| k == h) {
Some((_, Self::Null)) | None => String::new(),
Some((_, Self::Bool(b))) => b.to_string(),
Some((_, Self::Number(n))) => n.to_string(),
Some((_, Self::String(s))) => s.clone(),
Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
return Err(format!(
"nested value at key {h} cannot be represented in tabular output"
)
.into());
}
};
record.push(cell);
}
wtr.write_record(&record)?;
}
let data = wtr.into_inner()?;
Ok(String::from_utf8(data)?)
}
fn as_object(&self) -> Result<&Vec<(String, CanonicalValue)>, Box<dyn Error>> {
match self {
Self::Object(o) => Ok(o),
_ => Err("expected object".into()),
}
}
fn as_tabular(&self) -> Result<Vec<&CanonicalValue>, Box<dyn Error>> {
match self {
Self::Array(a) => Ok(a.iter().collect()),
Self::Object(o) if o.len() == 1 => match &o[0].1 {
Self::Array(a) => Ok(a.iter().collect()),
Self::Object(inner) if inner.len() == 1 => match &inner[0].1 {
Self::Array(a) => Ok(a.iter().collect()),
Self::Object(_) => Ok(vec![&inner[0].1]),
_ => Err("XML payload is not tabular".into()),
},
Self::Object(_) => Ok(vec![&o[0].1]),
_ => Err("XML payload is not tabular".into()),
},
_ => Err("root must be an array of objects or a single object".into()),
}
}
pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
Self::from_delimited(input, b',')
}
pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
self.to_delimited(b',')
}
pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
Self::from_delimited(input, b'\t')
}
pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
self.to_delimited(b'\t')
}
pub fn from_yaml(input: &str) -> Result<Self, Box<dyn Error>> {
let opts = serde_saphyr::options! {
strict_booleans: true,
ignore_binary_tag_for_string: true,
};
let v: serde_json::Value = serde_saphyr::from_str_with_options(input, opts)?;
Ok(Self::from_json_value(v))
}
pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
Ok(serde_saphyr::to_string(&self.to_json_value())?)
}
pub fn from_xml(input: &str) -> Result<Self, Box<dyn Error>> {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_str(input);
reader.config_mut().trim_text(true);
let mut stack: Vec<XmlStackFrame> = Vec::new();
let mut root: Option<(String, CanonicalValue)> = None;
loop {
match reader.read_event() {
Ok(Event::Start(e)) => {
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
let mut attrs = Vec::new();
for a in e.attributes() {
let a = a?;
attrs.push((
String::from_utf8_lossy(a.key.as_ref()).to_string(),
a.unescape_value()?.to_string(),
));
}
stack.push((name, attrs, Vec::new(), String::new()));
}
Ok(Event::Text(e)) => {
if let Some(top) = stack.last_mut() {
top.3.push_str(&e.unescape()?);
}
}
Ok(Event::End(_)) => {
let (name, attributes, children, text) = stack.pop().ok_or("unbalanced XML")?;
let trimmed = text.trim().to_string();
let value = if attributes.is_empty() && children.is_empty() {
Self::String(trimmed)
} else {
let mut obj: Vec<(String, CanonicalValue)> = Vec::new();
for (k, v) in attributes {
obj.push((format!("@{k}"), Self::String(v)));
}
if !trimmed.is_empty() {
obj.push(("#text".to_string(), Self::String(trimmed)));
}
for (cname, cval) in children {
if let Some(slot) = obj.iter_mut().find(|(k, _)| k == &cname) {
match &mut slot.1 {
Self::Array(a) => a.push(cval),
_ => {
let old = slot.1.clone();
slot.1 = Self::Array(vec![old, cval]);
}
}
} else {
obj.push((cname, cval));
}
}
Self::Object(obj)
};
if let Some(parent) = stack.last_mut() {
parent.2.push((name, value));
} else {
root = Some((name, value));
}
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => return Err(e.into()),
}
}
let (root_name, root_value) = root.ok_or("empty XML")?;
Ok(Self::Object(vec![(root_name, root_value)]))
}
pub fn to_xml(&self) -> Result<String, Box<dyn Error>> {
let mut writer = quick_xml::Writer::new_with_indent(Vec::new(), b' ', 2);
match self {
Self::Object(o) if o.len() == 1 && !o[0].0.starts_with('@') && o[0].0 != "#text" => {
write_element(&mut writer, &o[0].0, &o[0].1)?;
}
Self::Array(a) => {
write_open(&mut writer, "root", &[])?;
for item in a {
write_element(&mut writer, "item", item)?;
}
write_close(&mut writer, "root")?;
}
_ => write_element(&mut writer, "root", self)?,
}
Ok(String::from_utf8(writer.into_inner())?)
}
}
fn write_open<W: std::io::Write>(
writer: &mut quick_xml::Writer<W>,
name: &str,
attrs: &[(String, String)],
) -> Result<(), Box<dyn Error>> {
use quick_xml::events::{BytesStart, Event};
let mut start = BytesStart::new(name);
for (k, v) in attrs {
start.push_attribute((k.as_str(), v.as_str()));
}
writer.write_event(Event::Start(start))?;
Ok(())
}
fn write_close<W: std::io::Write>(
writer: &mut quick_xml::Writer<W>,
name: &str,
) -> Result<(), Box<dyn Error>> {
use quick_xml::events::{BytesEnd, Event};
writer.write_event(Event::End(BytesEnd::new(name)))?;
Ok(())
}
fn write_element<W: std::io::Write>(
writer: &mut quick_xml::Writer<W>,
name: &str,
value: &CanonicalValue,
) -> Result<(), Box<dyn Error>> {
use quick_xml::events::{BytesText, Event};
match value {
CanonicalValue::Array(items) => {
for item in items {
write_element(writer, name, item)?;
}
}
CanonicalValue::Object(o) => {
let attrs: Vec<(String, String)> = o
.iter()
.filter_map(|(k, v)| {
k.strip_prefix('@').and_then(|attr| match v {
CanonicalValue::String(s) => Some((attr.to_string(), s.clone())),
_ => None,
})
})
.collect();
write_open(writer, name, &attrs)?;
for (k, v) in o {
if k.starts_with('@') {
continue;
}
if k == "#text" {
if let CanonicalValue::String(s) = v {
writer.write_event(Event::Text(BytesText::new(s)))?;
}
continue;
}
write_element(writer, k, v)?;
}
write_close(writer, name)?;
}
CanonicalValue::String(s) => {
write_open(writer, name, &[])?;
writer.write_event(Event::Text(BytesText::new(s)))?;
write_close(writer, name)?;
}
CanonicalValue::Number(n) => {
write_open(writer, name, &[])?;
writer.write_event(Event::Text(BytesText::new(&n.to_string())))?;
write_close(writer, name)?;
}
CanonicalValue::Bool(b) => {
write_open(writer, name, &[])?;
writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
write_close(writer, name)?;
}
CanonicalValue::Null => {
write_open(writer, name, &[])?;
write_close(writer, name)?;
}
}
Ok(())
}