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 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/// # Errors
44///
45/// Returns an error if the CSV is malformed (e.g., inconsistent number of columns)
46/// or if JSON serialization fails (which is unlikely for simple string maps).
47///
48/// # Examples
49///
50/// Basic conversion:
51///
52/// ```
53/// use sheeter_engine::csv_to_json;
54///
55/// let csv = "name,age\nAlice,30\nBob,25";
56/// let json = csv_to_json(csv).unwrap();
57/// assert_eq!(json, r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#);
58/// ```
59///
60/// Single column:
61///
62/// ```
63/// use sheeter_engine::csv_to_json;
64///
65/// let csv = "id\n1\n2\n3";
66/// let json = csv_to_json(csv).unwrap();
67/// assert_eq!(json, r#"[{"id":"1"},{"id":"2"},{"id":"3"}]"#);
68/// ```
69pub fn csv_to_json(csv: &str) -> Result<String, Box<dyn std::error::Error>> {
70    let mut rdr = csv::Reader::from_reader(Cursor::new(csv));
71    let headers = rdr.headers()?.clone();
72    let mut records = Vec::new();
73    for result in rdr.records() {
74        let record = result?;
75        let mut map = serde_json::Map::new();
76        for (header, value) in headers.iter().zip(record.iter()) {
77            map.insert(
78                header.to_string(),
79                serde_json::Value::String(value.to_string()),
80            );
81        }
82        records.push(serde_json::Value::Object(map));
83    }
84    Ok(serde_json::to_string(&records)?)
85}
86
87/// Converts a JSON array of objects into a CSV string.
88///
89/// JSON arrays of objects are a common way to represent tabular data. This function
90/// takes such a JSON string and produces a CSV string. The keys of the first object
91/// are used as the CSV header row. All objects are expected to have the same keys;
92/// if a key is missing in an object, the corresponding CSV cell is left empty.
93/// Values are converted to their string representation: strings are written as-is
94/// (without quotes), numbers and booleans use their JSON literal forms.
95///
96/// # Errors
97///
98/// Returns an error if the JSON is not a valid array of objects, or if CSV writing
99/// fails (e.g., due to I/O errors on the internal buffer).
100///
101/// # Examples
102///
103/// Basic conversion:
104///
105/// ```
106/// use sheeter_engine::json_to_csv;
107///
108/// let json = r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#;
109/// let csv = json_to_csv(json).unwrap();
110/// assert_eq!(csv, "name,age\nAlice,30\nBob,25\n");
111/// ```
112///
113/// With numbers and booleans:
114///
115/// ```
116/// use sheeter_engine::json_to_csv;
117///
118/// let json = r#"[{"id":1,"active":true},{"id":2,"active":false}]"#;
119/// let csv = json_to_csv(json).unwrap();
120/// assert_eq!(csv, "id,active\n1,true\n2,false\n");
121/// ```
122pub fn json_to_csv(json: &str) -> Result<String, Box<dyn std::error::Error>> {
123    let values: Vec<serde_json::Value> = serde_json::from_str(json)?;
124    if values.is_empty() {
125        return Ok(String::new());
126    }
127    let first = values[0]
128        .as_object()
129        .ok_or("JSON array must contain objects")?;
130    let headers: Vec<String> = first.keys().cloned().collect();
131    let mut wtr = csv::Writer::from_writer(vec![]);
132    wtr.write_record(&headers)?;
133    for value in values {
134        let obj = value.as_object().ok_or("JSON array must contain objects")?;
135        let record: Vec<String> = headers
136            .iter()
137            .map(|h| match obj.get(h) {
138                Some(serde_json::Value::String(s)) => s.clone(),
139                Some(v) => v.to_string(),
140                None => String::new(),
141            })
142            .collect();
143        wtr.write_record(&record)?;
144    }
145    let data = wtr.into_inner()?;
146    Ok(String::from_utf8(data)?)
147}