json_to_xlsx/
lib.rs

1//! Simple library to convert JSON files to Excel (xlsx).
2use std::io::{Cursor, Read, Seek, Write};
3
4use serde_json::{Deserializer, Value};
5use zip::write::FileOptions;
6
7use crate::result::{XlsxExportResult, XlsxExportError};
8
9pub mod result;
10
11pub fn json_to_xlsx(reader: impl Read, mut output: impl Write) -> XlsxExportResult<()> {
12    let mut stream = Deserializer::from_reader(reader).into_iter::<Value>();
13
14    let main_array: Vec<Value> = match stream.next() {
15        Some(Ok(Value::Array(list))) => list,
16        Some(Ok(_)) => return Err(XlsxExportError::NotAnArray),
17        Some(Err(e)) => return Err(XlsxExportError::JsonError(e)),
18        None => return Err(XlsxExportError::NotAnArray),
19    };
20
21    if main_array.is_empty() {
22        return Err(XlsxExportError::EmptyArray);
23    }
24
25    let first_item = main_array.first().unwrap();
26    let first_item = match first_item {
27        Value::Object(o) => o,
28        _ => return Err(XlsxExportError::ExpectedObject),
29    };
30    let headers: Vec<String> = first_item.keys().cloned().collect();
31
32    let mut buffer = Cursor::new(Vec::new());
33    {
34        let mut zip = zip::ZipWriter::new(&mut buffer);
35        let options = FileOptions::default();
36
37        write_content_types(&mut zip, options)?;
38        write_rels(&mut zip, options)?;
39        write_workbook(&mut zip, options)?;
40        write_sheet1(&mut zip, options, &headers, main_array)?;
41
42        zip.finish()?;
43    }
44
45    output.write_all(&buffer.into_inner())?;
46    Ok(())
47}
48
49fn write_content_types<W: Write + Seek>(
50    zip: &mut zip::ZipWriter<W>,
51    options: FileOptions,
52) -> zip::result::ZipResult<()> {
53    let xml = r#"
54        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
55        <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
56            <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
57            <Default Extension="xml" ContentType="application/xml"/>
58            <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
59            <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
60        </Types>
61    "#;
62    zip.start_file("[Content_Types].xml", options)?;
63    zip.write_all(xml.trim_start().as_bytes())
64        .map_err(zip::result::ZipError::Io)
65}
66
67fn write_rels<W: Write + Seek>(
68    zip: &mut zip::ZipWriter<W>,
69    options: FileOptions,
70) -> zip::result::ZipResult<()> {
71    zip.start_file("_rels/.rels", options)?;
72    let xml = r#"
73        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
74        <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
75            <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
76        </Relationships>
77    "#;
78    zip.write_all(xml.trim_start().as_bytes())
79        .map_err(zip::result::ZipError::Io)
80}
81
82fn write_workbook<W: Write + Seek>(
83    zip: &mut zip::ZipWriter<W>,
84    options: FileOptions,
85) -> zip::result::ZipResult<()> {
86    zip.start_file("xl/workbook.xml", options)?;
87    let xml = r#"
88        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
89        <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
90                  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
91            <sheets>
92                <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
93            </sheets>
94        </workbook>
95    "#;
96    zip.write_all(xml.trim_start().as_bytes())
97        .map_err(zip::result::ZipError::Io)?;
98
99    // Add relationships
100    zip.start_file("xl/_rels/workbook.xml.rels", options)?;
101    let rels = r#"
102        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
103        <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
104            <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
105        </Relationships>
106    "#;
107    zip.write_all(rels.trim_start().as_bytes())
108        .map_err(zip::result::ZipError::Io)
109}
110
111fn write_sheet1<W: Write + Seek>(
112    zip: &mut zip::ZipWriter<W>,
113    options: FileOptions,
114    headers: &[String],
115    main_array: Vec<Value>,
116) -> zip::result::ZipResult<()> {
117    zip.start_file("xl/worksheets/sheet1.xml", options)?;
118
119    let mut xml = String::new();
120    xml.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
121    xml.push_str(r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>"#);
122
123    let mut row_idx = 1;
124
125    // Write header
126    xml.push_str(&format!(r#"<row r="{}">"#, row_idx));
127    for (i, header) in headers.iter().enumerate() {
128        let col = column_letter(i + 1);
129        xml.push_str(&format!(
130            r#"<c r="{}{}" t="str"><v>{}</v></c>"#,
131            col, row_idx, header
132        ));
133    }
134    xml.push_str("</row>");
135    row_idx += 1;
136
137    // Write data rows
138    for row in main_array {
139        let value: Value = row;
140        if let Value::Object(o) = value {
141            xml.push_str(&format!(r#"<row r="{}">"#, row_idx));
142            for (i, key) in headers.iter().enumerate() {
143                let col = column_letter(i + 1);
144                let value = o.get(key).map_or("".to_string(), |v| {
145                    v.to_string().trim_matches('"').to_string()
146                });
147                xml.push_str(&format!(
148                    r#"<c r="{}{}" t="str"><v>{}</v></c>"#,
149                    col, row_idx, value
150                ));
151            }
152            xml.push_str("</row>");
153        }
154
155        row_idx += 1;
156    }
157
158    xml.push_str("</sheetData></worksheet>");
159    zip.write_all(xml.as_bytes())
160        .map_err(zip::result::ZipError::Io)
161}
162
163fn column_letter(mut n: usize) -> String {
164    let mut col = String::new();
165    while n > 0 {
166        let rem = (n - 1) % 26;
167        col.insert(0, (b'A' + rem as u8) as char);
168        n = (n - 1) / 26;
169    }
170    col
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use std::io::Cursor;
177
178    fn run_and_extract_xlsx(json_input: &str) -> Vec<u8> {
179        let reader = Cursor::new(json_input);
180        let mut output = Vec::new();
181        let result = json_to_xlsx(reader, &mut output);
182        assert!(
183            result.is_ok(),
184            "Expected OK but got error: {:?}",
185            result.err()
186        );
187        output
188    }
189
190    #[test]
191    fn test_valid_json_conversion() {
192        let json = r#"
193            [
194                { "name": "Alice", "age": 30 },
195                { "name": "Bob", "age": 25 }
196            ]
197        "#;
198
199        let output = run_and_extract_xlsx(json);
200        assert!(
201            output.starts_with(b"PK"),
202            "Expected XLSX (zip) to start with PK"
203        );
204        assert!(
205            output.len() > 100,
206            "Output seems too small to be valid XLSX"
207        );
208    }
209
210    #[test]
211    fn test_non_array_json_root() {
212        let json = r#"{ "name": "Not an array" }"#;
213        let reader = Cursor::new(json);
214        let mut output = Vec::new();
215        let result = json_to_xlsx(reader, &mut output);
216        assert!(matches!(result, Err(XlsxExportError::NotAnArray)));
217    }
218
219    #[test]
220    fn test_empty_array() {
221        let json = r#"[]"#;
222        let reader = Cursor::new(json);
223        let mut output = Vec::new();
224        let result = json_to_xlsx(reader, &mut output);
225        assert!(matches!(result, Err(XlsxExportError::EmptyArray)));
226    }
227
228    #[test]
229    fn test_array_with_non_object_elements() {
230        let json = r#"[1, 2, 3]"#;
231        let reader = Cursor::new(json);
232        let mut output = Vec::new();
233        let result = json_to_xlsx(reader, &mut output);
234        assert!(matches!(result, Err(XlsxExportError::ExpectedObject)));
235    }
236
237    #[test]
238    fn test_malformed_json() {
239        let json = r#"[{ "name": "John""#; // Missing closing brace
240        let reader = Cursor::new(json);
241        let mut output = Vec::new();
242        let result = json_to_xlsx(reader, &mut output);
243        assert!(matches!(result, Err(XlsxExportError::JsonError(_))));
244    }
245
246    #[test]
247    fn test_missing_fields_in_some_objects() {
248        let json = r#"
249            [
250                { "name": "Alice", "age": 30 },
251                { "name": "Bob" }
252            ]
253        "#;
254
255        let output = run_and_extract_xlsx(json);
256        assert!(output.starts_with(b"PK"));
257    }
258}