1use crate::core::error::PortError;
4use std::collections::BTreeMap;
5
6pub fn format_port_env_key(key: &str) -> String {
21 let sanitized: String = key
22 .chars()
23 .map(|c| if c.is_alphanumeric() { c } else { '_' })
24 .collect();
25 let upper = sanitized.to_uppercase();
26 if upper == "PORT" || upper.starts_with("PORT_") || upper.ends_with("_PORT") {
27 upper
28 } else {
29 format!("PORT_{}", upper)
30 }
31}
32
33pub fn calculate_effective_ports(
36 base_ports: &BTreeMap<String, u16>,
37 slot_id: u32,
38) -> Result<BTreeMap<String, u16>, PortError> {
39 let mut result = BTreeMap::new();
40 for (key, &base) in base_ports {
41 let total = (base as u32)
42 .checked_add(slot_id)
43 .ok_or_else(|| PortError::PortOverflow {
44 key: key.clone(),
45 base_port: base,
46 slot_id,
47 })?;
48
49 if total > u16::MAX as u32 {
50 return Err(PortError::PortOverflow {
51 key: key.clone(),
52 base_port: base,
53 slot_id,
54 });
55 }
56
57 result.insert(format_port_env_key(key), total as u16);
58 }
59 Ok(result)
60}
61
62pub fn is_shared_service_port(port_name: &str, shared_services: &[String]) -> bool {
78 let upper = port_name.to_ascii_uppercase();
79 for svc in shared_services {
80 let svc_upper = svc.to_ascii_uppercase();
81 if (svc_upper == "POSTGRES"
82 || svc_upper == "POSTGRESQL"
83 || svc_upper == "DB"
84 || svc_upper == "DATABASE")
85 && (upper.contains("DATABASE")
86 || upper.contains("POSTGRES")
87 || upper.contains("PGPORT")
88 || upper.contains("DB_")
89 || upper.starts_with("DB"))
90 {
91 return true;
92 }
93 if (svc_upper == "REDIS") && upper.contains("REDIS") {
94 return true;
95 }
96 if (svc_upper == "MYSQL" || svc_upper == "MARIADB")
97 && (upper.contains("MYSQL") || upper.contains("MARIADB"))
98 {
99 return true;
100 }
101 if (svc_upper == "MONGO" || svc_upper == "MONGODB") && upper.contains("MONGO") {
102 return true;
103 }
104 if (svc_upper == "RABBITMQ" || svc_upper == "AMQP")
105 && (upper.contains("RABBITMQ") || upper.contains("AMQP"))
106 {
107 return true;
108 }
109 }
110 false
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_format_port_env_key() {
119 assert_eq!(format_port_env_key("web"), "PORT_WEB");
120 assert_eq!(format_port_env_key("APP_PORT"), "APP_PORT");
121 assert_eq!(format_port_env_key("PORT"), "PORT");
122 assert_eq!(format_port_env_key("port_http"), "PORT_HTTP");
123 assert_eq!(format_port_env_key("frontend-app"), "PORT_FRONTEND_APP");
124 assert_eq!(format_port_env_key("db_primary"), "PORT_DB_PRIMARY");
125 }
126
127 #[test]
128 fn test_calculate_effective_ports_ok() {
129 let mut base_ports = BTreeMap::new();
130 base_ports.insert("web".to_string(), 3000);
131 base_ports.insert("api".to_string(), 8000);
132
133 let effective = calculate_effective_ports(&base_ports, 2).expect("calculates ports");
134 assert_eq!(effective.get("PORT_WEB"), Some(&3002));
135 assert_eq!(effective.get("PORT_API"), Some(&8002));
136 }
137
138 #[test]
139 fn test_calculate_effective_ports_overflow() {
140 let mut base_ports = BTreeMap::new();
141 base_ports.insert("overflow".to_string(), 65535);
142
143 let result = calculate_effective_ports(&base_ports, 1);
144 assert!(matches!(result, Err(PortError::PortOverflow { .. })));
145 }
146
147 #[test]
148 fn test_is_shared_service_port() {
149 let shared = vec!["postgres".to_string(), "redis".to_string()];
150 assert!(is_shared_service_port("DATABASE_PORT", &shared));
151 assert!(is_shared_service_port("TEST_DATABASE_PORT", &shared));
152 assert!(is_shared_service_port("DB_PORT", &shared));
153 assert!(is_shared_service_port("POSTGRES_PORT", &shared));
154 assert!(is_shared_service_port("PGPORT", &shared));
155 assert!(is_shared_service_port("REDIS_PORT", &shared));
156 assert!(!is_shared_service_port("APP_PORT", &shared));
157 assert!(!is_shared_service_port("FLOWER_PORT", &shared));
158 assert!(!is_shared_service_port("HTTP_PORT", &shared));
159 }
160}