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
31use std::io::Cursor;
32
33/// Converts a delimited string into a JSON array of objects.
34///
35/// This is the shared engine behind `csv_to_json` and `tsv_to_json`. The first
36/// line is read as the header row; every subsequent line becomes a JSON object
37/// mapping header names to string cell values. Duplicate header names are
38/// rejected because they would silently overwrite data.
39///
40/// # Errors
41///
42/// Returns an error on malformed input, duplicate headers, or JSON
43/// serialization failure.
44///
45/// # Examples
46///
47/// ```
48/// use sheeter_engine::csv_to_json;
49///
50/// let csv = "name,age\nAlice,30";
51/// assert_eq!(
52///     csv_to_json(csv).unwrap(),
53///     r#"[{"name":"Alice","age":"30"}]"#
54/// );
55/// ```
56fn delimited_to_json(input: &str, delim: u8) -> Result<String, Box<dyn std::error::Error>> {
57    let mut rdr = csv::ReaderBuilder::new()
58        .delimiter(delim)
59        .from_reader(Cursor::new(input));
60    let headers = rdr.headers()?.clone();
61    let mut seen = std::collections::HashSet::new();
62    for header in headers.iter() {
63        if !seen.insert(header) {
64            return Err(format!("duplicate header: {header}").into());
65        }
66    }
67    let mut records = Vec::new();
68    for result in rdr.records() {
69        let record = result?;
70        let mut map = serde_json::Map::new();
71        for (header, value) in headers.iter().zip(record.iter()) {
72            map.insert(
73                header.to_string(),
74                serde_json::Value::String(value.to_string()),
75            );
76        }
77        records.push(serde_json::Value::Object(map));
78    }
79    Ok(serde_json::to_string(&records)?)
80}
81
82/// Converts a JSON array of objects into a delimited string.
83///
84/// This is the shared engine behind `json_to_csv` and `json_to_tsv`. The header
85/// row is the union of all keys across all objects, in first-appearance order.
86/// Missing keys become empty cells. Strings are written as-is; numbers and
87/// booleans use their JSON literal forms; null becomes an empty cell. Nested
88/// objects and arrays cannot be represented losslessly in flat output and cause
89/// an error.
90///
91/// # Errors
92///
93/// Returns an error if the JSON is not an array of objects, contains nested
94/// values, or if writing fails.
95///
96/// # Examples
97///
98/// ```
99/// use sheeter_engine::json_to_csv;
100///
101/// let json = r#"[{"a":1},{"a":2,"b":3}]"#;
102/// assert_eq!(json_to_csv(json).unwrap(), "a,b\n1,\n2,3\n");
103/// ```
104fn json_to_delimited(input: &str, delim: u8) -> Result<String, Box<dyn std::error::Error>> {
105    let values: Vec<serde_json::Value> = serde_json::from_str(input)?;
106    if values.is_empty() {
107        return Ok(String::new());
108    }
109    let mut headers: Vec<String> = Vec::new();
110    for value in &values {
111        let obj = value.as_object().ok_or("JSON array must contain objects")?;
112        for key in obj.keys() {
113            if !headers.contains(key) {
114                headers.push(key.clone());
115            }
116        }
117    }
118    let mut wtr = csv::WriterBuilder::new()
119        .delimiter(delim)
120        .from_writer(vec![]);
121    wtr.write_record(&headers)?;
122    for value in values {
123        let obj = value.as_object().ok_or("JSON array must contain objects")?;
124        let mut record: Vec<String> = Vec::with_capacity(headers.len());
125        for h in &headers {
126            let cell = match obj.get(h) {
127                Some(serde_json::Value::String(s)) => s.clone(),
128                Some(serde_json::Value::Number(n)) => n.to_string(),
129                Some(serde_json::Value::Bool(b)) => b.to_string(),
130                Some(serde_json::Value::Null) => String::new(),
131                Some(serde_json::Value::Object(_)) | Some(serde_json::Value::Array(_)) => {
132                    return Err(format!(
133                        "nested value at key {h} cannot be represented in flat output"
134                    )
135                    .into());
136                }
137                None => String::new(),
138            };
139            record.push(cell);
140        }
141        wtr.write_record(&record)?;
142    }
143    let data = wtr.into_inner()?;
144    Ok(String::from_utf8(data)?)
145}
146
147/// Converts a CSV string into a JSON array of objects.
148///
149/// CSV (Comma-Separated Values) is a simple tabular format. This function reads
150/// the first row as the header and each subsequent row as a record, producing a
151/// JSON array of objects. All values are emitted as JSON strings.
152///
153/// # Errors
154///
155/// Returns an error on malformed CSV, duplicate headers, or JSON failure.
156///
157/// # Examples
158///
159/// ```
160/// use sheeter_engine::csv_to_json;
161///
162/// let csv = "name,age\nAlice,30\nBob,25";
163/// let json = csv_to_json(csv).unwrap();
164/// assert_eq!(json, r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#);
165/// ```
166pub fn csv_to_json(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
167    delimited_to_json(csv, b',')
168}
169
170/// Converts a TSV string into a JSON array of objects.
171///
172/// TSV (Tab-Separated Values) is the same tabular model as CSV but uses a tab
173/// character as the field separator. This is useful for data exported from
174/// spreadsheet tools that prefer tabs over commas.
175///
176/// # Errors
177///
178/// Returns an error on malformed TSV, duplicate headers, or JSON failure.
179///
180/// # Examples
181///
182/// ```
183/// use sheeter_engine::tsv_to_json;
184///
185/// let tsv = "name\tage\nAlice\t30";
186/// let json = tsv_to_json(tsv).unwrap();
187/// assert_eq!(json, r#"[{"name":"Alice","age":"30"}]"#);
188/// ```
189pub fn tsv_to_json(tsv: &str) -> Result<String, Box<dyn std::error::Error>> {
190    delimited_to_json(tsv, b'\t')
191}
192
193/// Converts a JSON array of objects into a CSV string.
194///
195/// The header row is the union of all keys across all objects. Missing keys
196/// become empty cells. Nested objects or arrays cause an error to prevent
197/// silent corruption of round-trips.
198///
199/// # Errors
200///
201/// Returns an error if the input is not an array of objects, contains nested
202/// values, or if writing fails.
203///
204/// # Examples
205///
206/// ```
207/// use sheeter_engine::json_to_csv;
208///
209/// let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
210/// assert_eq!(json_to_csv(json).unwrap(), "name,age\nAlice,30\nBob,25\n");
211/// ```
212pub fn json_to_csv(json: &str) -> Result<String, Box<dyn std::error::Error>> {
213    json_to_delimited(json, b',')
214}
215
216/// Converts a JSON array of objects into a TSV string.
217///
218/// Same semantics as `json_to_csv`, but fields are separated by a tab
219/// character. The output uses `\n` line endings.
220///
221/// # Errors
222///
223/// Returns an error if the input is not an array of objects, contains nested
224/// values, or if writing fails.
225///
226/// # Examples
227///
228/// ```
229/// use sheeter_engine::json_to_tsv;
230///
231/// let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
232/// assert_eq!(json_to_tsv(json).unwrap(), "name\tage\nAlice\t30\nBob\t25\n");
233/// ```
234pub fn json_to_tsv(json: &str) -> Result<String, Box<dyn std::error::Error>> {
235    json_to_delimited(json, b'\t')
236}