1use crate::types::{AdminError, AdminResult};
7use csv::Writer;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ExportFormat {
14 CSV,
16 JSON,
18 Excel,
20 TSV,
22 XML,
24}
25
26impl ExportFormat {
27 pub fn extension(&self) -> &'static str {
29 match self {
30 ExportFormat::CSV => "csv",
31 ExportFormat::JSON => "json",
32 ExportFormat::Excel => "xlsx",
33 ExportFormat::TSV => "tsv",
34 ExportFormat::XML => "xml",
35 }
36 }
37
38 pub fn mime_type(&self) -> &'static str {
40 match self {
41 ExportFormat::CSV => "text/csv",
42 ExportFormat::JSON => "application/json",
43 ExportFormat::Excel => {
44 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
45 }
46 ExportFormat::TSV => "text/tab-separated-values",
47 ExportFormat::XML => "application/xml",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
69pub struct ExportConfig {
70 model_name: String,
72 format: ExportFormat,
74 fields: Vec<String>,
76 field_labels: HashMap<String, String>,
78 filters: HashMap<String, String>,
80 ordering: Vec<String>,
82 max_rows: Option<usize>,
84 include_headers: bool,
86}
87
88impl ExportConfig {
89 pub fn new(model_name: impl Into<String>, format: ExportFormat) -> Self {
91 Self {
92 model_name: model_name.into(),
93 format,
94 fields: Vec::new(),
95 field_labels: HashMap::new(),
96 filters: HashMap::new(),
97 ordering: Vec::new(),
98 max_rows: None,
99 include_headers: true,
100 }
101 }
102
103 pub fn model_name(&self) -> &str {
105 &self.model_name
106 }
107
108 pub fn format(&self) -> ExportFormat {
110 self.format
111 }
112
113 pub fn with_field(mut self, field: impl Into<String>) -> Self {
115 self.fields.push(field.into());
116 self
117 }
118
119 pub fn with_fields(mut self, fields: Vec<String>) -> Self {
121 self.fields = fields;
122 self
123 }
124
125 pub fn fields(&self) -> &[String] {
127 &self.fields
128 }
129
130 pub fn field_count(&self) -> usize {
132 self.fields.len()
133 }
134
135 pub fn with_field_label(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
137 self.field_labels.insert(field.into(), label.into());
138 self
139 }
140
141 pub fn get_field_label(&self, field: &str) -> Option<&String> {
143 self.field_labels.get(field)
144 }
145
146 pub fn with_filter(mut self, field: impl Into<String>, value: impl Into<String>) -> Self {
148 self.filters.insert(field.into(), value.into());
149 self
150 }
151
152 pub fn filters(&self) -> &HashMap<String, String> {
154 &self.filters
155 }
156
157 pub fn with_ordering(mut self, ordering: Vec<String>) -> Self {
159 self.ordering = ordering;
160 self
161 }
162
163 pub fn ordering(&self) -> &[String] {
165 &self.ordering
166 }
167
168 pub fn with_max_rows(mut self, max: usize) -> Self {
170 self.max_rows = Some(max);
171 self
172 }
173
174 pub fn max_rows(&self) -> Option<usize> {
176 self.max_rows
177 }
178
179 pub fn with_headers(mut self, include: bool) -> Self {
181 self.include_headers = include;
182 self
183 }
184
185 pub fn include_headers(&self) -> bool {
187 self.include_headers
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ExportResult {
194 pub data: Vec<u8>,
196 pub mime_type: String,
198 pub filename: String,
200 pub row_count: usize,
202}
203
204impl ExportResult {
205 pub fn new(
207 data: Vec<u8>,
208 mime_type: impl Into<String>,
209 filename: impl Into<String>,
210 row_count: usize,
211 ) -> Self {
212 Self {
213 data,
214 mime_type: mime_type.into(),
215 filename: filename.into(),
216 row_count,
217 }
218 }
219
220 pub fn size_bytes(&self) -> usize {
222 self.data.len()
223 }
224
225 pub fn size_kb(&self) -> f64 {
227 self.data.len() as f64 / 1024.0
228 }
229}
230
231pub struct CsvExporter;
233
234impl CsvExporter {
235 pub fn export(
254 fields: &[String],
255 data: &[HashMap<String, String>],
256 include_headers: bool,
257 ) -> AdminResult<Vec<u8>> {
258 let mut writer = Writer::from_writer(Vec::new());
260
261 if include_headers {
263 writer.write_record(fields).map_err(|e| {
264 AdminError::ValidationError(format!("Failed to write CSV headers: {}", e))
265 })?;
266 }
267
268 for row in data {
270 let values: Vec<&str> = fields
271 .iter()
272 .map(|field| row.get(field).map(|v| v.as_str()).unwrap_or(""))
273 .collect();
274
275 writer.write_record(&values).map_err(|e| {
276 AdminError::ValidationError(format!("Failed to write CSV row: {}", e))
277 })?;
278 }
279
280 writer.flush().map_err(|e| {
282 AdminError::ValidationError(format!("Failed to flush CSV writer: {}", e))
283 })?;
284
285 let output = writer
286 .into_inner()
287 .map_err(|e| AdminError::ValidationError(format!("Failed to get CSV output: {}", e)))?;
288
289 Ok(output)
290 }
291}
292
293pub struct JsonExporter;
295
296impl JsonExporter {
297 pub fn export(data: &[HashMap<String, String>]) -> AdminResult<Vec<u8>> {
315 serde_json::to_vec_pretty(data)
316 .map_err(|e| AdminError::ValidationError(format!("JSON export failed: {}", e)))
317 }
318}
319
320pub struct TsvExporter;
322
323impl TsvExporter {
324 pub fn export(
326 fields: &[String],
327 data: &[HashMap<String, String>],
328 include_headers: bool,
329 ) -> AdminResult<Vec<u8>> {
330 let mut output = Vec::new();
331
332 if include_headers {
334 let header_line = fields.join("\t");
335 output.extend_from_slice(header_line.as_bytes());
336 output.extend_from_slice(b"\r\n");
337 }
338
339 for row in data {
341 let values: Vec<String> = fields
342 .iter()
343 .map(|field| {
344 row.get(field)
347 .map(|v| v.replace("\r\n", " ").replace(['\t', '\n', '\r'], " "))
348 .unwrap_or_default()
349 })
350 .collect();
351 let line = values.join("\t");
352 output.extend_from_slice(line.as_bytes());
353 output.extend_from_slice(b"\r\n");
354 }
355
356 Ok(output)
357 }
358}
359
360pub struct ExportBuilder {
381 config: ExportConfig,
382 data: Vec<HashMap<String, String>>,
383}
384
385impl ExportBuilder {
386 pub fn new(model_name: impl Into<String>, format: ExportFormat) -> Self {
388 Self {
389 config: ExportConfig::new(model_name, format),
390 data: Vec::new(),
391 }
392 }
393
394 pub fn field(mut self, field: impl Into<String>) -> Self {
396 self.config = self.config.with_field(field);
397 self
398 }
399
400 pub fn fields(mut self, fields: Vec<String>) -> Self {
402 self.config = self.config.with_fields(fields);
403 self
404 }
405
406 pub fn field_label(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
408 self.config = self.config.with_field_label(field, label);
409 self
410 }
411
412 pub fn data(mut self, data: Vec<HashMap<String, String>>) -> Self {
414 self.data = data;
415 self
416 }
417
418 pub fn max_rows(mut self, max: usize) -> Self {
420 self.config = self.config.with_max_rows(max);
421 self
422 }
423
424 pub fn build(self) -> AdminResult<ExportResult> {
426 let fields = if self.config.fields().is_empty() {
427 let mut all_fields: Vec<String> = self
429 .data
430 .iter()
431 .flat_map(|row| row.keys().cloned())
432 .collect::<std::collections::HashSet<_>>()
433 .into_iter()
434 .collect();
435 all_fields.sort();
436 all_fields
437 } else {
438 self.config.fields().to_vec()
439 };
440
441 let effective_data = if let Some(max) = self.config.max_rows() {
443 &self.data[..self.data.len().min(max)]
444 } else {
445 &self.data
446 };
447
448 let exported = match self.config.format() {
449 ExportFormat::CSV => {
450 CsvExporter::export(&fields, effective_data, self.config.include_headers())?
451 }
452 ExportFormat::JSON => JsonExporter::export(effective_data)?,
453 ExportFormat::TSV => {
454 TsvExporter::export(&fields, effective_data, self.config.include_headers())?
455 }
456 ExportFormat::Excel | ExportFormat::XML => {
457 return Err(AdminError::ValidationError(format!(
458 "{:?} export not yet implemented",
459 self.config.format()
460 )));
461 }
462 };
463
464 let filename = format!(
465 "{}_{}.{}",
466 self.config.model_name(),
467 chrono::Utc::now().format("%Y%m%d_%H%M%S"),
468 self.config.format().extension()
469 );
470
471 Ok(ExportResult::new(
472 exported,
473 self.config.format().mime_type().to_string(),
474 filename,
475 effective_data.len(),
476 ))
477 }
478}
479
480#[cfg(all(test, server))]
481mod tests {
482 use super::*;
483 use rstest::rstest;
484
485 #[test]
486 fn test_export_format_extension() {
487 assert_eq!(ExportFormat::CSV.extension(), "csv");
488 assert_eq!(ExportFormat::JSON.extension(), "json");
489 assert_eq!(ExportFormat::Excel.extension(), "xlsx");
490 assert_eq!(ExportFormat::TSV.extension(), "tsv");
491 }
492
493 #[test]
494 fn test_export_format_mime_type() {
495 assert_eq!(ExportFormat::CSV.mime_type(), "text/csv");
496 assert_eq!(ExportFormat::JSON.mime_type(), "application/json");
497 }
498
499 #[test]
500 fn test_export_config_new() {
501 let config = ExportConfig::new("User", ExportFormat::CSV);
502 assert_eq!(config.model_name(), "User");
503 assert_eq!(config.format(), ExportFormat::CSV);
504 assert!(config.include_headers());
505 }
506
507 #[test]
508 fn test_export_config_with_field() {
509 let config = ExportConfig::new("User", ExportFormat::CSV)
510 .with_field("id")
511 .with_field("username");
512
513 assert_eq!(config.field_count(), 2);
514 }
515
516 #[test]
517 fn test_csv_exporter_basic() {
518 let fields = vec!["id".to_string(), "name".to_string()];
519 let mut row1 = HashMap::new();
520 row1.insert("id".to_string(), "1".to_string());
521 row1.insert("name".to_string(), "Alice".to_string());
522
523 let mut row2 = HashMap::new();
524 row2.insert("id".to_string(), "2".to_string());
525 row2.insert("name".to_string(), "Bob".to_string());
526
527 let data = vec![row1, row2];
528 let result = CsvExporter::export(&fields, &data, true);
529
530 assert!(result.is_ok());
531 let output = String::from_utf8(result.unwrap()).unwrap();
532 assert!(output.contains("id,name"));
533 assert!(output.contains("1,Alice"));
534 assert!(output.contains("2,Bob"));
535 }
536
537 #[test]
538 fn test_csv_exporter_escape() {
539 let fields = vec!["id".to_string(), "name".to_string()];
540 let mut row = HashMap::new();
541 row.insert("id".to_string(), "1".to_string());
542 row.insert("name".to_string(), "Smith, John".to_string());
543
544 let data = vec![row];
545 let result = CsvExporter::export(&fields, &data, true);
546
547 assert!(result.is_ok());
548 let output = String::from_utf8(result.unwrap()).unwrap();
549 assert!(output.contains("\"Smith, John\""));
550 }
551
552 #[test]
553 fn test_json_exporter() {
554 let mut row1 = HashMap::new();
555 row1.insert("id".to_string(), "1".to_string());
556 row1.insert("name".to_string(), "Alice".to_string());
557
558 let data = vec![row1];
559 let result = JsonExporter::export(&data);
560
561 assert!(result.is_ok());
562 let output = String::from_utf8(result.unwrap()).unwrap();
563 assert!(output.contains("\"id\""));
564 assert!(output.contains("\"Alice\""));
565 }
566
567 #[test]
568 fn test_tsv_exporter() {
569 let fields = vec!["id".to_string(), "name".to_string()];
570 let mut row = HashMap::new();
571 row.insert("id".to_string(), "1".to_string());
572 row.insert("name".to_string(), "Alice".to_string());
573
574 let data = vec![row];
575 let result = TsvExporter::export(&fields, &data, true);
576
577 assert!(result.is_ok());
578 let output = String::from_utf8(result.unwrap()).unwrap();
579 assert!(output.contains("id\tname\r\n"));
580 assert!(output.contains("1\tAlice\r\n"));
581 }
582
583 #[rstest]
584 fn test_tsv_exporter_escapes_newlines() {
585 let fields = vec!["id".to_string(), "bio".to_string()];
586 let mut row = HashMap::new();
587 row.insert("id".to_string(), "1".to_string());
588 row.insert("bio".to_string(), "line1\nline2\r\nline3".to_string());
589
590 let data = vec![row];
591 let result = TsvExporter::export(&fields, &data, true);
592
593 assert!(result.is_ok());
594 let output = String::from_utf8(result.unwrap()).unwrap();
595 assert_eq!(output, "id\tbio\r\n1\tline1 line2 line3\r\n");
597 }
598
599 #[test]
600 fn test_export_builder() {
601 let mut row = HashMap::new();
602 row.insert("id".to_string(), "1".to_string());
603 row.insert("username".to_string(), "alice".to_string());
604
605 let result = ExportBuilder::new("User", ExportFormat::CSV)
606 .field("id")
607 .field("username")
608 .data(vec![row])
609 .build();
610
611 let export = result.unwrap();
612 assert_eq!(export.row_count, 1);
613 assert!(export.filename.starts_with("User_"));
614 assert!(export.filename.ends_with(".csv"));
615 }
616
617 #[test]
618 fn test_export_result() {
619 let data = vec![1, 2, 3, 4, 5];
620 let result = ExportResult::new(data, "text/csv".to_string(), "test.csv".to_string(), 10);
621
622 assert_eq!(result.row_count, 10);
623 assert_eq!(result.size_bytes(), 5);
624 assert!((result.size_kb() - 0.00488).abs() < 0.001);
625 }
626
627 #[test]
628 fn test_export_config_filters() {
629 let config = ExportConfig::new("User", ExportFormat::CSV)
630 .with_filter("status", "active")
631 .with_filter("role", "admin");
632
633 assert_eq!(config.filters().len(), 2);
634 assert_eq!(config.filters().get("status"), Some(&"active".to_string()));
635 }
636
637 #[test]
638 fn test_export_config_ordering() {
639 let config = ExportConfig::new("User", ExportFormat::CSV)
640 .with_ordering(vec!["name".to_string(), "-created_at".to_string()]);
641
642 assert_eq!(config.ordering().len(), 2);
643 }
644
645 #[rstest]
646 fn test_export_builder_max_rows_truncates_output() {
647 let mut row1 = HashMap::new();
649 row1.insert("id".to_string(), "1".to_string());
650 row1.insert("name".to_string(), "Alice".to_string());
651
652 let mut row2 = HashMap::new();
653 row2.insert("id".to_string(), "2".to_string());
654 row2.insert("name".to_string(), "Bob".to_string());
655
656 let mut row3 = HashMap::new();
657 row3.insert("id".to_string(), "3".to_string());
658 row3.insert("name".to_string(), "Carol".to_string());
659
660 let result = ExportBuilder::new("User", ExportFormat::CSV)
662 .field("id")
663 .field("name")
664 .data(vec![row1, row2, row3])
665 .max_rows(2)
666 .build();
667
668 let export = result.unwrap();
670 assert_eq!(export.row_count, 2);
671 let output = String::from_utf8(export.data).unwrap();
672 assert!(output.contains("1,Alice"));
673 assert!(output.contains("2,Bob"));
674 assert!(!output.contains("3,Carol"));
675 }
676}