Skip to main content

reinhardt_admin/core/
export.rs

1//! Export functionality for admin data
2//!
3//! This module provides export capabilities for admin data in various formats
4//! including CSV, JSON, and Excel.
5
6use crate::types::{AdminError, AdminResult};
7use csv::Writer;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Export format
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ExportFormat {
14	/// Comma-separated values
15	CSV,
16	/// JSON format
17	JSON,
18	/// Excel format (XLSX)
19	Excel,
20	/// Tab-separated values
21	TSV,
22	/// XML format
23	XML,
24}
25
26impl ExportFormat {
27	/// Get file extension for this format
28	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	/// Get MIME type for this format
39	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/// Export configuration
53///
54/// # Examples
55///
56/// ```
57/// use reinhardt_admin::core::ExportConfig;
58/// use reinhardt_admin::core::export::ExportFormat;
59///
60/// let config = ExportConfig::new("User", ExportFormat::CSV)
61///     .with_field("id")
62///     .with_field("username")
63///     .with_field("email");
64///
65/// assert_eq!(config.model_name(), "User");
66/// assert_eq!(config.field_count(), 3);
67/// ```
68#[derive(Debug, Clone)]
69pub struct ExportConfig {
70	/// Model name
71	model_name: String,
72	/// Export format
73	format: ExportFormat,
74	/// Fields to export (empty means all fields)
75	fields: Vec<String>,
76	/// Field labels (for headers)
77	field_labels: HashMap<String, String>,
78	/// Filter conditions
79	filters: HashMap<String, String>,
80	/// Sort order
81	ordering: Vec<String>,
82	/// Maximum number of rows to export
83	max_rows: Option<usize>,
84	/// Include column headers
85	include_headers: bool,
86}
87
88impl ExportConfig {
89	/// Create a new export configuration
90	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	/// Get model name
104	pub fn model_name(&self) -> &str {
105		&self.model_name
106	}
107
108	/// Get export format
109	pub fn format(&self) -> ExportFormat {
110		self.format
111	}
112
113	/// Add a field to export
114	pub fn with_field(mut self, field: impl Into<String>) -> Self {
115		self.fields.push(field.into());
116		self
117	}
118
119	/// Set fields to export
120	pub fn with_fields(mut self, fields: Vec<String>) -> Self {
121		self.fields = fields;
122		self
123	}
124
125	/// Get fields
126	pub fn fields(&self) -> &[String] {
127		&self.fields
128	}
129
130	/// Get field count
131	pub fn field_count(&self) -> usize {
132		self.fields.len()
133	}
134
135	/// Set field label
136	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	/// Get field label
142	pub fn get_field_label(&self, field: &str) -> Option<&String> {
143		self.field_labels.get(field)
144	}
145
146	/// Add a filter
147	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	/// Get filters
153	pub fn filters(&self) -> &HashMap<String, String> {
154		&self.filters
155	}
156
157	/// Set ordering
158	pub fn with_ordering(mut self, ordering: Vec<String>) -> Self {
159		self.ordering = ordering;
160		self
161	}
162
163	/// Get ordering
164	pub fn ordering(&self) -> &[String] {
165		&self.ordering
166	}
167
168	/// Set maximum rows
169	pub fn with_max_rows(mut self, max: usize) -> Self {
170		self.max_rows = Some(max);
171		self
172	}
173
174	/// Get maximum rows
175	pub fn max_rows(&self) -> Option<usize> {
176		self.max_rows
177	}
178
179	/// Set whether to include headers
180	pub fn with_headers(mut self, include: bool) -> Self {
181		self.include_headers = include;
182		self
183	}
184
185	/// Check if headers should be included
186	pub fn include_headers(&self) -> bool {
187		self.include_headers
188	}
189}
190
191/// Export result
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ExportResult {
194	/// Exported data as bytes
195	pub data: Vec<u8>,
196	/// MIME type
197	pub mime_type: String,
198	/// Suggested filename
199	pub filename: String,
200	/// Number of rows exported
201	pub row_count: usize,
202}
203
204impl ExportResult {
205	/// Create a new export result
206	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	/// Get data size in bytes
221	pub fn size_bytes(&self) -> usize {
222		self.data.len()
223	}
224
225	/// Get data size in kilobytes
226	pub fn size_kb(&self) -> f64 {
227		self.data.len() as f64 / 1024.0
228	}
229}
230
231/// CSV exporter
232pub struct CsvExporter;
233
234impl CsvExporter {
235	/// Export data to CSV format
236	///
237	/// # Examples
238	///
239	/// ```
240	/// use reinhardt_admin::core::CsvExporter;
241	/// use std::collections::HashMap;
242	///
243	/// let fields = vec!["id".to_string(), "name".to_string()];
244	/// let mut row1 = HashMap::new();
245	/// row1.insert("id".to_string(), "1".to_string());
246	/// row1.insert("name".to_string(), "Alice".to_string());
247	///
248	/// let data = vec![row1];
249	/// let result = CsvExporter::export(&fields, &data, true);
250	///
251	/// assert!(result.is_ok());
252	/// ```
253	pub fn export(
254		fields: &[String],
255		data: &[HashMap<String, String>],
256		include_headers: bool,
257	) -> AdminResult<Vec<u8>> {
258		// Use csv crate for RFC 4180 compliant CSV writing
259		let mut writer = Writer::from_writer(Vec::new());
260
261		// Write headers
262		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		// Write data rows
269		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		// Flush and get the output
281		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
293/// JSON exporter
294pub struct JsonExporter;
295
296impl JsonExporter {
297	/// Export data to JSON format
298	///
299	/// # Examples
300	///
301	/// ```
302	/// use reinhardt_admin::core::JsonExporter;
303	/// use std::collections::HashMap;
304	///
305	/// let mut row1 = HashMap::new();
306	/// row1.insert("id".to_string(), "1".to_string());
307	/// row1.insert("name".to_string(), "Alice".to_string());
308	///
309	/// let data = vec![row1];
310	/// let result = JsonExporter::export(&data);
311	///
312	/// assert!(result.is_ok());
313	/// ```
314	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
320/// TSV (Tab-Separated Values) exporter
321pub struct TsvExporter;
322
323impl TsvExporter {
324	/// Export data to TSV format
325	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		// Write headers
333		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		// Write data rows
340		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
358/// Export builder for fluent API
359///
360/// # Examples
361///
362/// ```
363/// use reinhardt_admin::core::ExportBuilder;
364/// use reinhardt_admin::core::export::ExportFormat;
365/// use std::collections::HashMap;
366///
367/// let mut row = HashMap::new();
368/// row.insert("id".to_string(), "1".to_string());
369///
370/// let result = ExportBuilder::new("User", ExportFormat::CSV)
371///     .field("id")
372///     .field("username")
373///     .data(vec![row])
374///     .build();
375///
376/// assert!(result.is_ok());
377/// ```
378pub struct ExportBuilder {
379	config: ExportConfig,
380	data: Vec<HashMap<String, String>>,
381}
382
383impl ExportBuilder {
384	/// Create a new export builder
385	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	/// Add a field
393	pub fn field(mut self, field: impl Into<String>) -> Self {
394		self.config = self.config.with_field(field);
395		self
396	}
397
398	/// Add fields
399	pub fn fields(mut self, fields: Vec<String>) -> Self {
400		self.config = self.config.with_fields(fields);
401		self
402	}
403
404	/// Set field label
405	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	/// Set data
411	pub fn data(mut self, data: Vec<HashMap<String, String>>) -> Self {
412		self.data = data;
413		self
414	}
415
416	/// Set maximum rows
417	pub fn max_rows(mut self, max: usize) -> Self {
418		self.config = self.config.with_max_rows(max);
419		self
420	}
421
422	/// Build and export
423	pub fn build(self) -> AdminResult<ExportResult> {
424		let fields = if self.config.fields().is_empty() {
425			// Extract all unique field names from data
426			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}