ao_core/
parity_config_validation.rs1use std::collections::{HashMap, HashSet};
12use std::path::Path;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TsProjectConfig {
16 pub repo: String,
17 pub path: String,
18 pub default_branch: String,
19 pub session_prefix: Option<String>,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct TsOrchestratorConfig {
24 pub projects: HashMap<String, TsProjectConfig>,
25}
26
27pub fn generate_session_prefix(project_id: &str) -> String {
28 if project_id.len() <= 4 {
29 return project_id.to_lowercase();
30 }
31
32 let uppercase: Vec<char> = project_id
33 .chars()
34 .filter(|c| c.is_ascii_uppercase())
35 .collect();
36 if uppercase.len() > 1 {
37 return uppercase.into_iter().collect::<String>().to_lowercase();
38 }
39
40 if project_id.contains('-') || project_id.contains('_') {
41 let sep = if project_id.contains('-') { '-' } else { '_' };
42 return project_id
43 .split(sep)
44 .filter(|w| !w.is_empty())
45 .filter_map(|w| w.chars().next())
46 .collect::<String>()
47 .to_lowercase();
48 }
49
50 project_id
51 .chars()
52 .take(3)
53 .collect::<String>()
54 .to_lowercase()
55}
56
57pub fn validate_project_uniqueness(config: &TsOrchestratorConfig) -> Result<(), String> {
58 let mut basenames: HashSet<String> = HashSet::new();
59 let mut basename_to_paths: HashMap<String, Vec<String>> = HashMap::new();
60
61 for project in config.projects.values() {
62 let basename = Path::new(&project.path)
63 .file_name()
64 .and_then(|s| s.to_str())
65 .unwrap_or("")
66 .to_string();
67
68 basename_to_paths
69 .entry(basename.clone())
70 .or_default()
71 .push(project.path.clone());
72
73 if basenames.contains(&basename) {
74 let paths = basename_to_paths
75 .get(&basename)
76 .cloned()
77 .unwrap_or_default()
78 .join(", ");
79 return Err(format!(
80 "Duplicate project ID detected: \"{basename}\"\nMultiple projects have the same directory basename:\n {paths}\n\nTo fix this, ensure each project path has a unique directory name.\nAlternatively, you can use the config key as a unique identifier."
81 ));
82 }
83 basenames.insert(basename);
84 }
85
86 let mut prefixes: HashSet<String> = HashSet::new();
87 let mut prefix_to_project_key: HashMap<String, String> = HashMap::new();
88
89 for (config_key, project) in config.projects.iter() {
90 let basename = Path::new(&project.path)
91 .file_name()
92 .and_then(|s| s.to_str())
93 .unwrap_or("")
94 .to_string();
95 let prefix = project
96 .session_prefix
97 .clone()
98 .unwrap_or_else(|| generate_session_prefix(&basename));
99
100 if prefixes.contains(&prefix) {
101 let first = prefix_to_project_key
102 .get(&prefix)
103 .cloned()
104 .unwrap_or_default();
105 let first_path = config
106 .projects
107 .get(&first)
108 .map(|p| p.path.clone())
109 .unwrap_or_default();
110 return Err(format!(
111 "Duplicate session prefix detected: \"{prefix}\"\nProjects \"{first}\" and \"{config_key}\" would generate the same prefix.\n\nTo fix this, add an explicit sessionPrefix to one of these projects:\n\nprojects:\n {first}:\n path: {first_path}\n sessionPrefix: {prefix}1 # Add explicit prefix\n {config_key}:\n path: {path}\n sessionPrefix: {prefix}2 # Add explicit prefix\n",
112 path = project.path
113 ));
114 }
115
116 prefixes.insert(prefix.clone());
117 prefix_to_project_key.insert(prefix, config_key.clone());
118 }
119
120 Ok(())
121}