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.push(b'\n');
337 }
338
339 for row in data {
341 let values: Vec<String> = fields
342 .iter()
343 .map(|field| {
344 row.get(field)
345 .map(|v| v.replace('\t', " "))
346 .unwrap_or_default()
347 })
348 .collect();
349 let line = values.join("\t");
350 output.extend_from_slice(line.as_bytes());
351 output.push(b'\n');
352 }
353
354 Ok(output)
355 }
356}
357
358pub struct ExportBuilder {
379 config: ExportConfig,
380 data: Vec<HashMap<String, String>>,
381}
382
383impl ExportBuilder {
384 pub fn new(model_name: impl Into<String>, format: ExportFormat) -> Self {
386 Self {
387 config: ExportConfig::new(model_name, format),
388 data: Vec::new(),
389 }
390 }
391
392 pub fn field(mut self, field: impl Into<String>) -> Self {
394 self.config = self.config.with_field(field);
395 self
396 }
397
398 pub fn fields(mut self, fields: Vec<String>) -> Self {
400 self.config = self.config.with_fields(fields);
401 self
402 }
403
404 pub fn field_label(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
406 self.config = self.config.with_field_label(field, label);
407 self
408 }
409
410 pub fn data(mut self, data: Vec<HashMap<String, String>>) -> Self {
412 self.data = data;
413 self
414 }
415
416 pub fn max_rows(mut self, max: usize) -> Self {
418 self.config = self.config.with_max_rows(max);
419 self
420 }
421
422 pub fn build(self) -> AdminResult<ExportResult> {
424 let fields = if self.config.fields().is_empty() {
425 let mut all_fields: Vec<String> = self
427 .data
428 .iter()
429 .flat_map(|row| row.keys().cloned())
430 .collect::<std::collections::HashSet<_>>()
431 .into_iter()
432 .collect();
433 all_fields.sort();
434 all_fields
435 } else {
436 self.config.fields().to_vec()
437 };
438
439 let data = match self.config.format() {
440 ExportFormat::CSV => {
441 CsvExporter::export(&fields, &self.data, self.config.include_headers())?
442 }
443 ExportFormat::JSON => JsonExporter::export(&self.data)?,
444 ExportFormat::TSV => {
445 TsvExporter::export(&fields, &self.data, self.config.include_headers())?
446 }
447 ExportFormat::Excel | ExportFormat::XML => {
448 return Err(AdminError::ValidationError(format!(
449 "{:?} export not yet implemented",
450 self.config.format()
451 )));
452 }
453 };
454
455 let filename = format!(
456 "{}_{}.{}",
457 self.config.model_name(),
458 chrono::Utc::now().format("%Y%m%d_%H%M%S"),
459 self.config.format().extension()
460 );
461
462 Ok(ExportResult::new(
463 data,
464 self.config.format().mime_type().to_string(),
465 filename,
466 self.data.len(),
467 ))
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn test_export_format_extension() {
477 assert_eq!(ExportFormat::CSV.extension(), "csv");
478 assert_eq!(ExportFormat::JSON.extension(), "json");
479 assert_eq!(ExportFormat::Excel.extension(), "xlsx");
480 assert_eq!(ExportFormat::TSV.extension(), "tsv");
481 }
482
483 #[test]
484 fn test_export_format_mime_type() {
485 assert_eq!(ExportFormat::CSV.mime_type(), "text/csv");
486 assert_eq!(ExportFormat::JSON.mime_type(), "application/json");
487 }
488
489 #[test]
490 fn test_export_config_new() {
491 let config = ExportConfig::new("User", ExportFormat::CSV);
492 assert_eq!(config.model_name(), "User");
493 assert_eq!(config.format(), ExportFormat::CSV);
494 assert!(config.include_headers());
495 }
496
497 #[test]
498 fn test_export_config_with_field() {
499 let config = ExportConfig::new("User", ExportFormat::CSV)
500 .with_field("id")
501 .with_field("username");
502
503 assert_eq!(config.field_count(), 2);
504 }
505
506 #[test]
507 fn test_csv_exporter_basic() {
508 let fields = vec!["id".to_string(), "name".to_string()];
509 let mut row1 = HashMap::new();
510 row1.insert("id".to_string(), "1".to_string());
511 row1.insert("name".to_string(), "Alice".to_string());
512
513 let mut row2 = HashMap::new();
514 row2.insert("id".to_string(), "2".to_string());
515 row2.insert("name".to_string(), "Bob".to_string());
516
517 let data = vec![row1, row2];
518 let result = CsvExporter::export(&fields, &data, true);
519
520 assert!(result.is_ok());
521 let output = String::from_utf8(result.unwrap()).unwrap();
522 assert!(output.contains("id,name"));
523 assert!(output.contains("1,Alice"));
524 assert!(output.contains("2,Bob"));
525 }
526
527 #[test]
528 fn test_csv_exporter_escape() {
529 let fields = vec!["id".to_string(), "name".to_string()];
530 let mut row = HashMap::new();
531 row.insert("id".to_string(), "1".to_string());
532 row.insert("name".to_string(), "Smith, John".to_string());
533
534 let data = vec![row];
535 let result = CsvExporter::export(&fields, &data, true);
536
537 assert!(result.is_ok());
538 let output = String::from_utf8(result.unwrap()).unwrap();
539 assert!(output.contains("\"Smith, John\""));
540 }
541
542 #[test]
543 fn test_json_exporter() {
544 let mut row1 = HashMap::new();
545 row1.insert("id".to_string(), "1".to_string());
546 row1.insert("name".to_string(), "Alice".to_string());
547
548 let data = vec![row1];
549 let result = JsonExporter::export(&data);
550
551 assert!(result.is_ok());
552 let output = String::from_utf8(result.unwrap()).unwrap();
553 assert!(output.contains("\"id\""));
554 assert!(output.contains("\"Alice\""));
555 }
556
557 #[test]
558 fn test_tsv_exporter() {
559 let fields = vec!["id".to_string(), "name".to_string()];
560 let mut row = HashMap::new();
561 row.insert("id".to_string(), "1".to_string());
562 row.insert("name".to_string(), "Alice".to_string());
563
564 let data = vec![row];
565 let result = TsvExporter::export(&fields, &data, true);
566
567 assert!(result.is_ok());
568 let output = String::from_utf8(result.unwrap()).unwrap();
569 assert!(output.contains("id\tname"));
570 assert!(output.contains("1\tAlice"));
571 }
572
573 #[test]
574 fn test_export_builder() {
575 let mut row = HashMap::new();
576 row.insert("id".to_string(), "1".to_string());
577 row.insert("username".to_string(), "alice".to_string());
578
579 let result = ExportBuilder::new("User", ExportFormat::CSV)
580 .field("id")
581 .field("username")
582 .data(vec![row])
583 .build();
584
585 let export = result.unwrap();
586 assert_eq!(export.row_count, 1);
587 assert!(export.filename.starts_with("User_"));
588 assert!(export.filename.ends_with(".csv"));
589 }
590
591 #[test]
592 fn test_export_result() {
593 let data = vec![1, 2, 3, 4, 5];
594 let result = ExportResult::new(data, "text/csv".to_string(), "test.csv".to_string(), 10);
595
596 assert_eq!(result.row_count, 10);
597 assert_eq!(result.size_bytes(), 5);
598 assert!((result.size_kb() - 0.00488).abs() < 0.001);
599 }
600
601 #[test]
602 fn test_export_config_filters() {
603 let config = ExportConfig::new("User", ExportFormat::CSV)
604 .with_filter("status", "active")
605 .with_filter("role", "admin");
606
607 assert_eq!(config.filters().len(), 2);
608 assert_eq!(config.filters().get("status"), Some(&"active".to_string()));
609 }
610
611 #[test]
612 fn test_export_config_ordering() {
613 let config = ExportConfig::new("User", ExportFormat::CSV)
614 .with_ordering(vec!["name".to_string(), "-created_at".to_string()]);
615
616 assert_eq!(config.ordering().len(), 2);
617 }
618}