Skip to main content

sheeter_engine/
canonical.rs

1use std::error::Error;
2use std::io::Cursor;
3
4/// Format-agnostic intermediate value used by the Sheeter hub.
5///
6/// Every reader (CSV, TSV, JSON, YAML, XML) produces a `CanonicalValue`, and
7/// every writer consumes one. Adding a format requires exactly one reader and
8/// one writer, not `N-1` pairwise converters, because every spoke composes
9/// through this hub.
10///
11/// Tabular data is represented as `Array<Object>`. XML documents are
12/// represented as a single-key `Object` whose key is the root element name,
13/// with attributes prefixed by `@` and text stored under `#text`. Repeated
14/// sibling elements become `Array`.
15///
16/// # Examples
17///
18/// ```
19/// use sheeter_engine::CanonicalValue;
20///
21/// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
22/// assert_eq!(v.to_csv().unwrap(), "a\n1\n");
23/// ```
24#[derive(Debug, Clone, PartialEq)]
25pub enum CanonicalValue {
26    Null,
27    Bool(bool),
28    Number(serde_json::Number),
29    String(String),
30    Array(Vec<CanonicalValue>),
31    Object(Vec<(String, CanonicalValue)>),
32}
33
34type XmlStackFrame = (
35    String,
36    Vec<(String, String)>,
37    Vec<(String, CanonicalValue)>,
38    String,
39);
40
41impl CanonicalValue {
42    fn from_json_value(v: serde_json::Value) -> Self {
43        match v {
44            serde_json::Value::Null => Self::Null,
45            serde_json::Value::Bool(b) => Self::Bool(b),
46            serde_json::Value::Number(n) => Self::Number(n),
47            serde_json::Value::String(s) => Self::String(s),
48            serde_json::Value::Array(a) => {
49                Self::Array(a.into_iter().map(Self::from_json_value).collect())
50            }
51            serde_json::Value::Object(m) => Self::Object(
52                m.into_iter()
53                    .map(|(k, v)| (k, Self::from_json_value(v)))
54                    .collect(),
55            ),
56        }
57    }
58
59    fn to_json_value(&self) -> serde_json::Value {
60        match self {
61            Self::Null => serde_json::Value::Null,
62            Self::Bool(b) => serde_json::Value::Bool(*b),
63            Self::Number(n) => serde_json::Value::Number(n.clone()),
64            Self::String(s) => serde_json::Value::String(s.clone()),
65            Self::Array(a) => {
66                serde_json::Value::Array(a.iter().map(|v| v.to_json_value()).collect())
67            }
68            Self::Object(o) => {
69                let mut map = serde_json::Map::new();
70                for (k, v) in o {
71                    map.insert(k.clone(), v.to_json_value());
72                }
73                serde_json::Value::Object(map)
74            }
75        }
76    }
77
78    /// Parses JSON into the canonical hub.
79    ///
80    /// # Errors
81    /// Returns an error if the JSON is malformed.
82    ///
83    /// # Examples
84    /// ```
85    /// use sheeter_engine::CanonicalValue;
86    /// assert!(CanonicalValue::from_json(r#"{"a":1}"#).is_ok());
87    /// ```
88    pub fn from_json(input: &str) -> Result<Self, Box<dyn Error>> {
89        Ok(Self::from_json_value(serde_json::from_str(input)?))
90    }
91
92    /// Serializes the canonical hub to JSON.
93    ///
94    /// # Examples
95    /// ```
96    /// use sheeter_engine::CanonicalValue;
97    /// let v = CanonicalValue::String("x".into());
98    /// assert_eq!(v.to_json().unwrap(), r#""x""#);
99    /// ```
100    pub fn to_json(&self) -> Result<String, Box<dyn Error>> {
101        Ok(serde_json::to_string(&self.to_json_value())?)
102    }
103
104    fn from_delimited(input: &str, delim: u8) -> Result<Self, Box<dyn Error>> {
105        let input = input.strip_prefix('\u{feff}').unwrap_or(input);
106        let mut rdr = csv::ReaderBuilder::new()
107            .delimiter(delim)
108            .from_reader(Cursor::new(input));
109        let headers = rdr.headers()?.clone();
110        let mut seen = std::collections::HashSet::new();
111        for h in headers.iter() {
112            if !seen.insert(h) {
113                return Err(format!("duplicate header: {h}").into());
114            }
115        }
116        let mut rows = Vec::new();
117        for result in rdr.records() {
118            let record = result?;
119            let mut obj = Vec::with_capacity(headers.len());
120            for (h, v) in headers.iter().zip(record.iter()) {
121                obj.push((h.to_string(), Self::String(v.to_string())));
122            }
123            rows.push(Self::Object(obj));
124        }
125        Ok(Self::Array(rows))
126    }
127
128    fn to_delimited(&self, delim: u8) -> Result<String, Box<dyn Error>> {
129        let rows = self.as_tabular()?;
130        if rows.is_empty() {
131            return Ok(String::new());
132        }
133        let mut headers: Vec<String> = Vec::new();
134        for row in &rows {
135            let obj = row.as_object()?;
136            for (k, _) in obj {
137                if !headers.contains(k) {
138                    headers.push(k.clone());
139                }
140            }
141        }
142        let mut wtr = csv::WriterBuilder::new()
143            .delimiter(delim)
144            .from_writer(vec![]);
145        wtr.write_record(&headers)?;
146        for row in &rows {
147            let obj = row.as_object()?;
148            let mut record = Vec::with_capacity(headers.len());
149            for h in &headers {
150                let cell = match obj.iter().find(|(k, _)| k == h) {
151                    Some((_, Self::Null)) | None => String::new(),
152                    Some((_, Self::Bool(b))) => b.to_string(),
153                    Some((_, Self::Number(n))) => n.to_string(),
154                    Some((_, Self::String(s))) => s.clone(),
155                    Some((_, Self::Array(_))) | Some((_, Self::Object(_))) => {
156                        return Err(format!(
157                            "nested value at key {h} cannot be represented in tabular output"
158                        )
159                        .into());
160                    }
161                };
162                record.push(cell);
163            }
164            wtr.write_record(&record)?;
165        }
166        let data = wtr.into_inner()?;
167        Ok(String::from_utf8(data)?)
168    }
169
170    fn as_object(&self) -> Result<&Vec<(String, CanonicalValue)>, Box<dyn Error>> {
171        match self {
172            Self::Object(o) => Ok(o),
173            _ => Err("expected object".into()),
174        }
175    }
176
177    fn as_tabular(&self) -> Result<Vec<&CanonicalValue>, Box<dyn Error>> {
178        match self {
179            Self::Array(a) => Ok(a.iter().collect()),
180            Self::Object(o) if o.len() == 1 => match &o[0].1 {
181                Self::Array(a) => Ok(a.iter().collect()),
182                Self::Object(inner) if inner.len() == 1 => match &inner[0].1 {
183                    Self::Array(a) => Ok(a.iter().collect()),
184                    Self::Object(_) => Ok(vec![&inner[0].1]),
185                    _ => Err("XML payload is not tabular".into()),
186                },
187                Self::Object(_) => Ok(vec![&o[0].1]),
188                _ => Err("XML payload is not tabular".into()),
189            },
190            _ => Err("root must be an array of objects or a single object".into()),
191        }
192    }
193
194    /// Parses CSV into the canonical hub.
195    ///
196    /// # Examples
197    /// ```
198    /// use sheeter_engine::CanonicalValue;
199    /// let v = CanonicalValue::from_csv("a\n1").unwrap();
200    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1"}]"#);
201    /// ```
202    pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
203        Self::from_delimited(input, b',')
204    }
205
206    /// Serializes the canonical hub to CSV.
207    ///
208    /// # Examples
209    /// ```
210    /// use sheeter_engine::CanonicalValue;
211    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
212    /// assert_eq!(v.to_csv().unwrap(), "a\n1\n");
213    /// ```
214    pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
215        self.to_delimited(b',')
216    }
217
218    /// Parses TSV into the canonical hub.
219    ///
220    /// # Examples
221    /// ```
222    /// use sheeter_engine::CanonicalValue;
223    /// let v = CanonicalValue::from_tsv("a\tb\n1\t2").unwrap();
224    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1","b":"2"}]"#);
225    /// ```
226    pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
227        Self::from_delimited(input, b'\t')
228    }
229
230    /// Serializes the canonical hub to TSV.
231    ///
232    /// # Examples
233    /// ```
234    /// use sheeter_engine::CanonicalValue;
235    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
236    /// assert_eq!(v.to_tsv().unwrap(), "a\n1\n");
237    /// ```
238    pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
239        self.to_delimited(b'\t')
240    }
241
242    /// Parses YAML into the canonical hub.
243    ///
244    /// # Examples
245    /// ```
246    /// use sheeter_engine::CanonicalValue;
247    /// let v = CanonicalValue::from_yaml("- a: 1\n").unwrap();
248    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":1}]"#);
249    /// ```
250    pub fn from_yaml(input: &str) -> Result<Self, Box<dyn Error>> {
251        let opts = serde_saphyr::options! {
252            strict_booleans: true,
253            ignore_binary_tag_for_string: true,
254        };
255        let v: serde_json::Value = serde_saphyr::from_str_with_options(input, opts)?;
256        Ok(Self::from_json_value(v))
257    }
258
259    /// Serializes the canonical hub to YAML.
260    ///
261    /// # Examples
262    /// ```
263    /// use sheeter_engine::CanonicalValue;
264    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
265    /// assert!(v.to_yaml().unwrap().contains("a: 1"));
266    /// ```
267    pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
268        Ok(serde_saphyr::to_string(&self.to_json_value())?)
269    }
270
271    /// Parses XML into the canonical hub.
272    ///
273    /// Attributes become `@name` keys, text content becomes `#text`, and
274    /// repeated sibling elements become arrays. The returned value is an
275    /// object with exactly one key: the root element name.
276    ///
277    /// # Examples
278    /// ```
279    /// use sheeter_engine::CanonicalValue;
280    /// let v = CanonicalValue::from_xml("<r><a>1</a><a>2</a></r>").unwrap();
281    /// assert_eq!(v.to_json().unwrap(), r#"{"r":{"a":["1","2"]}}"#);
282    /// ```
283    pub fn from_xml(input: &str) -> Result<Self, Box<dyn Error>> {
284        use quick_xml::events::Event;
285        use quick_xml::Reader;
286
287        let mut reader = Reader::from_str(input);
288        reader.config_mut().trim_text(true);
289        let mut stack: Vec<XmlStackFrame> = Vec::new();
290        let mut root: Option<(String, CanonicalValue)> = None;
291
292        loop {
293            match reader.read_event() {
294                Ok(Event::Start(e)) => {
295                    let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
296                    let mut attrs = Vec::new();
297                    for a in e.attributes() {
298                        let a = a?;
299                        attrs.push((
300                            String::from_utf8_lossy(a.key.as_ref()).to_string(),
301                            a.unescape_value()?.to_string(),
302                        ));
303                    }
304                    stack.push((name, attrs, Vec::new(), String::new()));
305                }
306                Ok(Event::Text(e)) => {
307                    if let Some(top) = stack.last_mut() {
308                        top.3.push_str(&e.unescape()?);
309                    }
310                }
311                Ok(Event::End(_)) => {
312                    let (name, attributes, children, text) = stack.pop().ok_or("unbalanced XML")?;
313                    let trimmed = text.trim().to_string();
314                    let value = if attributes.is_empty() && children.is_empty() {
315                        Self::String(trimmed)
316                    } else {
317                        let mut obj: Vec<(String, CanonicalValue)> = Vec::new();
318                        for (k, v) in attributes {
319                            obj.push((format!("@{k}"), Self::String(v)));
320                        }
321                        if !trimmed.is_empty() {
322                            obj.push(("#text".to_string(), Self::String(trimmed)));
323                        }
324                        for (cname, cval) in children {
325                            if let Some(slot) = obj.iter_mut().find(|(k, _)| k == &cname) {
326                                match &mut slot.1 {
327                                    Self::Array(a) => a.push(cval),
328                                    _ => {
329                                        let old = slot.1.clone();
330                                        slot.1 = Self::Array(vec![old, cval]);
331                                    }
332                                }
333                            } else {
334                                obj.push((cname, cval));
335                            }
336                        }
337                        Self::Object(obj)
338                    };
339                    if let Some(parent) = stack.last_mut() {
340                        parent.2.push((name, value));
341                    } else {
342                        root = Some((name, value));
343                    }
344                }
345                Ok(Event::Eof) => break,
346                Ok(_) => {}
347                Err(e) => return Err(e.into()),
348            }
349        }
350
351        let (root_name, root_value) = root.ok_or("empty XML")?;
352        Ok(Self::Object(vec![(root_name, root_value)]))
353    }
354
355    /// Serializes the canonical hub to XML.
356    ///
357    /// # Examples
358    /// ```
359    /// use sheeter_engine::CanonicalValue;
360    /// let v = CanonicalValue::from_json(r#"[{"a":"1"}]"#).unwrap();
361    /// let xml = v.to_xml().unwrap();
362    /// assert!(xml.contains("<root>"));
363    /// assert!(xml.contains("<a>1</a>"));
364    /// ```
365    pub fn to_xml(&self) -> Result<String, Box<dyn Error>> {
366        let mut writer = quick_xml::Writer::new_with_indent(Vec::new(), b' ', 2);
367        match self {
368            Self::Object(o) if o.len() == 1 && !o[0].0.starts_with('@') && o[0].0 != "#text" => {
369                write_element(&mut writer, &o[0].0, &o[0].1)?;
370            }
371            Self::Array(a) => {
372                write_open(&mut writer, "root", &[])?;
373                for item in a {
374                    write_element(&mut writer, "item", item)?;
375                }
376                write_close(&mut writer, "root")?;
377            }
378            _ => write_element(&mut writer, "root", self)?,
379        }
380        Ok(String::from_utf8(writer.into_inner())?)
381    }
382}
383
384fn write_open<W: std::io::Write>(
385    writer: &mut quick_xml::Writer<W>,
386    name: &str,
387    attrs: &[(String, String)],
388) -> Result<(), Box<dyn Error>> {
389    use quick_xml::events::{BytesStart, Event};
390    let mut start = BytesStart::new(name);
391    for (k, v) in attrs {
392        start.push_attribute((k.as_str(), v.as_str()));
393    }
394    writer.write_event(Event::Start(start))?;
395    Ok(())
396}
397
398fn write_close<W: std::io::Write>(
399    writer: &mut quick_xml::Writer<W>,
400    name: &str,
401) -> Result<(), Box<dyn Error>> {
402    use quick_xml::events::{BytesEnd, Event};
403    writer.write_event(Event::End(BytesEnd::new(name)))?;
404    Ok(())
405}
406
407fn write_element<W: std::io::Write>(
408    writer: &mut quick_xml::Writer<W>,
409    name: &str,
410    value: &CanonicalValue,
411) -> Result<(), Box<dyn Error>> {
412    use quick_xml::events::{BytesText, Event};
413
414    match value {
415        CanonicalValue::Array(items) => {
416            for item in items {
417                write_element(writer, name, item)?;
418            }
419        }
420        CanonicalValue::Object(o) => {
421            let attrs: Vec<(String, String)> = o
422                .iter()
423                .filter_map(|(k, v)| {
424                    k.strip_prefix('@').and_then(|attr| match v {
425                        CanonicalValue::String(s) => Some((attr.to_string(), s.clone())),
426                        _ => None,
427                    })
428                })
429                .collect();
430            write_open(writer, name, &attrs)?;
431            for (k, v) in o {
432                if k.starts_with('@') {
433                    continue;
434                }
435                if k == "#text" {
436                    if let CanonicalValue::String(s) = v {
437                        writer.write_event(Event::Text(BytesText::new(s)))?;
438                    }
439                    continue;
440                }
441                write_element(writer, k, v)?;
442            }
443            write_close(writer, name)?;
444        }
445        CanonicalValue::String(s) => {
446            write_open(writer, name, &[])?;
447            writer.write_event(Event::Text(BytesText::new(s)))?;
448            write_close(writer, name)?;
449        }
450        CanonicalValue::Number(n) => {
451            write_open(writer, name, &[])?;
452            writer.write_event(Event::Text(BytesText::new(&n.to_string())))?;
453            write_close(writer, name)?;
454        }
455        CanonicalValue::Bool(b) => {
456            write_open(writer, name, &[])?;
457            writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
458            write_close(writer, name)?;
459        }
460        CanonicalValue::Null => {
461            write_open(writer, name, &[])?;
462            write_close(writer, name)?;
463        }
464    }
465    Ok(())
466}