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 CSV string into a JSON array of objects.
34///
35/// CSV (Comma-Separated Values) is a simple tabular format where each line is a
36/// row and columns are separated by commas. JSON (JavaScript Object Notation) is
37/// a hierarchical data format. This function reads the first row of the CSV as
38/// the header (column names) and each subsequent row as a data record. It then
39/// builds a JSON array where each element is an object mapping header names to
40/// the corresponding cell values as strings. This is useful for feeding CSV data
41/// into JSON-based systems or APIs, or for further processing with JSON tools.
42///
43/// Duplicate header names are rejected because they would silently overwrite
44/// data in the resulting JSON objects. All cells are represented as strings in
45/// the JSON output; type information (numbers, booleans) is not inferred.
46///
47/// # Errors
48///
49/// Returns an error if the CSV is malformed (e.g., inconsistent number of columns),
50/// if a header name appears more than once, or if JSON serialization fails (which
51/// is unlikely for simple string maps).
52///
53/// # Examples
54///
55/// Basic conversion:
56///
57/// ```
58/// use sheeter_engine::csv_to_json;
59///
60/// let csv = "name,age\nAlice,30\nBob,25";
61/// let json = csv_to_json(csv).unwrap();
62/// assert_eq!(json, r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#);
63/// ```
64///
65/// Single column:
66///
67/// ```
68/// use sheeter_engine::csv_to_json;
69///
70/// let csv = "id\n1\n2\n3";
71/// let json = csv_to_json(csv).unwrap();
72/// assert_eq!(json, r#"[{"id":"1"},{"id":"2"},{"id":"3"}]"#);
73/// ```
74///
75/// Duplicate headers are rejected:
76///
77/// ```
78/// use sheeter_engine::csv_to_json;
79///
80/// assert!(csv_to_json("a,a\n1,2").is_err());
81/// ```
82pub fn csv_to_json(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
83 let mut rdr = csv::Reader::from_reader(Cursor::new(csv));
84 let headers = rdr.headers()?.clone();
85 let mut seen = std::collections::HashSet::new();
86 for header in headers.iter() {
87 if !seen.insert(header) {
88 return Err(format!("duplicate CSV header: {header}").into());
89 }
90 }
91 let mut records = Vec::new();
92 for result in rdr.records() {
93 let record = result?;
94 let mut map = serde_json::Map::new();
95 for (header, value) in headers.iter().zip(record.iter()) {
96 map.insert(
97 header.to_string(),
98 serde_json::Value::String(value.to_string()),
99 );
100 }
101 records.push(serde_json::Value::Object(map));
102 }
103 Ok(serde_json::to_string(&records)?)
104}
105
106/// Converts a JSON array of objects into a CSV string.
107///
108/// JSON arrays of objects are a common way to represent tabular data. This function
109/// takes such a JSON string and produces a CSV string. The CSV header row is the
110/// union of all keys across all objects, in the order they first appear; if a key
111/// is missing in a given object, the corresponding cell is left empty. Values are
112/// converted to their string representation: strings are written as-is (without
113/// quotes), numbers and booleans use their JSON literal forms, and null becomes
114/// an empty cell. Nested objects and arrays cannot be represented losslessly in
115/// flat CSV and cause an error, ensuring round-trips never silently corrupt data.
116///
117/// # Errors
118///
119/// Returns an error if the JSON is not a valid array of objects, if any value is
120/// a nested object or array, or if CSV writing fails (e.g., due to I/O errors on
121/// the internal buffer).
122///
123/// # Examples
124///
125/// Basic conversion:
126///
127/// ```
128/// use sheeter_engine::json_to_csv;
129///
130/// let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
131/// let csv = json_to_csv(json).unwrap();
132/// assert_eq!(csv, "name,age\nAlice,30\nBob,25\n");
133/// ```
134///
135/// With numbers and booleans:
136///
137/// ```
138/// use sheeter_engine::json_to_csv;
139///
140/// let json = r#"[{"id":1,"active":true},{"id":2,"active":false}]"#;
141/// let csv = json_to_csv(json).unwrap();
142/// assert_eq!(csv, "id,active\n1,true\n2,false\n");
143/// ```
144///
145/// Extra keys in later objects are preserved:
146///
147/// ```
148/// use sheeter_engine::json_to_csv;
149///
150/// let json = r#"[{"a":1},{"a":2,"b":3}]"#;
151/// assert_eq!(json_to_csv(json).unwrap(), "a,b\n1,\n2,3\n");
152/// ```
153///
154/// Nested values are rejected:
155///
156/// ```
157/// use sheeter_engine::json_to_csv;
158///
159/// assert!(json_to_csv(r#"[{"a":{"nested":true}}]"#).is_err());
160/// ```
161pub fn json_to_csv(json: &str) -> Result<String, Box<dyn std::error::Error>> {
162 let values: Vec<serde_json::Value> = serde_json::from_str(json)?;
163 if values.is_empty() {
164 return Ok(String::new());
165 }
166 let mut headers: Vec<String> = Vec::new();
167 for value in &values {
168 let obj = value.as_object().ok_or("JSON array must contain objects")?;
169 for key in obj.keys() {
170 if !headers.contains(key) {
171 headers.push(key.clone());
172 }
173 }
174 }
175 let mut wtr = csv::Writer::from_writer(vec![]);
176 wtr.write_record(&headers)?;
177 for value in values {
178 let obj = value.as_object().ok_or("JSON array must contain objects")?;
179 let mut record: Vec<String> = Vec::with_capacity(headers.len());
180 for h in &headers {
181 let cell = match obj.get(h) {
182 Some(serde_json::Value::String(s)) => s.clone(),
183 Some(serde_json::Value::Number(n)) => n.to_string(),
184 Some(serde_json::Value::Bool(b)) => b.to_string(),
185 Some(serde_json::Value::Null) => String::new(),
186 Some(serde_json::Value::Object(_)) | Some(serde_json::Value::Array(_)) => {
187 return Err(
188 format!("nested value at key {h} cannot be represented in CSV").into(),
189 );
190 }
191 None => String::new(),
192 };
193 record.push(cell);
194 }
195 wtr.write_record(&record)?;
196 }
197 let data = wtr.into_inner()?;
198 Ok(String::from_utf8(data)?)
199}