Skip to main content

sheeter_engine/
lib.rs

1//! Sheeter is a data file conversion library and CLI tool.
2//!
3//! This crate provides functions to convert between CSV and JSON formats.
4//! It is designed to be small, fast, and portable, suitable for use on
5//! low-end devices such as Android phones. The core idea is simple: take
6//! data in one format, understand its structure, and emit it in another
7//! format without unnecessary overhead.
8//!
9//! # Examples
10//!
11//! Convert CSV to JSON:
12//!
13//! ```
14//! use sheeter_engine::csv_to_json;
15//!
16//! let csv = "name,age\nAlice,30\nBob,25";
17//! let json = csv_to_json(csv).unwrap();
18//! assert_eq!(json, r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#);
19//! ```
20//!
21//! Convert JSON to CSV:
22//!
23//! ```
24//! use sheeter_engine::json_to_csv;
25//!
26//! let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
27//! let csv = json_to_csv(json).unwrap();
28//! assert_eq!(csv, "name,age\nAlice,30\nBob,25\n");
29//! ```
30
31pub mod canonical;
32pub mod detect;
33pub mod filter;
34pub mod inspect;
35pub mod ndjson;
36pub mod query;
37pub mod sort;
38pub mod transform;
39pub mod view;
40pub mod xlsx_tools;
41
42pub use canonical::{to_ods_workbook, CanonicalValue};
43pub use detect::{detect_content, detect_extension};
44pub use filter::Filter;
45pub use inspect::inspect;
46pub use ndjson::{stream_delimited_to_ndjson, stream_ndjson_to_delimited, stream_ndjson_to_ndjson};
47pub use sort::{external_sort_delimited, SortSpec};
48pub use transform::Transforms;
49
50use std::io::Cursor;
51
52/// Converts a delimited string into a JSON array of objects.
53///
54/// This is the shared engine behind `csv_to_json` and `tsv_to_json`. The first
55/// line is read as the header row; every subsequent line becomes a JSON object
56/// mapping header names to string cell values. Duplicate header names are
57/// rejected because they would silently overwrite data.
58///
59/// # Errors
60///
61/// Returns an error on malformed input, duplicate headers, or JSON
62/// serialization failure.
63///
64/// # Examples
65///
66/// ```
67/// use sheeter_engine::csv_to_json;
68///
69/// let csv = "name,age\nAlice,30";
70/// assert_eq!(
71///     csv_to_json(csv).unwrap(),
72///     r#"[{"name":"Alice","age":"30"}]"#
73/// );
74/// ```
75fn delimited_to_json(input: &str, delim: u8) -> Result<String, Box<dyn std::error::Error>> {
76    let input = strip_bom(input);
77    let mut rdr = csv::ReaderBuilder::new()
78        .delimiter(delim)
79        .from_reader(Cursor::new(input));
80    let headers = rdr.headers()?.clone();
81    let mut seen = std::collections::HashSet::new();
82    for header in headers.iter() {
83        if !seen.insert(header) {
84            return Err(format!("duplicate header: {header}").into());
85        }
86    }
87    let mut records = Vec::new();
88    for result in rdr.records() {
89        let record = result?;
90        let mut map = serde_json::Map::new();
91        for (header, value) in headers.iter().zip(record.iter()) {
92            map.insert(
93                header.to_string(),
94                serde_json::Value::String(value.to_string()),
95            );
96        }
97        records.push(serde_json::Value::Object(map));
98    }
99    Ok(serde_json::to_string(&records)?)
100}
101
102/// Converts a JSON array of objects into a delimited string.
103///
104/// This is the shared engine behind `json_to_csv` and `json_to_tsv`. The header
105/// row is the union of all keys across all objects, in first-appearance order.
106/// Missing keys become empty cells. Strings are written as-is; numbers and
107/// booleans use their JSON literal forms; null becomes an empty cell. Nested
108/// objects and arrays cannot be represented losslessly in flat output and cause
109/// an error.
110///
111/// # Errors
112///
113/// Returns an error if the JSON is not an array of objects, contains nested
114/// values, or if writing fails.
115///
116/// # Examples
117///
118/// ```
119/// use sheeter_engine::json_to_csv;
120///
121/// let json = r#"[{"a":1},{"a":2,"b":3}]"#;
122/// assert_eq!(json_to_csv(json).unwrap(), "a,b\n1,\n2,3\n");
123/// ```
124fn json_to_delimited(input: &str, delim: u8) -> Result<String, Box<dyn std::error::Error>> {
125    let values: Vec<serde_json::Value> = serde_json::from_str(input)?;
126    if values.is_empty() {
127        return Ok(String::new());
128    }
129    let mut headers: Vec<String> = Vec::new();
130    for value in &values {
131        let obj = value.as_object().ok_or("JSON array must contain objects")?;
132        for key in obj.keys() {
133            if !headers.contains(key) {
134                headers.push(key.clone());
135            }
136        }
137    }
138    let mut wtr = csv::WriterBuilder::new()
139        .delimiter(delim)
140        .from_writer(vec![]);
141    wtr.write_record(&headers)?;
142    for value in values {
143        let obj = value.as_object().ok_or("JSON array must contain objects")?;
144        let mut record: Vec<String> = Vec::with_capacity(headers.len());
145        for h in &headers {
146            let cell = match obj.get(h) {
147                Some(serde_json::Value::String(s)) => s.clone(),
148                Some(serde_json::Value::Number(n)) => n.to_string(),
149                Some(serde_json::Value::Bool(b)) => b.to_string(),
150                Some(serde_json::Value::Null) => String::new(),
151                Some(serde_json::Value::Object(_)) | Some(serde_json::Value::Array(_)) => {
152                    return Err(format!(
153                        "nested value at key {h} cannot be represented in flat output"
154                    )
155                    .into());
156                }
157                None => String::new(),
158            };
159            record.push(cell);
160        }
161        wtr.write_record(&record)?;
162    }
163    let data = wtr.into_inner()?;
164    Ok(String::from_utf8(data)?)
165}
166
167/// Converts a CSV string into a JSON array of objects.
168///
169/// CSV (Comma-Separated Values) is a simple tabular format. This function reads
170/// the first row as the header and each subsequent row as a record, producing a
171/// JSON array of objects. All values are emitted as JSON strings.
172///
173/// # Errors
174///
175/// Returns an error on malformed CSV, duplicate headers, or JSON failure.
176///
177/// # Examples
178///
179/// ```
180/// use sheeter_engine::csv_to_json;
181///
182/// let csv = "name,age\nAlice,30\nBob,25";
183/// let json = csv_to_json(csv).unwrap();
184/// assert_eq!(json, r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#);
185/// ```
186pub fn csv_to_json(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
187    delimited_to_json(csv, b',')
188}
189
190/// Converts a TSV string into a JSON array of objects.
191///
192/// TSV (Tab-Separated Values) is the same tabular model as CSV but uses a tab
193/// character as the field separator. This is useful for data exported from
194/// spreadsheet tools that prefer tabs over commas.
195///
196/// # Errors
197///
198/// Returns an error on malformed TSV, duplicate headers, or JSON failure.
199///
200/// # Examples
201///
202/// ```
203/// use sheeter_engine::tsv_to_json;
204///
205/// let tsv = "name\tage\nAlice\t30";
206/// let json = tsv_to_json(tsv).unwrap();
207/// assert_eq!(json, r#"[{"name":"Alice","age":"30"}]"#);
208/// ```
209pub fn tsv_to_json(tsv: &str) -> Result<String, Box<dyn std::error::Error>> {
210    delimited_to_json(tsv, b'\t')
211}
212
213/// Converts a JSON array of objects into a CSV string.
214///
215/// The header row is the union of all keys across all objects. Missing keys
216/// become empty cells. Nested objects or arrays cause an error to prevent
217/// silent corruption of round-trips.
218///
219/// # Errors
220///
221/// Returns an error if the input is not an array of objects, contains nested
222/// values, or if writing fails.
223///
224/// # Examples
225///
226/// ```
227/// use sheeter_engine::json_to_csv;
228///
229/// let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
230/// assert_eq!(json_to_csv(json).unwrap(), "name,age\nAlice,30\nBob,25\n");
231/// ```
232pub fn json_to_csv(json: &str) -> Result<String, Box<dyn std::error::Error>> {
233    json_to_delimited(json, b',')
234}
235
236/// Converts a JSON array of objects into a TSV string.
237///
238/// Same semantics as `json_to_csv`, but fields are separated by a tab
239/// character. The output uses `\n` line endings.
240///
241/// # Errors
242///
243/// Returns an error if the input is not an array of objects, contains nested
244/// values, or if writing fails.
245///
246/// # Examples
247///
248/// ```
249/// use sheeter_engine::json_to_tsv;
250///
251/// let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
252/// assert_eq!(json_to_tsv(json).unwrap(), "name\tage\nAlice\t30\nBob\t25\n");
253/// ```
254pub fn json_to_tsv(json: &str) -> Result<String, Box<dyn std::error::Error>> {
255    json_to_delimited(json, b'\t')
256}
257
258/// Removes the UTF-8 BOM (`\u{feff}`) from the start of a string if present.
259///
260/// Some tools (notably Excel) prepend a BOM to CSV/TSV exports. Without
261/// stripping, the first header name would include the invisible BOM character
262/// and any downstream lookup by name would silently fail. This explicit
263/// preprocessing guarantees clean headers regardless of parser behavior.
264fn strip_bom(input: &str) -> &str {
265    input.strip_prefix('\u{feff}').unwrap_or(input)
266}
267
268/// Converts a YAML sequence of mappings into a JSON array of objects.
269///
270/// Non-string YAML keys (e.g. `1:`) are converted to their string form
271/// (`"1"`), matching the convention of every mainstream YAML-to-JSON
272/// converter. Collisions are not detected; if the source YAML contains both
273/// `1:` and `"1":`, one value wins.
274///
275/// This function parses YAML with a strict schema to avoid the "Norway problem":
276/// `yes`, `no`, `on`, `off`, `y`, `n` are treated as strings under YAML 1.2
277/// semantics. Nested sequences or mappings cause an error because they cannot
278/// round-trip losslessly through flat CSV/TSV.
279///
280/// # Errors
281///
282/// Returns an error if the YAML is not a sequence of mappings, contains nested
283/// values, or if JSON serialization fails.
284///
285/// # Examples
286///
287/// ```
288/// use sheeter_engine::yaml_to_json;
289///
290/// let yaml = "- name: Alice\n  age: '30'\n- name: Bob\n  age: '25'\n";
291/// let json = yaml_to_json(yaml).unwrap();
292/// assert_eq!(json, r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#);
293/// ```
294pub fn yaml_to_json(yaml: &str) -> Result<String, Box<dyn std::error::Error>> {
295    let opts = serde_saphyr::options! {
296        strict_booleans: true,
297        ignore_binary_tag_for_string: true,
298    };
299    let values: Vec<serde_json::Value> = serde_saphyr::from_str_with_options(yaml, opts)?;
300    for value in &values {
301        let obj = value
302            .as_object()
303            .ok_or("YAML sequence must contain mappings")?;
304        for (key, val) in obj {
305            if val.is_object() || val.is_array() {
306                return Err(format!(
307                    "nested YAML value at key {key} cannot be represented in flat output"
308                )
309                .into());
310            }
311        }
312    }
313    Ok(serde_json::to_string(&values)?)
314}
315
316/// Converts a JSON array of objects into a YAML sequence of mappings.
317///
318/// This is the inverse of `yaml_to_json`. The output is a YAML 1.2 document
319/// where each object becomes a mapping in a sequence. Key order is preserved.
320/// Nested objects or arrays cause an error for the same reason as CSV: they
321/// cannot round-trip losslessly through a flat format.
322///
323/// # Errors
324///
325/// Returns an error if the JSON is not an array of objects, contains nested
326/// values, or if YAML serialization fails.
327///
328/// # Examples
329///
330/// ```
331/// use sheeter_engine::json_to_yaml;
332///
333/// let json = r#"[{"name":"Alice","age":"30"}]"#;
334/// let yaml = json_to_yaml(json).unwrap();
335/// assert!(yaml.contains("name: Alice"));
336/// ```
337pub fn json_to_yaml(json: &str) -> Result<String, Box<dyn std::error::Error>> {
338    let values: Vec<serde_json::Value> = serde_json::from_str(json)?;
339    for value in &values {
340        let obj = value.as_object().ok_or("JSON array must contain objects")?;
341        for (key, val) in obj {
342            if val.is_object() || val.is_array() {
343                return Err(format!(
344                    "nested JSON value at key {key} cannot be represented in flat YAML"
345                )
346                .into());
347            }
348        }
349    }
350    Ok(serde_saphyr::to_string(&values)?)
351}
352
353/// Converts CSV to YAML by composing `csv_to_json` and `json_to_yaml`.
354///
355/// # Examples
356///
357/// ```
358/// use sheeter_engine::csv_to_yaml;
359///
360/// let csv = "name,age\nAlice,30";
361/// let yaml = csv_to_yaml(csv).unwrap();
362/// assert!(yaml.contains("name: Alice"));
363/// ```
364pub fn csv_to_yaml(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
365    json_to_yaml(&csv_to_json(csv)?)
366}
367
368/// Converts TSV to YAML by composing `tsv_to_json` and `json_to_yaml`.
369///
370/// # Examples
371///
372/// ```
373/// use sheeter_engine::tsv_to_yaml;
374///
375/// let tsv = "name\tage\nAlice\t30";
376/// let yaml = tsv_to_yaml(tsv).unwrap();
377/// assert!(yaml.contains("name: Alice"));
378/// ```
379pub fn tsv_to_yaml(tsv: &str) -> Result<String, Box<dyn std::error::Error>> {
380    json_to_yaml(&tsv_to_json(tsv)?)
381}
382
383/// Converts YAML to CSV by composing `yaml_to_json` and `json_to_csv`.
384///
385/// # Examples
386///
387/// ```
388/// use sheeter_engine::yaml_to_csv;
389///
390/// let yaml = "- name: Alice\n  age: '30'\n";
391/// let csv = yaml_to_csv(yaml).unwrap();
392/// assert_eq!(csv, "name,age\nAlice,30\n");
393/// ```
394pub fn yaml_to_csv(yaml: &str) -> Result<String, Box<dyn std::error::Error>> {
395    json_to_csv(&yaml_to_json(yaml)?)
396}
397
398/// Converts YAML to TSV by composing `yaml_to_json` and `json_to_tsv`.
399///
400/// # Examples
401///
402/// ```
403/// use sheeter_engine::yaml_to_tsv;
404///
405/// let yaml = "- name: Alice\n  age: '30'\n";
406/// let tsv = yaml_to_tsv(yaml).unwrap();
407/// assert_eq!(tsv, "name\tage\nAlice\t30\n");
408/// ```
409pub fn yaml_to_tsv(yaml: &str) -> Result<String, Box<dyn std::error::Error>> {
410    json_to_tsv(&yaml_to_json(yaml)?)
411}
412
413/// Converts CSV to TSV by composing `csv_to_json` and `json_to_tsv`.
414///
415/// # Examples
416///
417/// ```
418/// use sheeter_engine::csv_to_tsv;
419///
420/// let csv = "name,age\nAlice,30";
421/// let tsv = csv_to_tsv(csv).unwrap();
422/// assert_eq!(tsv, "name\tage\nAlice\t30\n");
423/// ```
424pub fn csv_to_tsv(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
425    json_to_tsv(&csv_to_json(csv)?)
426}
427
428/// Converts TSV to CSV by composing `tsv_to_json` and `json_to_csv`.
429///
430/// # Examples
431///
432/// ```
433/// use sheeter_engine::tsv_to_csv;
434///
435/// let tsv = "name\tage\nAlice\t30";
436/// let csv = tsv_to_csv(tsv).unwrap();
437/// assert_eq!(csv, "name,age\nAlice,30\n");
438/// ```
439pub fn tsv_to_csv(tsv: &str) -> Result<String, Box<dyn std::error::Error>> {
440    json_to_csv(&tsv_to_json(tsv)?)
441}
442
443/// Converts an XML document to a JSON representation.
444///
445/// Attributes become `@name` keys, text becomes `#text`, and repeated
446/// sibling elements become arrays. The output has exactly one top-level key:
447/// the root element name.
448///
449/// # Errors
450/// Returns an error if the XML is malformed or empty.
451///
452/// # Examples
453/// ```
454/// use sheeter_engine::xml_to_json;
455/// let json = xml_to_json("<r><a>1</a><a>2</a></r>").unwrap();
456/// assert_eq!(json, r#"{"r":{"a":["1","2"]}}"#);
457/// ```
458pub fn xml_to_json(xml: &str) -> Result<String, Box<dyn std::error::Error>> {
459    CanonicalValue::from_xml(xml)?.to_json()
460}
461
462/// Converts a JSON value to XML.
463///
464/// Tabular JSON (array of objects) is wrapped in `<root><item>...`. Other
465/// JSON shapes are wrapped in `<root>` unless they are a single-key object,
466/// which becomes the root element directly. Keys starting with `@` become
467/// attributes and `#text` becomes text content.
468///
469/// # Errors
470/// Returns an error if the JSON is malformed.
471///
472/// # Examples
473/// ```
474/// use sheeter_engine::json_to_xml;
475/// let xml = json_to_xml(r#"[{"a":"1"}]"#).unwrap();
476/// assert!(xml.contains("<root>"));
477/// ```
478pub fn json_to_xml(json: &str) -> Result<String, Box<dyn std::error::Error>> {
479    CanonicalValue::from_json(json)?.to_xml()
480}
481
482/// Converts CSV to XML via the canonical hub.
483///
484/// # Examples
485/// ```
486/// use sheeter_engine::csv_to_xml;
487/// let xml = csv_to_xml("a\n1").unwrap();
488/// assert!(xml.contains("<item>"));
489/// ```
490pub fn csv_to_xml(input: &str) -> Result<String, Box<dyn std::error::Error>> {
491    CanonicalValue::from_csv(input)?.to_xml()
492}
493
494/// Converts TSV to XML via the canonical hub.
495///
496/// # Examples
497/// ```
498/// use sheeter_engine::tsv_to_xml;
499/// let xml = tsv_to_xml("a\tb\n1\t2").unwrap();
500/// assert!(xml.contains("<item>"));
501/// ```
502pub fn tsv_to_xml(input: &str) -> Result<String, Box<dyn std::error::Error>> {
503    CanonicalValue::from_tsv(input)?.to_xml()
504}
505
506/// Converts YAML to XML via the canonical hub.
507///
508/// # Examples
509/// ```
510/// use sheeter_engine::yaml_to_xml;
511/// let xml = yaml_to_xml("- a: 1\n").unwrap();
512/// assert!(xml.contains("<item>"));
513/// ```
514pub fn yaml_to_xml(input: &str) -> Result<String, Box<dyn std::error::Error>> {
515    CanonicalValue::from_yaml(input)?.to_xml()
516}
517
518/// Converts XML to CSV, extracting the tabular array if present.
519///
520/// Expects the XML to follow `<root><item><col>value</col></item></root>`
521/// (or any single-level root whose single child key holds the rows). Errors
522/// if the structure is not tabular.
523///
524/// # Errors
525/// Returns an error if the XML is malformed or not tabular.
526///
527/// # Examples
528/// ```
529/// use sheeter_engine::xml_to_csv;
530/// let csv = xml_to_csv("<root><item><a>1</a></item></root>").unwrap();
531/// assert_eq!(csv, "a\n1\n");
532/// ```
533pub fn xml_to_csv(xml: &str) -> Result<String, Box<dyn std::error::Error>> {
534    CanonicalValue::from_xml(xml)?.to_csv()
535}
536
537/// Converts XML to TSV, extracting the tabular array if present.
538///
539/// # Examples
540/// ```
541/// use sheeter_engine::xml_to_tsv;
542/// let tsv = xml_to_tsv("<root><item><a>1</a></item></root>").unwrap();
543/// assert_eq!(tsv, "a\n1\n");
544/// ```
545pub fn xml_to_tsv(xml: &str) -> Result<String, Box<dyn std::error::Error>> {
546    CanonicalValue::from_xml(xml)?.to_tsv()
547}
548
549/// Converts XML to YAML, extracting the tabular array if present.
550///
551/// # Examples
552/// ```
553/// use sheeter_engine::xml_to_yaml;
554/// let yaml = xml_to_yaml("<root><item><a>1</a></item></root>").unwrap();
555/// assert!(yaml.contains("a:"));
556/// ```
557pub fn xml_to_yaml(xml: &str) -> Result<String, Box<dyn std::error::Error>> {
558    CanonicalValue::from_xml(xml)?.to_yaml()
559}
560
561/// Converts a TOML document to a JSON value.
562///
563/// TOML datetimes are stringified (`2024-01-15T10:30:00Z` becomes the string
564/// `"2024-01-15T10:30:00Z"`), matching the behavior of YAML. Nested tables
565/// and arrays of tables map directly to JSON objects and arrays.
566///
567/// # Errors
568/// Returns an error if the TOML is malformed.
569///
570/// # Examples
571/// ```
572/// use sheeter_engine::toml_to_json;
573/// let json = toml_to_json("a = 1\n").unwrap();
574/// assert_eq!(json, r#"{"a":1}"#);
575/// ```
576pub fn toml_to_json(toml: &str) -> Result<String, Box<dyn std::error::Error>> {
577    CanonicalValue::from_toml(toml)?.to_json()
578}
579
580/// Converts a TOML document to CSV, extracting the first array of tables.
581///
582/// Flat TOML documents (all scalar values) become a single-row CSV. Documents
583/// with an array of tables at any depth use that array as the row source.
584///
585/// # Errors
586/// Returns an error if the TOML is malformed or has no tabular structure.
587///
588/// # Examples
589/// ```
590/// use sheeter_engine::toml_to_csv;
591/// assert_eq!(toml_to_csv("a = 1\n").unwrap(), "a\n1\n");
592/// ```
593pub fn toml_to_csv(toml: &str) -> Result<String, Box<dyn std::error::Error>> {
594    CanonicalValue::from_toml(toml)?.to_csv()
595}
596
597/// Converts a TOML document to TSV.
598///
599/// # Errors
600/// Returns an error if the TOML is malformed or has no tabular structure.
601///
602/// # Examples
603/// ```
604/// use sheeter_engine::toml_to_tsv;
605/// assert_eq!(toml_to_tsv("a = 1\n").unwrap(), "a\n1\n");
606/// ```
607pub fn toml_to_tsv(toml: &str) -> Result<String, Box<dyn std::error::Error>> {
608    CanonicalValue::from_toml(toml)?.to_tsv()
609}
610
611/// Converts a TOML document to YAML.
612///
613/// # Errors
614/// Returns an error if the TOML is malformed.
615///
616/// # Examples
617/// ```
618/// use sheeter_engine::toml_to_yaml;
619/// let yaml = toml_to_yaml("a = 1\n").unwrap();
620/// assert!(yaml.contains("a:"));
621/// ```
622pub fn toml_to_yaml(toml: &str) -> Result<String, Box<dyn std::error::Error>> {
623    CanonicalValue::from_toml(toml)?.to_yaml()
624}
625
626/// Converts a TOML document to XML.
627///
628/// # Errors
629/// Returns an error if the TOML is malformed.
630///
631/// # Examples
632/// ```
633/// use sheeter_engine::toml_to_xml;
634/// let xml = toml_to_xml("a = 1\n").unwrap();
635/// assert!(xml.contains("<a>"));
636/// ```
637pub fn toml_to_xml(toml: &str) -> Result<String, Box<dyn std::error::Error>> {
638    CanonicalValue::from_toml(toml)?.to_xml()
639}
640
641/// Converts a JSON value to TOML.
642///
643/// JSON arrays at the root are wrapped in `{item: [...]}` because TOML
644/// requires a table at the document root. Null values are skipped, since
645/// TOML has no null type; JSON arrays containing null error out.
646///
647/// # Errors
648/// Returns an error if the JSON is malformed or contains values TOML cannot
649/// represent (null inside arrays, integers outside i64 range).
650///
651/// # Examples
652/// ```
653/// use sheeter_engine::json_to_toml;
654/// let toml = json_to_toml(r#"{"a":1}"#).unwrap();
655/// assert!(toml.contains("a = 1"));
656/// ```
657pub fn json_to_toml(json: &str) -> Result<String, Box<dyn std::error::Error>> {
658    CanonicalValue::from_json(json)?.to_toml()
659}
660
661/// Converts CSV to TOML via the canonical hub.
662///
663/// The resulting TOML document is `[[item]]` array-of-tables, one item per
664/// CSV row.
665///
666/// # Errors
667/// Returns an error if the CSV is malformed.
668///
669/// # Examples
670/// ```
671/// use sheeter_engine::csv_to_toml;
672/// let toml = csv_to_toml("a\n1\n").unwrap();
673/// assert!(toml.contains("[[item]]"));
674/// ```
675pub fn csv_to_toml(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
676    CanonicalValue::from_csv(csv)?.to_toml()
677}
678
679/// Converts TSV to TOML via the canonical hub.
680///
681/// # Errors
682/// Returns an error if the TSV is malformed.
683///
684/// # Examples
685/// ```
686/// use sheeter_engine::tsv_to_toml;
687/// let toml = tsv_to_toml("a\tb\n1\t2\n").unwrap();
688/// assert!(toml.contains("[[item]]"));
689/// ```
690pub fn tsv_to_toml(tsv: &str) -> Result<String, Box<dyn std::error::Error>> {
691    CanonicalValue::from_tsv(tsv)?.to_toml()
692}
693
694/// Converts YAML to TOML via the canonical hub.
695///
696/// # Errors
697/// Returns an error if the YAML is malformed or cannot be represented in TOML.
698///
699/// # Examples
700/// ```
701/// use sheeter_engine::yaml_to_toml;
702/// let toml = yaml_to_toml("- a: 1\n").unwrap();
703/// assert!(toml.contains("[[item]]"));
704/// ```
705pub fn yaml_to_toml(yaml: &str) -> Result<String, Box<dyn std::error::Error>> {
706    CanonicalValue::from_yaml(yaml)?.to_toml()
707}
708
709/// Converts XML to TOML via the canonical hub.
710///
711/// Attribute markers (`@name`) and text markers (`#text`) are preserved as
712/// quoted TOML keys, allowing round-trip back to XML.
713///
714/// # Errors
715/// Returns an error if the XML is malformed or cannot be represented in TOML.
716///
717/// # Examples
718/// ```
719/// use sheeter_engine::xml_to_toml;
720/// let toml = xml_to_toml("<r><a>1</a></r>").unwrap();
721/// assert!(toml.contains("a = \"1\""));
722/// ```
723pub fn xml_to_toml(xml: &str) -> Result<String, Box<dyn std::error::Error>> {
724    CanonicalValue::from_xml(xml)?.to_toml()
725}
726
727/// Supported data formats.
728///
729/// Each variant corresponds to a reader/writer pair on
730/// [`CanonicalValue`](crate::CanonicalValue).
731#[derive(Debug, Clone, Copy, PartialEq, Eq)]
732#[non_exhaustive]
733pub enum Format {
734    Csv,
735    Tsv,
736    Json,
737    Yaml,
738    Xml,
739    Toml,
740    Xlsx,
741    Sql,
742    Ndjson,
743    Ods,
744}
745
746/// SQL dialect used when parsing input. The writer emits ANSI-compatible
747/// `INSERT` statements that SQLite, PostgreSQL, and MySQL all accept.
748#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
749#[non_exhaustive]
750pub enum SqlDialect {
751    #[default]
752    Generic,
753    PostgreSql,
754    Sqlite,
755    MySql,
756}
757
758impl std::str::FromStr for SqlDialect {
759    type Err = String;
760    fn from_str(s: &str) -> Result<Self, Self::Err> {
761        match s.to_ascii_lowercase().as_str() {
762            "generic" | "ansi" => Ok(Self::Generic),
763            "postgres" | "postgresql" | "pg" => Ok(Self::PostgreSql),
764            "sqlite" => Ok(Self::Sqlite),
765            "mysql" | "mariadb" => Ok(Self::MySql),
766            other => Err(format!(
767                "unknown SQL dialect {other:?}; expected generic, postgresql, sqlite, or mysql"
768            )),
769        }
770    }
771}
772
773/// One output document: `(sheet_name, bytes)`. Returned by
774/// [`convert_workbook`] because multi-sheet inputs can produce more than one
775/// output.
776pub type OutputDoc = (String, Vec<u8>);
777
778impl Format {
779    /// Every format the Sheeter engine can read and write.
780    pub const ALL: [Format; 10] = [
781        Format::Csv,
782        Format::Tsv,
783        Format::Json,
784        Format::Yaml,
785        Format::Xml,
786        Format::Toml,
787        Format::Xlsx,
788        Format::Sql,
789        Format::Ndjson,
790        Format::Ods,
791    ];
792}
793
794/// Converts `input` from one format to another via the canonical hub.
795///
796/// If `from` equals `to`, the input is returned unchanged. That preserves
797/// BOM markers, formatting, and other byte-level details that a round-trip
798/// through the hub would normalize away.
799///
800/// # Errors
801///
802/// Returns an error if the input cannot be parsed as `from` or serialized
803/// as `to`, or if the document cannot be represented in the target format
804/// (for example, nested values when converting to CSV).
805///
806/// # Examples
807///
808/// ```
809/// use sheeter_engine::{convert, Format};
810///
811/// let json = convert(Format::Csv, Format::Json, "a\n1\n").unwrap();
812/// assert_eq!(json, r#"[{"a":"1"}]"#);
813/// ```
814pub fn convert(
815    from: Format,
816    to: Format,
817    input: &str,
818) -> Result<String, Box<dyn std::error::Error>> {
819    if from == to {
820        return Ok(input.to_string());
821    }
822    if from == Format::Xlsx || to == Format::Xlsx {
823        return Err("XLSX is binary; use convert_workbook instead".into());
824    }
825    if from == Format::Ods || to == Format::Ods {
826        return Err("ODS is binary; use convert_workbook instead".into());
827    }
828    let hub = match from {
829        Format::Csv => CanonicalValue::from_csv(input)?,
830        Format::Tsv => CanonicalValue::from_tsv(input)?,
831        Format::Json => CanonicalValue::from_json(input)?,
832        Format::Yaml => CanonicalValue::from_yaml(input)?,
833        Format::Xml => CanonicalValue::from_xml(input)?,
834        Format::Toml => CanonicalValue::from_toml(input)?,
835        Format::Sql => {
836            let mut sheets = CanonicalValue::from_sql_workbook(input, SqlDialect::Generic)?;
837            if sheets.len() != 1 {
838                return Err("multi-table SQL requires convert_workbook".into());
839            }
840            sheets.remove(0).1
841        }
842        Format::Ndjson => CanonicalValue::from_ndjson(input)?,
843        Format::Ods => unreachable!(),
844        Format::Xlsx => unreachable!(),
845    };
846    match to {
847        Format::Csv => hub.to_csv(),
848        Format::Tsv => hub.to_tsv(),
849        Format::Json => hub.to_json(),
850        Format::Yaml => hub.to_yaml(),
851        Format::Xml => hub.to_xml(),
852        Format::Toml => hub.to_toml(),
853        Format::Sql => hub.to_sql_with_table("data"),
854        Format::Ndjson => hub.to_ndjson(),
855
856        Format::Ods => unreachable!(),
857        Format::Xlsx => unreachable!(),
858    }
859}
860
861/// Converts a byte buffer between formats, supporting multi-sheet workbooks.
862///
863/// Returns one entry per output document. For XLSX inputs with N sheets and
864/// CSV/TSV outputs, the result has N entries. For JSON/YAML/XML/TOML/XLSX
865/// outputs, the result has exactly one entry containing all sheets merged
866/// into a single document.
867///
868/// The returned string is the sheet name (from XLSX) or `"Sheet1"` for
869/// single-document inputs. Callers decide how to map these names to file
870/// paths.
871///
872/// # Errors
873///
874/// Returns an error if the input cannot be parsed or the output cannot be
875/// represented in the target format.
876///
877/// # Examples
878///
879/// ```
880/// use sheeter_engine::{convert_workbook, Format};
881///
882/// let out = convert_workbook(Format::Csv, Format::Json, b"a\n1\n").unwrap();
883/// assert_eq!(out.len(), 1);
884/// assert_eq!(out[0].0, "Sheet1");
885/// ```
886pub fn convert_workbook(
887    from: Format,
888    to: Format,
889    input: &[u8],
890) -> Result<Vec<OutputDoc>, Box<dyn std::error::Error>> {
891    convert_workbook_with_options(from, to, input, SqlDialect::Generic, None)
892}
893
894/// Like [`convert_workbook`], but lets the caller pick the SQL dialect and
895/// the table name used when writing SQL output.
896pub fn convert_workbook_with_options(
897    from: Format,
898    to: Format,
899    input: &[u8],
900    dialect: SqlDialect,
901    table_name: Option<&str>,
902) -> Result<Vec<OutputDoc>, Box<dyn std::error::Error>> {
903    if from == to && from != Format::Xlsx {
904        let text = std::str::from_utf8(input)?;
905        return Ok(vec![("Sheet1".to_string(), text.as_bytes().to_vec())]);
906    }
907
908    let sheets: Vec<(String, CanonicalValue)> = match from {
909        Format::Xlsx => CanonicalValue::from_xlsx_workbook(input)?,
910        Format::Ods => {
911            let v = CanonicalValue::from_ods(input)?;
912            match v {
913                CanonicalValue::Object(o) => o,
914                other => vec![("Sheet1".to_string(), other)],
915            }
916        }
917        Format::Sql => {
918            let text = std::str::from_utf8(input)?;
919            CanonicalValue::from_sql_workbook(text, dialect)?
920        }
921        Format::Ndjson => {
922            let text = std::str::from_utf8(input)?;
923            vec![("Sheet1".to_string(), CanonicalValue::from_ndjson(text)?)]
924        }
925        _ => {
926            let text = std::str::from_utf8(input)?;
927            let hub = match from {
928                Format::Csv => CanonicalValue::from_csv(text)?,
929                Format::Tsv => CanonicalValue::from_tsv(text)?,
930                Format::Json => CanonicalValue::from_json(text)?,
931                Format::Yaml => CanonicalValue::from_yaml(text)?,
932                Format::Xml => CanonicalValue::from_xml(text)?,
933                Format::Toml => CanonicalValue::from_toml(text)?,
934                Format::Sql | Format::Ndjson | Format::Xlsx | Format::Ods => unreachable!(),
935            };
936            vec![("Sheet1".to_string(), hub)]
937        }
938    };
939
940    if to == Format::Sql {
941        let text = if sheets.len() == 1 {
942            let table = table_name.unwrap_or("data");
943            sheets[0].1.to_sql_with_table(table)?
944        } else {
945            CanonicalValue::to_sql_workbook(&sheets)?
946        };
947        return Ok(vec![("Sheet1".to_string(), text.into_bytes())]);
948    }
949
950    if to == Format::Ods {
951        let bytes = to_ods_workbook(&sheets)?;
952        return Ok(vec![("Sheet1".to_string(), bytes)]);
953    }
954
955    if to == Format::Ndjson {
956        let text = if sheets.len() == 1 {
957            sheets[0].1.to_ndjson()?
958        } else {
959            let merged = CanonicalValue::Object(sheets);
960            merged.to_ndjson()?
961        };
962        return Ok(vec![("Sheet1".to_string(), text.into_bytes())]);
963    }
964
965    if to == Format::Xlsx {
966        let bytes = CanonicalValue::to_xlsx_workbook(&sheets)?;
967        return Ok(vec![("Sheet1".to_string(), bytes)]);
968    }
969
970    if sheets.len() == 1 {
971        let text = write_single(to, &sheets[0].1)?;
972        return Ok(vec![(sheets[0].0.clone(), text.into_bytes())]);
973    }
974
975    match to {
976        Format::Json | Format::Yaml | Format::Xml | Format::Toml => {
977            let merged = CanonicalValue::Object(sheets);
978            Ok(vec![(
979                "Sheet1".to_string(),
980                write_single(to, &merged)?.into_bytes(),
981            )])
982        }
983        Format::Csv | Format::Tsv => {
984            let mut out = Vec::with_capacity(sheets.len());
985            for (name, value) in &sheets {
986                out.push((name.clone(), write_single(to, value)?.into_bytes()));
987            }
988            Ok(out)
989        }
990        Format::Sql => unreachable!(),
991        Format::Ndjson => unreachable!(),
992        Format::Ods => unreachable!(),
993        Format::Xlsx => unreachable!(),
994    }
995}
996
997fn write_single(
998    format: Format,
999    value: &CanonicalValue,
1000) -> Result<String, Box<dyn std::error::Error>> {
1001    match format {
1002        Format::Csv => value.to_csv(),
1003        Format::Tsv => value.to_tsv(),
1004        Format::Json => value.to_json(),
1005        Format::Yaml => value.to_yaml(),
1006        Format::Xml => value.to_xml(),
1007        Format::Toml => value.to_toml(),
1008        Format::Sql => value.to_sql_with_table("data"),
1009        Format::Ndjson => value.to_ndjson(),
1010        Format::Ods => Err("ODS must go through to_ods_workbook".into()),
1011        Format::Xlsx => Err("XLSX must go through to_xlsx_workbook".into()),
1012    }
1013}
1014
1015/// Converts CSV or TSV input to CSV or TSV output using constant memory.
1016///
1017/// Reads row by row and writes row by row, so peak memory is bounded by the
1018/// largest single row, not by file size. Applies filter, select, rename, and
1019/// limit transforms on the fly. Sorting and other buffering transforms are
1020/// not supported here.
1021///
1022/// # Errors
1023/// Returns an error if the input is malformed, if a transform fails, or if
1024/// the output writer fails.
1025///
1026/// # Examples
1027///
1028/// ```
1029/// use sheeter_engine::{stream_delimited, Transforms};
1030///
1031/// let input = b"a\n1\n2\n3\n";
1032/// let mut output = Vec::new();
1033/// stream_delimited(input.as_slice(), b',', &mut output, b',', &Transforms::default()).unwrap();
1034/// assert_eq!(output, b"a\n1\n2\n3\n");
1035/// ```
1036pub fn stream_delimited(
1037    reader: impl std::io::Read,
1038    from_delim: u8,
1039    writer: impl std::io::Write,
1040    to_delim: u8,
1041    transforms: &Transforms,
1042) -> Result<(), Box<dyn std::error::Error>> {
1043    let mut rdr = csv::ReaderBuilder::new()
1044        .delimiter(from_delim)
1045        .from_reader(reader);
1046    let raw_headers: Vec<String> = rdr.headers()?.iter().map(|s| s.to_string()).collect();
1047
1048    let mut renamed_headers = raw_headers.clone();
1049    for (from, to) in &transforms.rename {
1050        for h in renamed_headers.iter_mut() {
1051            if h == from {
1052                *h = to.clone();
1053                break;
1054            }
1055        }
1056    }
1057    let final_headers: Vec<String> = match &transforms.select {
1058        Some(sel) => sel.clone(),
1059        None => renamed_headers,
1060    };
1061
1062    let mut wtr = csv::WriterBuilder::new()
1063        .delimiter(to_delim)
1064        .from_writer(writer);
1065    wtr.write_record(&final_headers)?;
1066
1067    let limit = transforms.limit.unwrap_or(usize::MAX);
1068    let mut emitted = 0usize;
1069    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1070    let dedup_cols = transforms.dedup.as_ref().cloned();
1071    for result in rdr.records() {
1072        if emitted >= limit {
1073            break;
1074        }
1075        let record = result?;
1076        let mut row: Vec<(String, CanonicalValue)> = Vec::with_capacity(raw_headers.len());
1077        for (i, h) in raw_headers.iter().enumerate() {
1078            let v = record.get(i).unwrap_or("").to_string();
1079            row.push((h.clone(), CanonicalValue::String(v)));
1080        }
1081        if let Some(new_row) = transforms.apply_to_row(row) {
1082            if let Some(cols) = &dedup_cols {
1083                let key: String = if cols.is_empty() {
1084                    new_row
1085                        .iter()
1086                        .map(|(_, v)| match v {
1087                            CanonicalValue::String(s) => s.clone(),
1088                            _ => String::new(),
1089                        })
1090                        .collect::<Vec<_>>()
1091                        .join("\x1f")
1092                } else {
1093                    cols.iter()
1094                        .map(|c| {
1095                            new_row
1096                                .iter()
1097                                .find(|(k, _)| k == c)
1098                                .map(|(_, v)| match v {
1099                                    CanonicalValue::String(s) => s.clone(),
1100                                    _ => String::new(),
1101                                })
1102                                .unwrap_or_default()
1103                        })
1104                        .collect::<Vec<_>>()
1105                        .join("\x1f")
1106                };
1107                if !seen.insert(key) {
1108                    continue;
1109                }
1110            }
1111            let values: Vec<String> = new_row
1112                .iter()
1113                .map(|(_, v)| match v {
1114                    CanonicalValue::Null => String::new(),
1115                    CanonicalValue::String(s) => s.clone(),
1116                    CanonicalValue::Number(n) => n.to_string(),
1117                    CanonicalValue::Bool(b) => b.to_string(),
1118                    _ => String::new(),
1119                })
1120                .collect();
1121            wtr.write_record(&values)?;
1122            emitted += 1;
1123        }
1124    }
1125    wtr.flush()?;
1126    Ok(())
1127}