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    pub fn from_toml(input: &str) -> Result<Self, Box<dyn Error>> {
171        let value: toml::Value = toml::from_str(input)?;
172        Ok(Self::from_toml_value(value))
173    }
174
175    fn from_toml_value(v: toml::Value) -> Self {
176        match v {
177            toml::Value::String(s) => Self::String(s),
178            toml::Value::Integer(i) => Self::Number(serde_json::Number::from(i)),
179            toml::Value::Float(f) => match serde_json::Number::from_f64(f) {
180                Some(n) => Self::Number(n),
181                None => Self::String(f.to_string()),
182            },
183            toml::Value::Boolean(b) => Self::Bool(b),
184            toml::Value::Datetime(dt) => Self::String(dt.to_string()),
185            toml::Value::Array(a) => {
186                Self::Array(a.into_iter().map(Self::from_toml_value).collect())
187            }
188            toml::Value::Table(t) => Self::Object(
189                t.into_iter()
190                    .map(|(k, v)| (k, Self::from_toml_value(v)))
191                    .collect(),
192            ),
193        }
194    }
195
196    pub fn to_toml(&self) -> Result<String, Box<dyn Error>> {
197        let value = match self {
198            Self::Array(_) => {
199                let mut table = toml::map::Map::new();
200                table.insert("item".to_string(), self.to_toml_value()?);
201                toml::Value::Table(table)
202            }
203            Self::Object(_) => self.to_toml_value()?,
204            _ => {
205                let mut table = toml::map::Map::new();
206                table.insert("value".to_string(), self.to_toml_value()?);
207                toml::Value::Table(table)
208            }
209        };
210        Ok(toml::to_string_pretty(&value)?)
211    }
212
213    fn to_toml_value(&self) -> Result<toml::Value, Box<dyn Error>> {
214        Ok(match self {
215            Self::Null => return Err("TOML has no null type".into()),
216            Self::Bool(b) => toml::Value::Boolean(*b),
217            Self::Number(n) => {
218                if let Some(i) = n.as_i64() {
219                    toml::Value::Integer(i)
220                } else if let Some(u) = n.as_u64() {
221                    if u <= i64::MAX as u64 {
222                        toml::Value::Integer(u as i64)
223                    } else {
224                        return Err(format!("TOML integer out of range: {u}").into());
225                    }
226                } else if let Some(f) = n.as_f64() {
227                    toml::Value::Float(f)
228                } else {
229                    return Err(format!("cannot represent number {n} in TOML").into());
230                }
231            }
232            Self::String(s) => toml::Value::String(s.clone()),
233            Self::Array(a) => {
234                let mut items = Vec::with_capacity(a.len());
235                for item in a {
236                    if let Self::Null = item {
237                        return Err("TOML arrays cannot contain null".into());
238                    }
239                    items.push(item.to_toml_value()?);
240                }
241                toml::Value::Array(items)
242            }
243            Self::Object(o) => {
244                let mut table = toml::map::Map::new();
245                for (k, v) in o {
246                    if let Self::Null = v {
247                        continue;
248                    }
249                    table.insert(k.clone(), v.to_toml_value()?);
250                }
251                toml::Value::Table(table)
252            }
253        })
254    }
255
256    fn as_object(&self) -> Result<&Vec<(String, CanonicalValue)>, Box<dyn Error>> {
257        match self {
258            Self::Object(o) => Ok(o),
259            _ => Err("expected object".into()),
260        }
261    }
262
263    fn as_tabular(&self) -> Result<Vec<&CanonicalValue>, Box<dyn Error>> {
264        if let Some(arr) = find_first_object_array(self) {
265            return Ok(arr.iter().collect());
266        }
267        let mut current = self;
268        while let Self::Object(o) = current {
269            if o.len() != 1 {
270                break;
271            }
272            match &o[0].1 {
273                Self::Object(_) => current = &o[0].1,
274                _ => break,
275            }
276        }
277        if let Self::Object(o) = current {
278            if o.iter().all(|(_, v)| is_scalar(v)) {
279                return Ok(vec![current]);
280            }
281        }
282        Err("cannot determine tabular structure".into())
283    }
284
285    /// Parses CSV into the canonical hub.
286    ///
287    /// # Examples
288    /// ```
289    /// use sheeter_engine::CanonicalValue;
290    /// let v = CanonicalValue::from_csv("a\n1").unwrap();
291    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1"}]"#);
292    /// ```
293    pub fn from_csv(input: &str) -> Result<Self, Box<dyn Error>> {
294        Self::from_delimited(input, b',')
295    }
296
297    /// Serializes the canonical hub to CSV.
298    ///
299    /// # Examples
300    /// ```
301    /// use sheeter_engine::CanonicalValue;
302    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
303    /// assert_eq!(v.to_csv().unwrap(), "a\n1\n");
304    /// ```
305    pub fn to_csv(&self) -> Result<String, Box<dyn Error>> {
306        self.to_delimited(b',')
307    }
308
309    /// Parses TSV into the canonical hub.
310    ///
311    /// # Examples
312    /// ```
313    /// use sheeter_engine::CanonicalValue;
314    /// let v = CanonicalValue::from_tsv("a\tb\n1\t2").unwrap();
315    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":"1","b":"2"}]"#);
316    /// ```
317    pub fn from_tsv(input: &str) -> Result<Self, Box<dyn Error>> {
318        Self::from_delimited(input, b'\t')
319    }
320
321    /// Serializes the canonical hub to TSV.
322    ///
323    /// # Examples
324    /// ```
325    /// use sheeter_engine::CanonicalValue;
326    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
327    /// assert_eq!(v.to_tsv().unwrap(), "a\n1\n");
328    /// ```
329    pub fn to_tsv(&self) -> Result<String, Box<dyn Error>> {
330        self.to_delimited(b'\t')
331    }
332
333    /// Parses YAML into the canonical hub.
334    ///
335    /// # Examples
336    /// ```
337    /// use sheeter_engine::CanonicalValue;
338    /// let v = CanonicalValue::from_yaml("- a: 1\n").unwrap();
339    /// assert_eq!(v.to_json().unwrap(), r#"[{"a":1}]"#);
340    /// ```
341    pub fn from_yaml(input: &str) -> Result<Self, Box<dyn Error>> {
342        let opts = serde_saphyr::options! {
343            strict_booleans: true,
344            ignore_binary_tag_for_string: true,
345        };
346        let v: serde_json::Value = serde_saphyr::from_str_with_options(input, opts)?;
347        Ok(Self::from_json_value(v))
348    }
349
350    /// Serializes the canonical hub to YAML.
351    ///
352    /// # Examples
353    /// ```
354    /// use sheeter_engine::CanonicalValue;
355    /// let v = CanonicalValue::from_json(r#"[{"a":1}]"#).unwrap();
356    /// assert!(v.to_yaml().unwrap().contains("a: 1"));
357    /// ```
358    pub fn to_yaml(&self) -> Result<String, Box<dyn Error>> {
359        Ok(serde_saphyr::to_string(&self.to_json_value())?)
360    }
361
362    /// Parses XML into the canonical hub.
363    ///
364    /// Attributes become `@name` keys, text content becomes `#text`, and
365    /// repeated sibling elements become arrays. The returned value is an
366    /// object with exactly one key: the root element name.
367    ///
368    /// # Examples
369    /// ```
370    /// use sheeter_engine::CanonicalValue;
371    /// let v = CanonicalValue::from_xml("<r><a>1</a><a>2</a></r>").unwrap();
372    /// assert_eq!(v.to_json().unwrap(), r#"{"r":{"a":["1","2"]}}"#);
373    /// ```
374    pub fn from_xml(input: &str) -> Result<Self, Box<dyn Error>> {
375        use quick_xml::events::Event;
376        use quick_xml::Reader;
377
378        let mut reader = Reader::from_str(input);
379        reader.config_mut().trim_text(true);
380        let mut stack: Vec<XmlStackFrame> = Vec::new();
381        let mut root: Option<(String, CanonicalValue)> = None;
382
383        loop {
384            match reader.read_event() {
385                Ok(Event::Start(e)) => {
386                    let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
387                    let mut attrs = Vec::new();
388                    for a in e.attributes() {
389                        let a = a?;
390                        attrs.push((
391                            String::from_utf8_lossy(a.key.as_ref()).to_string(),
392                            a.unescape_value()?.to_string(),
393                        ));
394                    }
395                    stack.push((name, attrs, Vec::new(), String::new()));
396                }
397                Ok(Event::Text(e)) => {
398                    if let Some(top) = stack.last_mut() {
399                        top.3.push_str(&e.unescape()?);
400                    }
401                }
402                Ok(Event::End(_)) => {
403                    let (name, attributes, children, text) = stack.pop().ok_or("unbalanced XML")?;
404                    let trimmed = text.trim().to_string();
405                    let value = if attributes.is_empty() && children.is_empty() {
406                        Self::String(trimmed)
407                    } else {
408                        let mut obj: Vec<(String, CanonicalValue)> = Vec::new();
409                        for (k, v) in attributes {
410                            obj.push((format!("@{k}"), Self::String(v)));
411                        }
412                        if !trimmed.is_empty() {
413                            obj.push(("#text".to_string(), Self::String(trimmed)));
414                        }
415                        for (cname, cval) in children {
416                            if let Some(slot) = obj.iter_mut().find(|(k, _)| k == &cname) {
417                                match &mut slot.1 {
418                                    Self::Array(a) => a.push(cval),
419                                    _ => {
420                                        let old = slot.1.clone();
421                                        slot.1 = Self::Array(vec![old, cval]);
422                                    }
423                                }
424                            } else {
425                                obj.push((cname, cval));
426                            }
427                        }
428                        Self::Object(obj)
429                    };
430                    if let Some(parent) = stack.last_mut() {
431                        parent.2.push((name, value));
432                    } else {
433                        root = Some((name, value));
434                    }
435                }
436                Ok(Event::Eof) => break,
437                Ok(_) => {}
438                Err(e) => return Err(e.into()),
439            }
440        }
441
442        let (root_name, root_value) = root.ok_or("empty XML")?;
443        Ok(Self::Object(vec![(root_name, root_value)]))
444    }
445
446    /// Serializes the canonical hub to XML.
447    ///
448    /// # Examples
449    /// ```
450    /// use sheeter_engine::CanonicalValue;
451    /// let v = CanonicalValue::from_json(r#"[{"a":"1"}]"#).unwrap();
452    /// let xml = v.to_xml().unwrap();
453    /// assert!(xml.contains("<root>"));
454    /// assert!(xml.contains("<a>1</a>"));
455    /// ```
456    pub fn to_xml(&self) -> Result<String, Box<dyn Error>> {
457        let mut writer = quick_xml::Writer::new_with_indent(Vec::new(), b' ', 2);
458        match self {
459            Self::Object(o) if o.len() == 1 && !o[0].0.starts_with('@') && o[0].0 != "#text" => {
460                write_element(&mut writer, &o[0].0, &o[0].1)?;
461            }
462            Self::Array(a) => {
463                write_open(&mut writer, "root", &[])?;
464                for item in a {
465                    write_element(&mut writer, "item", item)?;
466                }
467                write_close(&mut writer, "root")?;
468            }
469            _ => write_element(&mut writer, "root", self)?,
470        }
471        Ok(String::from_utf8(writer.into_inner())?)
472    }
473}
474
475fn write_open<W: std::io::Write>(
476    writer: &mut quick_xml::Writer<W>,
477    name: &str,
478    attrs: &[(String, String)],
479) -> Result<(), Box<dyn Error>> {
480    use quick_xml::events::{BytesStart, Event};
481    let mut start = BytesStart::new(name);
482    for (k, v) in attrs {
483        start.push_attribute((k.as_str(), v.as_str()));
484    }
485    writer.write_event(Event::Start(start))?;
486    Ok(())
487}
488
489fn write_close<W: std::io::Write>(
490    writer: &mut quick_xml::Writer<W>,
491    name: &str,
492) -> Result<(), Box<dyn Error>> {
493    use quick_xml::events::{BytesEnd, Event};
494    writer.write_event(Event::End(BytesEnd::new(name)))?;
495    Ok(())
496}
497
498fn write_element<W: std::io::Write>(
499    writer: &mut quick_xml::Writer<W>,
500    name: &str,
501    value: &CanonicalValue,
502) -> Result<(), Box<dyn Error>> {
503    use quick_xml::events::{BytesText, Event};
504
505    match value {
506        CanonicalValue::Array(items) => {
507            for item in items {
508                write_element(writer, name, item)?;
509            }
510        }
511        CanonicalValue::Object(o) => {
512            let attrs: Vec<(String, String)> = o
513                .iter()
514                .filter_map(|(k, v)| {
515                    k.strip_prefix('@').and_then(|attr| match v {
516                        CanonicalValue::String(s) => Some((attr.to_string(), s.clone())),
517                        _ => None,
518                    })
519                })
520                .collect();
521            write_open(writer, name, &attrs)?;
522            for (k, v) in o {
523                if k.starts_with('@') {
524                    continue;
525                }
526                if k == "#text" {
527                    if let CanonicalValue::String(s) = v {
528                        writer.write_event(Event::Text(BytesText::new(s)))?;
529                    }
530                    continue;
531                }
532                write_element(writer, k, v)?;
533            }
534            write_close(writer, name)?;
535        }
536        CanonicalValue::String(s) => {
537            write_open(writer, name, &[])?;
538            writer.write_event(Event::Text(BytesText::new(s)))?;
539            write_close(writer, name)?;
540        }
541        CanonicalValue::Number(n) => {
542            write_open(writer, name, &[])?;
543            writer.write_event(Event::Text(BytesText::new(&n.to_string())))?;
544            write_close(writer, name)?;
545        }
546        CanonicalValue::Bool(b) => {
547            write_open(writer, name, &[])?;
548            writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
549            write_close(writer, name)?;
550        }
551        CanonicalValue::Null => {
552            write_open(writer, name, &[])?;
553            write_close(writer, name)?;
554        }
555    }
556    Ok(())
557}
558
559fn is_scalar(v: &CanonicalValue) -> bool {
560    matches!(
561        v,
562        CanonicalValue::Null
563            | CanonicalValue::Bool(_)
564            | CanonicalValue::Number(_)
565            | CanonicalValue::String(_)
566    )
567}
568
569fn find_first_object_array(v: &CanonicalValue) -> Option<&Vec<CanonicalValue>> {
570    match v {
571        CanonicalValue::Array(a)
572            if !a.is_empty() && a.iter().all(|x| matches!(x, CanonicalValue::Object(_))) =>
573        {
574            Some(a)
575        }
576        CanonicalValue::Object(o) => {
577            for (_, v) in o {
578                if let Some(arr) = find_first_object_array(v) {
579                    return Some(arr);
580                }
581            }
582            None
583        }
584        _ => None,
585    }
586}