dotenv_space/core/
converter.rs1use anyhow::Result;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub enum KeyTransform {
12 Uppercase,
14 Lowercase,
16 CamelCase,
18 SnakeCase,
20}
21
22#[derive(Debug, Clone, Default)]
26pub struct ConvertOptions {
27 pub include_pattern: Option<String>,
29
30 pub exclude_pattern: Option<String>,
32
33 pub base64: bool,
35
36 pub prefix: Option<String>,
38
39 pub transform: Option<KeyTransform>,
41}
42
43impl ConvertOptions {
46 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn filter_vars(&self, vars: &HashMap<String, String>) -> HashMap<String, String> {
53 vars.iter()
54 .filter(|(key, _)| self.should_include(key))
55 .map(|(k, v)| (k.clone(), v.clone()))
56 .collect()
57 }
58
59 fn should_include(&self, key: &str) -> bool {
61 if let Some(ref exclude) = self.exclude_pattern {
63 if glob_match(key, exclude) {
64 return false;
65 }
66 }
67
68 if let Some(ref include) = self.include_pattern {
70 return glob_match(key, include);
71 }
72
73 true
75 }
76
77 pub fn transform_key(&self, key: &str) -> String {
79 let key = if let Some(ref prefix) = self.prefix {
80 format!("{}{}", prefix, key)
81 } else {
82 key.to_string()
83 };
84
85 match self.transform {
86 Some(KeyTransform::Uppercase) => key.to_uppercase(),
87 Some(KeyTransform::Lowercase) => key.to_lowercase(),
88 Some(KeyTransform::CamelCase) => to_camel_case(&key),
89 Some(KeyTransform::SnakeCase) => to_snake_case(&key),
90 None => key,
91 }
92 }
93
94 pub fn transform_value(&self, value: &str) -> String {
96 if self.base64 {
97 use base64::{engine::general_purpose, Engine as _};
98 general_purpose::STANDARD.encode(value.as_bytes())
99 } else {
100 value.to_string()
101 }
102 }
103}
104
105pub trait Converter {
107 fn convert(&self, vars: &HashMap<String, String>, options: &ConvertOptions) -> Result<String>;
118
119 fn name(&self) -> &str;
123
124 fn description(&self) -> &str;
128}
129
130fn glob_match(text: &str, pattern: &str) -> bool {
136 if pattern == "*" {
137 return true;
138 }
139
140 if pattern.starts_with('*') && pattern.ends_with('*') {
141 let substring = &pattern[1..pattern.len() - 1];
142 return text.contains(substring);
143 }
144
145 if let Some(suffix) = pattern.strip_prefix('*') {
155 return text.ends_with(suffix);
156 }
157
158 if let Some(prefix) = pattern.strip_suffix('*') {
159 return text.starts_with(prefix);
160 }
161
162 text == pattern
163}
164
165fn to_camel_case(s: &str) -> String {
167 let parts: Vec<&str> = s.split('_').collect();
168 if parts.is_empty() {
169 return String::new();
170 }
171
172 let mut result = parts[0].to_lowercase();
173 for part in &parts[1..] {
174 if !part.is_empty() {
175 let mut chars = part.chars();
176 if let Some(first) = chars.next() {
177 result.push_str(&first.to_uppercase().to_string());
178 result.push_str(&chars.as_str().to_lowercase());
179 }
180 }
181 }
182 result
183}
184
185fn to_snake_case(s: &str) -> String {
207 let mut result = String::with_capacity(s.len());
208 let chars: Vec<char> = s.chars().collect();
209
210 for i in 0..chars.len() {
211 let ch = chars[i];
212
213 if ch.is_uppercase() {
214 if i > 0 {
215 let prev = chars[i - 1];
216 let next = chars.get(i + 1);
217
218 if prev.is_lowercase()
219 || (prev.is_uppercase() && next.map(|c| c.is_lowercase()).unwrap_or(false))
220 {
221 result.push('_');
222 }
223 }
224 result.extend(ch.to_lowercase());
225 } else {
226 result.push(ch);
227 }
228 }
229
230 result
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_default_options() {
239 let opts = ConvertOptions::default();
240 assert!(opts.include_pattern.is_none());
241 assert!(opts.exclude_pattern.is_none());
242 assert!(!opts.base64);
243 assert!(opts.prefix.is_none());
244 assert!(opts.transform.is_none());
245 }
246
247 #[test]
248 fn test_new_options() {
249 let opts = ConvertOptions::new();
250 assert!(opts.include_pattern.is_none());
251 }
252
253 #[test]
254 fn test_filter_vars_no_pattern() {
255 let mut vars = HashMap::new();
256 vars.insert("KEY1".to_string(), "value1".to_string());
257 vars.insert("KEY2".to_string(), "value2".to_string());
258
259 let opts = ConvertOptions::default();
260 let filtered = opts.filter_vars(&vars);
261
262 assert_eq!(filtered.len(), 2);
263 assert!(filtered.contains_key("KEY1"));
264 assert!(filtered.contains_key("KEY2"));
265 }
266
267 #[test]
268 fn test_filter_vars_include() {
269 let mut vars = HashMap::new();
270 vars.insert("AWS_KEY".to_string(), "value1".to_string());
271 vars.insert("DB_KEY".to_string(), "value2".to_string());
272
273 let mut opts = ConvertOptions::default();
274 opts.include_pattern = Some("AWS_*".to_string());
275 let filtered = opts.filter_vars(&vars);
276
277 assert_eq!(filtered.len(), 1);
278 assert!(filtered.contains_key("AWS_KEY"));
279 assert!(!filtered.contains_key("DB_KEY"));
280 }
281
282 #[test]
283 fn test_filter_vars_exclude() {
284 let mut vars = HashMap::new();
285 vars.insert("KEY1".to_string(), "value1".to_string());
286 vars.insert("KEY2_LOCAL".to_string(), "value2".to_string());
287
288 let mut opts = ConvertOptions::default();
289 opts.exclude_pattern = Some("*_LOCAL".to_string());
290 let filtered = opts.filter_vars(&vars);
291
292 assert_eq!(filtered.len(), 1);
293 assert!(filtered.contains_key("KEY1"));
294 assert!(!filtered.contains_key("KEY2_LOCAL"));
295 }
296
297 #[test]
298 fn test_transform_key_prefix() {
299 let mut opts = ConvertOptions::default();
300 opts.prefix = Some("APP_".to_string());
301
302 assert_eq!(opts.transform_key("DATABASE_URL"), "APP_DATABASE_URL");
303 }
304
305 #[test]
306 fn test_transform_key_uppercase() {
307 let mut opts = ConvertOptions::default();
308 opts.transform = Some(KeyTransform::Uppercase);
309
310 assert_eq!(opts.transform_key("database_url"), "DATABASE_URL");
311 }
312
313 #[test]
314 fn test_transform_key_lowercase() {
315 let mut opts = ConvertOptions::default();
316 opts.transform = Some(KeyTransform::Lowercase);
317
318 assert_eq!(opts.transform_key("DATABASE_URL"), "database_url");
319 }
320
321 #[test]
322 fn test_transform_value_base64() {
323 let mut opts = ConvertOptions::default();
324 opts.base64 = true;
325
326 let result = opts.transform_value("hello");
327 assert_eq!(result, "aGVsbG8="); }
329
330 #[test]
331 fn test_glob_match() {
332 assert!(glob_match("AWS_KEY", "AWS_*"));
333 assert!(glob_match("MY_AWS_KEY", "*_AWS_*"));
334 assert!(glob_match("KEY_LOCAL", "*_LOCAL"));
335 assert!(glob_match("anything", "*"));
336 assert!(!glob_match("AWS_KEY", "DB_*"));
337 }
338
339 #[test]
340 fn test_to_camel_case() {
341 assert_eq!(to_camel_case("database_url"), "databaseUrl");
342 assert_eq!(to_camel_case("secret_key"), "secretKey");
343 assert_eq!(to_camel_case("aws_access_key_id"), "awsAccessKeyId");
344 }
345
346 #[test]
347 fn test_to_snake_case() {
348 assert_eq!(to_snake_case("DatabaseURL"), "database_url");
349 assert_eq!(to_snake_case("SecretKey"), "secret_key");
350 assert_eq!(to_snake_case("HTTPServer"), "http_server");
351 assert_eq!(to_snake_case("UserID"), "user_id");
352 assert_eq!(to_snake_case("MySQLDatabase"), "my_sql_database");
353 assert_eq!(to_snake_case("JSONParser"), "json_parser");
354 assert_eq!(to_snake_case("Already_snake"), "already_snake");
355 }
357}