ravenrs_report_export/
lib.rs1use ravenrs_report::Document;
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23#[non_exhaustive]
24pub enum ExportFormat {
25 Pdf,
27 Xlsx,
29 Csv,
31}
32
33impl ExportFormat {
34 pub fn extension(self) -> &'static str {
36 match self {
37 Self::Pdf => "pdf",
38 Self::Xlsx => "xlsx",
39 Self::Csv => "csv",
40 }
41 }
42
43 pub fn mime(self) -> &'static str {
45 match self {
46 Self::Pdf => "application/pdf",
47 Self::Xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
48 Self::Csv => "text/csv; charset=utf-8",
49 }
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct ExportedFile {
56 pub format: ExportFormat,
58 pub name: String,
60 pub mime: &'static str,
62 pub bytes: Vec<u8>,
64}
65
66pub fn export(
68 document: &Document,
69 format: ExportFormat,
70 suffix: Option<&str>,
71) -> Result<ExportedFile, String> {
72 let bytes = match format {
73 ExportFormat::Pdf => ravenrs_report_pdf::to_pdf(document).map_err(|e| e.to_string())?,
74 ExportFormat::Xlsx => ravenrs_report_xlsx::to_xlsx(document).map_err(|e| e.to_string())?,
75 ExportFormat::Csv => format!("\u{feff}{}", ravenrs_report_xlsx::to_csv(document)).into_bytes(),
77 };
78 Ok(ExportedFile { format, name: file_name(&document.title, suffix, format), mime: format.mime(), bytes })
79}
80
81pub fn file_name(title: &str, suffix: Option<&str>, format: ExportFormat) -> String {
83 let clean: String = title
84 .chars()
85 .map(|c| if c.is_control() || r#"<>:"/\|?*"#.contains(c) { ' ' } else { c })
86 .take(100)
87 .collect();
88 let clean = clean.trim().trim_matches('.').trim();
89 let stem = if clean.is_empty() { "report" } else { clean };
90 match suffix {
91 Some(suffix) => format!("{stem} {suffix}.{}", format.extension()),
92 None => format!("{stem}.{}", format.extension()),
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn file_names_drop_what_file_systems_refuse() {
102 assert_eq!(
103 file_name("Vendas: Sul/Norte", Some("2026-11-01"), ExportFormat::Pdf),
104 "Vendas Sul Norte 2026-11-01.pdf"
105 );
106 assert_eq!(file_name("Relatório.", None, ExportFormat::Xlsx), "Relatório.xlsx");
107 assert_eq!(file_name("../..", None, ExportFormat::Csv), "report.csv");
108 assert_eq!(file_name(" ?? ", None, ExportFormat::Csv), "report.csv");
109 }
110
111 #[test]
112 fn each_format_gives_a_file_of_its_kind() -> Result<(), String> {
113 let document = Document { title: "T".to_owned(), ..Document::default() };
114 assert!(export(&document, ExportFormat::Pdf, None)?.bytes.starts_with(b"%PDF-"));
115 assert!(export(&document, ExportFormat::Xlsx, None)?.bytes.starts_with(b"PK"));
116 assert!(export(&document, ExportFormat::Csv, None)?.bytes.starts_with("\u{feff}".as_bytes()));
117 Ok(())
118 }
119
120 #[test]
121 fn formats_read_as_lowercase_names() {
122 assert_eq!(serde_json::from_str::<ExportFormat>("\"pdf\"").ok(), Some(ExportFormat::Pdf));
123 assert!(serde_json::from_str::<ExportFormat>("\"docx\"").is_err());
124 }
125}