1use 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 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 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 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""#; 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}