flowscope_export/
naming.rs1use chrono::{DateTime, Utc};
2
3use crate::ExportFormat;
4
5#[derive(Debug, Clone)]
6pub struct ExportNaming {
7 project_name: String,
8 exported_at: DateTime<Utc>,
9}
10
11impl ExportNaming {
12 pub fn new(project_name: impl Into<String>) -> Self {
13 Self::with_exported_at(project_name, Utc::now())
14 }
15
16 pub fn with_exported_at(project_name: impl Into<String>, exported_at: DateTime<Utc>) -> Self {
17 let mut name = project_name.into();
18 if name.trim().is_empty() {
19 name = "lineage".to_string();
20 }
21 Self {
22 project_name: sanitize_project_name(&name),
23 exported_at,
24 }
25 }
26
27 pub fn exported_at(&self) -> DateTime<Utc> {
28 self.exported_at
29 }
30
31 pub fn project_name(&self) -> &str {
32 &self.project_name
33 }
34
35 pub fn filename(&self, format: ExportFormat) -> String {
36 let timestamp = self.exported_at.format("%Y%m%d-%H%M%S");
37 let (suffix, extension) = format_filename_parts(format);
38 format!(
39 "{}-{}-{}.{}",
40 self.project_name, timestamp, suffix, extension
41 )
42 }
43}
44
45fn format_filename_parts(format: ExportFormat) -> (&'static str, &'static str) {
46 match format {
47 ExportFormat::Json { .. } => ("json", "json"),
48 ExportFormat::Mermaid { view } => match view {
49 crate::MermaidView::All => ("mermaid", "md"),
50 crate::MermaidView::Script => ("mermaid-script", "md"),
51 crate::MermaidView::Table => ("mermaid-table", "md"),
52 crate::MermaidView::Column => ("mermaid-column", "md"),
53 crate::MermaidView::Hybrid => ("mermaid-hybrid", "md"),
54 },
55 ExportFormat::Html => ("report", "html"),
56 ExportFormat::Sql { .. } => ("duckdb", "sql"),
57 ExportFormat::CsvBundle => ("csv", "zip"),
58 ExportFormat::Xlsx => ("xlsx", "xlsx"),
59 ExportFormat::DuckDb => ("duckdb", "duckdb"),
60 ExportFormat::Png => ("png", "png"),
61 }
62}
63
64fn sanitize_project_name(name: &str) -> String {
65 let mut cleaned = String::new();
66 let mut last_dash = false;
67
68 for ch in name.trim().chars() {
69 let normalized = ch.to_ascii_lowercase();
70 if normalized.is_ascii_alphanumeric() {
71 cleaned.push(normalized);
72 last_dash = false;
73 } else if matches!(normalized, '-' | '_' | ' ') && !last_dash {
74 cleaned.push('-');
75 last_dash = true;
76 }
77 }
78
79 let cleaned = cleaned.trim_matches('-').to_string();
80 if cleaned.is_empty() {
81 "lineage".to_string()
82 } else {
83 cleaned
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use chrono::{TimeZone, Utc};
90
91 use super::*;
92
93 #[test]
94 fn test_filename_generation() {
95 let timestamp = Utc.with_ymd_and_hms(2026, 1, 18, 12, 30, 5).unwrap();
96 let naming = ExportNaming::with_exported_at("FlowScope Demo", timestamp);
97 let name = naming.filename(ExportFormat::Json { compact: false });
98 assert_eq!(name, "flowscope-demo-20260118-123005-json.json");
99 }
100
101 #[test]
102 fn test_project_name_sanitization() {
103 let naming = ExportNaming::with_exported_at(" !!! ", Utc::now());
104 assert_eq!(naming.project_name(), "lineage");
105 }
106}