envx_core/
importer.rs

1use color_eyre::Result;
2use regex::Regex;
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy)]
8pub enum ImportFormat {
9    DotEnv,
10    Json,
11    Yaml,
12    Text,
13}
14
15impl ImportFormat {
16    /// Determines the import format based on file extension.
17    ///
18    /// # Errors
19    ///
20    /// This function currently never returns an error, but uses `Result` for future extensibility.
21    pub fn from_extension(path: &str) -> Result<Self> {
22        let ext = Path::new(path).extension().and_then(|s| s.to_str()).unwrap_or("");
23
24        match ext.to_lowercase().as_str() {
25            "env" => Ok(Self::DotEnv),
26            "json" => Ok(Self::Json),
27            "yaml" | "yml" => Ok(Self::Yaml),
28            "txt" | "text" => Ok(Self::Text),
29            _ => {
30                // Check if filename is .env or similar
31                let filename = Path::new(path).file_name().and_then(|s| s.to_str()).unwrap_or("");
32
33                if filename.starts_with('.') && filename.contains("env") {
34                    Ok(Self::DotEnv)
35                } else {
36                    Ok(Self::Text) // Default to text format
37                }
38            }
39        }
40    }
41}
42
43#[derive(Debug, Clone, Default)]
44pub struct Importer {
45    variables: HashMap<String, String>,
46}
47
48impl Importer {
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Imports environment variables from a file in the specified format.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if:
59    /// - The file cannot be read (file not found, permission denied, etc.)
60    /// - The file content cannot be parsed in the specified format (e.g., invalid JSON syntax)
61    pub fn import_from_file(&mut self, path: &str, format: ImportFormat) -> Result<()> {
62        let content = fs::read_to_string(path)?;
63
64        match format {
65            ImportFormat::DotEnv => self.parse_dotenv(&content),
66            ImportFormat::Json => self.parse_json(&content)?,
67            ImportFormat::Yaml => self.parse_yaml(&content),
68            ImportFormat::Text => self.parse_text(&content),
69        }
70
71        Ok(())
72    }
73
74    #[must_use]
75    pub fn get_variables(&self) -> Vec<(String, String)> {
76        self.variables.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
77    }
78
79    pub fn filter_by_patterns(&mut self, patterns: &[String]) {
80        let mut matched = HashMap::new();
81
82        for pattern in patterns {
83            let regex_pattern = if pattern.contains('*') || pattern.contains('?') {
84                wildcard_to_regex(pattern)
85            } else {
86                format!("^{}$", regex::escape(pattern))
87            };
88
89            if let Ok(re) = Regex::new(&regex_pattern) {
90                for (key, value) in &self.variables {
91                    if re.is_match(key) {
92                        matched.insert(key.clone(), value.clone());
93                    }
94                }
95            }
96        }
97
98        self.variables = matched;
99    }
100
101    pub fn add_prefix(&mut self, prefix: &str) {
102        let mut prefixed = HashMap::new();
103
104        for (key, value) in self.variables.drain() {
105            prefixed.insert(format!("{prefix}{key}"), value);
106        }
107
108        self.variables = prefixed;
109    }
110
111    fn parse_dotenv(&mut self, content: &str) {
112        for line in content.lines() {
113            let line = line.trim();
114
115            // Skip empty lines and comments
116            if line.is_empty() || line.starts_with('#') {
117                continue;
118            }
119
120            // Parse KEY=VALUE or KEY="VALUE"
121            if let Some(eq_pos) = line.find('=') {
122                let key = line[..eq_pos].trim();
123                let value = line[eq_pos + 1..].trim();
124
125                // Validate key
126                if key.is_empty() || key.contains(' ') {
127                    continue;
128                }
129
130                // Process value
131                let processed_value = if (value.starts_with('"') && value.ends_with('"'))
132                    || (value.starts_with('\'') && value.ends_with('\''))
133                {
134                    // Remove quotes and unescape
135                    let unquoted = &value[1..value.len() - 1];
136
137                    // Process escape sequences properly
138                    Self::unescape_string(unquoted)
139                } else {
140                    // Remove inline comments (but not # in values)
141                    if let Some(comment_pos) = value.find(" #") {
142                        value[..comment_pos].trim().to_string()
143                    } else {
144                        value.to_string()
145                    }
146                };
147
148                self.variables.insert(key.to_string(), processed_value);
149            }
150        }
151    }
152
153    fn unescape_string(input: &str) -> String {
154        let mut result = String::new();
155        let mut chars = input.chars().peekable();
156
157        while let Some(ch) = chars.next() {
158            if ch == '\\' {
159                match chars.peek() {
160                    Some('\\') => {
161                        result.push('\\');
162                        chars.next(); // consume the second backslash
163                    }
164                    Some('n') => {
165                        result.push('\n');
166                        chars.next(); // consume the 'n'
167                    }
168                    Some('r') => {
169                        result.push('\r');
170                        chars.next(); // consume the 'r'
171                    }
172                    Some('t') => {
173                        result.push('\t');
174                        chars.next(); // consume the 't'
175                    }
176                    Some('"') => {
177                        result.push('"');
178                        chars.next(); // consume the quote
179                    }
180                    Some('\'') => {
181                        result.push('\'');
182                        chars.next(); // consume the single quote
183                    }
184                    _ => {
185                        // Unknown escape sequence, keep the backslash
186                        result.push('\\');
187                    }
188                }
189            } else {
190                result.push(ch);
191            }
192        }
193
194        result
195    }
196
197    fn parse_json(&mut self, content: &str) -> Result<()> {
198        let parsed: serde_json::Value = serde_json::from_str(content)?;
199
200        // Handle both simple object and structured format
201        if let Some(obj) = parsed.as_object() {
202            // Check if it's a structured export with metadata
203            if obj.contains_key("variables") {
204                if let Some(vars) = obj["variables"].as_array() {
205                    for var in vars {
206                        if let (Some(name), Some(value)) = (
207                            var.get("name").and_then(|v| v.as_str()),
208                            var.get("value").and_then(|v| v.as_str()),
209                        ) {
210                            self.variables.insert(name.to_string(), value.to_string());
211                        }
212                    }
213                }
214            } else {
215                // Simple key-value format
216                for (key, value) in obj {
217                    if let Some(val_str) = value.as_str() {
218                        self.variables.insert(key.clone(), val_str.to_string());
219                    }
220                }
221            }
222        }
223
224        Ok(())
225    }
226
227    fn parse_yaml(&mut self, content: &str) {
228        // Simple YAML parser for key: value pairs
229        let mut skip_remaining = false;
230
231        for line in content.lines() {
232            let line = line.trim();
233
234            // Skip empty lines and comments
235            if line.is_empty() || line.starts_with('#') {
236                continue;
237            }
238
239            // Stop processing after document separator
240            if line == "---" {
241                skip_remaining = true;
242                continue;
243            }
244
245            // Skip all content after document separator
246            if skip_remaining {
247                continue;
248            }
249
250            // Parse key: value
251            if let Some(colon_pos) = line.find(':') {
252                let key = line[..colon_pos].trim();
253                let value = line[colon_pos + 1..].trim();
254
255                // Remove quotes if present
256                let processed_value = if (value.starts_with('"') && value.ends_with('"'))
257                    || (value.starts_with('\'') && value.ends_with('\''))
258                {
259                    value[1..value.len() - 1].to_string()
260                } else {
261                    value.to_string()
262                };
263
264                self.variables.insert(key.to_string(), processed_value);
265            }
266        }
267    }
268
269    fn parse_text(&mut self, content: &str) {
270        // Same as dotenv but more lenient
271        self.parse_dotenv(content);
272    }
273}
274
275fn wildcard_to_regex(pattern: &str) -> String {
276    let mut regex = String::new();
277    regex.push('^');
278
279    for ch in pattern.chars() {
280        match ch {
281            '*' => regex.push_str(".*"),
282            '?' => regex.push('.'),
283            '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
284                regex.push('\\');
285                regex.push(ch);
286            }
287            _ => regex.push(ch),
288        }
289    }
290
291    regex.push('$');
292    regex
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use std::io::Write;
299    use tempfile::NamedTempFile;
300
301    // Helper function to create a temporary file with content
302    fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
303        let mut file = NamedTempFile::new().unwrap();
304        if !extension.is_empty() {
305            file = NamedTempFile::with_suffix(extension).unwrap();
306        }
307        file.write_all(content.as_bytes()).unwrap();
308        file.flush().unwrap();
309        file
310    }
311
312    #[test]
313    fn test_import_format_from_extension() {
314        assert!(matches!(
315            ImportFormat::from_extension("file.env").unwrap(),
316            ImportFormat::DotEnv
317        ));
318        assert!(matches!(
319            ImportFormat::from_extension("file.ENV").unwrap(),
320            ImportFormat::DotEnv
321        ));
322        assert!(matches!(
323            ImportFormat::from_extension("file.json").unwrap(),
324            ImportFormat::Json
325        ));
326        assert!(matches!(
327            ImportFormat::from_extension("file.JSON").unwrap(),
328            ImportFormat::Json
329        ));
330        assert!(matches!(
331            ImportFormat::from_extension("file.yaml").unwrap(),
332            ImportFormat::Yaml
333        ));
334        assert!(matches!(
335            ImportFormat::from_extension("file.yml").unwrap(),
336            ImportFormat::Yaml
337        ));
338        assert!(matches!(
339            ImportFormat::from_extension("file.txt").unwrap(),
340            ImportFormat::Text
341        ));
342        assert!(matches!(
343            ImportFormat::from_extension("file.text").unwrap(),
344            ImportFormat::Text
345        ));
346
347        // Default to Text for unknown extensions
348        assert!(matches!(
349            ImportFormat::from_extension("file.xyz").unwrap(),
350            ImportFormat::Text
351        ));
352        assert!(matches!(
353            ImportFormat::from_extension("file").unwrap(),
354            ImportFormat::Text
355        ));
356
357        // Special case for .env files
358        assert!(matches!(
359            ImportFormat::from_extension(".env").unwrap(),
360            ImportFormat::DotEnv
361        ));
362        assert!(matches!(
363            ImportFormat::from_extension(".env.local").unwrap(),
364            ImportFormat::DotEnv
365        ));
366        assert!(matches!(
367            ImportFormat::from_extension(".env.production").unwrap(),
368            ImportFormat::DotEnv
369        ));
370    }
371
372    #[test]
373    fn test_parse_dotenv_basic() {
374        let mut importer = Importer::new();
375        let content = r#"
376# This is a comment
377KEY1=value1
378KEY2=value2
379KEY3=value with spaces
380
381# Another comment
382KEY4="quoted value"
383KEY5='single quoted'
384"#;
385
386        importer.parse_dotenv(content);
387        let vars = importer.get_variables();
388        let vars_map: HashMap<_, _> = vars.into_iter().collect();
389
390        assert_eq!(vars_map.get("KEY1").unwrap(), "value1");
391        assert_eq!(vars_map.get("KEY2").unwrap(), "value2");
392        assert_eq!(vars_map.get("KEY3").unwrap(), "value with spaces");
393        assert_eq!(vars_map.get("KEY4").unwrap(), "quoted value");
394        assert_eq!(vars_map.get("KEY5").unwrap(), "single quoted");
395    }
396
397    #[test]
398    fn test_parse_dotenv_with_escapes() {
399        let mut importer = Importer::new();
400        let content = r#"
401ESCAPED="line1\nline2\ttab"
402BACKSLASH="path\\to\\file"
403DOUBLE_BACKSLASH="path\\\\to\\\\file"
404QUOTE="He said \"hello\""
405"#;
406
407        importer.parse_dotenv(content);
408        let vars = importer.get_variables();
409        let vars_map: HashMap<_, _> = vars.into_iter().collect();
410
411        assert_eq!(vars_map.get("ESCAPED").unwrap(), "line1\nline2\ttab");
412        assert_eq!(vars_map.get("BACKSLASH").unwrap(), "path\\to\\file");
413        assert_eq!(vars_map.get("DOUBLE_BACKSLASH").unwrap(), "path\\\\to\\\\file");
414        assert_eq!(vars_map.get("QUOTE").unwrap(), "He said \"hello\"");
415    }
416
417    #[test]
418    fn test_parse_dotenv_inline_comments() {
419        let mut importer = Importer::new();
420        let content = r#"
421KEY1=value1 # This is an inline comment
422KEY2=value#notacomment
423KEY3=value # comment
424KEY4="value # not a comment in quotes"
425"#;
426
427        importer.parse_dotenv(content);
428        let vars = importer.get_variables();
429        let vars_map: HashMap<_, _> = vars.into_iter().collect();
430
431        assert_eq!(vars_map.get("KEY1").unwrap(), "value1");
432        assert_eq!(vars_map.get("KEY2").unwrap(), "value#notacomment");
433        assert_eq!(vars_map.get("KEY3").unwrap(), "value");
434        assert_eq!(vars_map.get("KEY4").unwrap(), "value # not a comment in quotes");
435    }
436
437    #[test]
438    fn test_parse_dotenv_edge_cases() {
439        let mut importer = Importer::new();
440        let content = r"
441# Empty value
442EMPTY=
443# No spaces around equals
444COMPACT=value
445# Spaces in key should be ignored
446INVALID KEY=value
447# Key with spaces is invalid
448KEY WITH SPACES=value
449# Just equals sign
450=value
451# No equals sign
452NOEQUALS
453# Multiple equals signs
454KEY=value=with=equals
455# Unicode values
456UNICODE=こんにちは
457# Special characters
458SPECIAL=!@#$%^&*()
459";
460
461        importer.parse_dotenv(content);
462        let vars = importer.get_variables();
463        let vars_map: HashMap<_, _> = vars.into_iter().collect();
464
465        assert_eq!(vars_map.get("EMPTY").unwrap(), "");
466        assert_eq!(vars_map.get("COMPACT").unwrap(), "value");
467        assert!(!vars_map.contains_key("INVALID KEY"));
468        assert!(!vars_map.contains_key("KEY WITH SPACES"));
469        assert!(!vars_map.contains_key("NOEQUALS"));
470        assert_eq!(vars_map.get("KEY").unwrap(), "value=with=equals");
471        assert_eq!(vars_map.get("UNICODE").unwrap(), "こんにちは");
472        assert_eq!(vars_map.get("SPECIAL").unwrap(), "!@#$%^&*()");
473    }
474
475    #[test]
476    fn test_parse_json_simple() {
477        let mut importer = Importer::new();
478        let content = r#"{
479            "KEY1": "value1",
480            "KEY2": "value2",
481            "KEY3": "value with spaces"
482        }"#;
483
484        importer.parse_json(content).unwrap();
485        let vars = importer.get_variables();
486        let vars_map: HashMap<_, _> = vars.into_iter().collect();
487
488        assert_eq!(vars_map.get("KEY1").unwrap(), "value1");
489        assert_eq!(vars_map.get("KEY2").unwrap(), "value2");
490        assert_eq!(vars_map.get("KEY3").unwrap(), "value with spaces");
491    }
492
493    #[test]
494    fn test_parse_json_structured() {
495        let mut importer = Importer::new();
496        let content = r#"{
497            "exported_at": "2024-01-01T00:00:00Z",
498            "count": 3,
499            "variables": [
500                {"name": "KEY1", "value": "value1"},
501                {"name": "KEY2", "value": "value2"},
502                {"name": "KEY3", "value": "value3"}
503            ]
504        }"#;
505
506        importer.parse_json(content).unwrap();
507        let vars = importer.get_variables();
508        let vars_map: HashMap<_, _> = vars.into_iter().collect();
509
510        assert_eq!(vars_map.len(), 3);
511        assert_eq!(vars_map.get("KEY1").unwrap(), "value1");
512        assert_eq!(vars_map.get("KEY2").unwrap(), "value2");
513        assert_eq!(vars_map.get("KEY3").unwrap(), "value3");
514    }
515
516    #[test]
517    fn test_parse_json_invalid() {
518        let mut importer = Importer::new();
519        let content = "not valid json";
520
521        assert!(importer.parse_json(content).is_err());
522    }
523
524    #[test]
525    fn test_parse_json_non_string_values() {
526        let mut importer = Importer::new();
527        let content = r#"{
528            "STRING": "value",
529            "NUMBER": 42,
530            "BOOLEAN": true,
531            "NULL": null,
532            "ARRAY": [1, 2, 3],
533            "OBJECT": {"nested": "value"}
534        }"#;
535
536        importer.parse_json(content).unwrap();
537        let vars = importer.get_variables();
538        let vars_map: HashMap<_, _> = vars.into_iter().collect();
539
540        // Only string values should be imported
541        assert_eq!(vars_map.len(), 1);
542        assert_eq!(vars_map.get("STRING").unwrap(), "value");
543    }
544
545    #[test]
546    fn test_parse_yaml_basic() {
547        let mut importer = Importer::new();
548        let content = r"
549# YAML comment
550KEY1: value1
551KEY2: value2
552KEY3: value with spaces
553---
554KEY4: after document marker
555";
556
557        importer.parse_yaml(content);
558        let vars = importer.get_variables();
559        let vars_map: HashMap<_, _> = vars.into_iter().collect();
560
561        assert_eq!(vars_map.get("KEY1").unwrap(), "value1");
562        assert_eq!(vars_map.get("KEY2").unwrap(), "value2");
563        assert_eq!(vars_map.get("KEY3").unwrap(), "value with spaces");
564        assert!(!vars_map.contains_key("KEY4")); // After --- should be ignored
565    }
566
567    #[test]
568    fn test_parse_yaml_quoted() {
569        let mut importer = Importer::new();
570        let content = r#"
571KEY1: "quoted value"
572KEY2: 'single quoted'
573KEY3: "value: with colon"
574KEY4: unquoted: with colon
575"#;
576
577        importer.parse_yaml(content);
578        let vars = importer.get_variables();
579        let vars_map: HashMap<_, _> = vars.into_iter().collect();
580
581        assert_eq!(vars_map.get("KEY1").unwrap(), "quoted value");
582        assert_eq!(vars_map.get("KEY2").unwrap(), "single quoted");
583        assert_eq!(vars_map.get("KEY3").unwrap(), "value: with colon");
584        assert_eq!(vars_map.get("KEY4").unwrap(), "unquoted: with colon");
585    }
586
587    #[test]
588    fn test_parse_yaml_edge_cases() {
589        let mut importer = Importer::new();
590        let content = r"
591# Empty value
592EMPTY:
593EMPTY2: 
594# No space after colon
595COMPACT:value
596# Multiple colons
597URL: http://example.com:8080
598# Special characters
599SPECIAL: !@#$%^&*()
600";
601
602        importer.parse_yaml(content);
603        let vars = importer.get_variables();
604        let vars_map: HashMap<_, _> = vars.into_iter().collect();
605
606        assert_eq!(vars_map.get("EMPTY").unwrap(), "");
607        assert_eq!(vars_map.get("EMPTY2").unwrap(), "");
608        assert_eq!(vars_map.get("COMPACT").unwrap(), "value");
609        assert_eq!(vars_map.get("URL").unwrap(), "http://example.com:8080");
610        assert_eq!(vars_map.get("SPECIAL").unwrap(), "!@#$%^&*()");
611    }
612
613    #[test]
614    fn test_import_from_file_dotenv() {
615        let content = "KEY1=value1\nKEY2=value2";
616        let file = create_temp_file(content, ".env");
617
618        let mut importer = Importer::new();
619        importer
620            .import_from_file(file.path().to_str().unwrap(), ImportFormat::DotEnv)
621            .unwrap();
622
623        let vars = importer.get_variables();
624        assert_eq!(vars.len(), 2);
625    }
626
627    #[test]
628    fn test_import_from_file_auto_detect() {
629        // Test .env file
630        let env_file = create_temp_file("KEY=value", ".env");
631        let mut importer = Importer::new();
632        let format = ImportFormat::from_extension(env_file.path().to_str().unwrap()).unwrap();
633        importer
634            .import_from_file(env_file.path().to_str().unwrap(), format)
635            .unwrap();
636        assert_eq!(importer.get_variables().len(), 1);
637
638        // Test .json file
639        let json_file = create_temp_file(r#"{"KEY": "value"}"#, ".json");
640        let mut importer = Importer::new();
641        let format = ImportFormat::from_extension(json_file.path().to_str().unwrap()).unwrap();
642        importer
643            .import_from_file(json_file.path().to_str().unwrap(), format)
644            .unwrap();
645        assert_eq!(importer.get_variables().len(), 1);
646    }
647
648    #[test]
649    fn test_filter_by_patterns_exact() {
650        let mut importer = Importer::new();
651        importer.variables.insert("KEY1".to_string(), "value1".to_string());
652        importer.variables.insert("KEY2".to_string(), "value2".to_string());
653        importer.variables.insert("OTHER".to_string(), "value3".to_string());
654
655        importer.filter_by_patterns(&["KEY1".to_string(), "KEY2".to_string()]);
656
657        let vars = importer.get_variables();
658        let vars_map: HashMap<_, _> = vars.into_iter().collect();
659
660        assert_eq!(vars_map.len(), 2);
661        assert!(vars_map.contains_key("KEY1"));
662        assert!(vars_map.contains_key("KEY2"));
663        assert!(!vars_map.contains_key("OTHER"));
664    }
665
666    #[test]
667    fn test_filter_by_patterns_wildcard() {
668        let mut importer = Importer::new();
669        importer.variables.insert("API_KEY".to_string(), "value1".to_string());
670        importer
671            .variables
672            .insert("API_SECRET".to_string(), "value2".to_string());
673        importer
674            .variables
675            .insert("DATABASE_URL".to_string(), "value3".to_string());
676        importer.variables.insert("OTHER".to_string(), "value4".to_string());
677
678        importer.filter_by_patterns(&["API_*".to_string()]);
679
680        let vars = importer.get_variables();
681        let vars_map: HashMap<_, _> = vars.into_iter().collect();
682
683        assert_eq!(vars_map.len(), 2);
684        assert!(vars_map.contains_key("API_KEY"));
685        assert!(vars_map.contains_key("API_SECRET"));
686        assert!(!vars_map.contains_key("DATABASE_URL"));
687    }
688
689    #[test]
690    fn test_filter_by_patterns_question_mark() {
691        let mut importer = Importer::new();
692        importer.variables.insert("KEY1".to_string(), "value1".to_string());
693        importer.variables.insert("KEY2".to_string(), "value2".to_string());
694        importer.variables.insert("KEY10".to_string(), "value3".to_string());
695        importer.variables.insert("OTHER".to_string(), "value4".to_string());
696
697        importer.filter_by_patterns(&["KEY?".to_string()]);
698
699        let vars = importer.get_variables();
700        let vars_map: HashMap<_, _> = vars.into_iter().collect();
701
702        assert_eq!(vars_map.len(), 2);
703        assert!(vars_map.contains_key("KEY1"));
704        assert!(vars_map.contains_key("KEY2"));
705        assert!(!vars_map.contains_key("KEY10")); // ? matches exactly one character
706    }
707
708    #[test]
709    fn test_filter_by_patterns_multiple() {
710        let mut importer = Importer::new();
711        importer.variables.insert("API_KEY".to_string(), "value1".to_string());
712        importer.variables.insert("DB_HOST".to_string(), "value2".to_string());
713        importer.variables.insert("DB_PORT".to_string(), "value3".to_string());
714        importer.variables.insert("OTHER".to_string(), "value4".to_string());
715
716        importer.filter_by_patterns(&["API_*".to_string(), "DB_*".to_string()]);
717
718        let vars = importer.get_variables();
719        assert_eq!(vars.len(), 3);
720    }
721
722    #[test]
723    fn test_add_prefix() {
724        let mut importer = Importer::new();
725        importer.variables.insert("KEY1".to_string(), "value1".to_string());
726        importer.variables.insert("KEY2".to_string(), "value2".to_string());
727
728        importer.add_prefix("PREFIX_");
729
730        let vars = importer.get_variables();
731        let vars_map: HashMap<_, _> = vars.into_iter().collect();
732
733        assert_eq!(vars_map.len(), 2);
734        assert!(vars_map.contains_key("PREFIX_KEY1"));
735        assert!(vars_map.contains_key("PREFIX_KEY2"));
736        assert!(!vars_map.contains_key("KEY1"));
737        assert!(!vars_map.contains_key("KEY2"));
738        assert_eq!(vars_map.get("PREFIX_KEY1").unwrap(), "value1");
739        assert_eq!(vars_map.get("PREFIX_KEY2").unwrap(), "value2");
740    }
741
742    #[test]
743    fn test_add_prefix_empty() {
744        let mut importer = Importer::new();
745        importer.variables.insert("KEY1".to_string(), "value1".to_string());
746
747        importer.add_prefix("");
748
749        let vars = importer.get_variables();
750        let vars_map: HashMap<_, _> = vars.into_iter().collect();
751
752        assert!(vars_map.contains_key("KEY1"));
753        assert_eq!(vars_map.get("KEY1").unwrap(), "value1");
754    }
755
756    #[test]
757    fn test_wildcard_to_regex() {
758        // Test asterisk wildcard
759        let regex = wildcard_to_regex("API_*");
760        assert_eq!(regex, "^API_.*$");
761
762        // Test question mark wildcard
763        let regex = wildcard_to_regex("KEY?");
764        assert_eq!(regex, "^KEY.$");
765
766        // Test special regex characters are escaped
767        let regex = wildcard_to_regex("KEY.VALUE");
768        assert_eq!(regex, "^KEY\\.VALUE$");
769
770        let regex = wildcard_to_regex("KEY[1]");
771        assert_eq!(regex, "^KEY\\[1\\]$");
772
773        // Test combination
774        let regex = wildcard_to_regex("*_KEY_?");
775        assert_eq!(regex, "^.*_KEY_.$");
776    }
777
778    #[test]
779    fn test_complex_import_workflow() {
780        // Create a complex .env file
781        let content = r#"
782# Database configuration
783DB_HOST=localhost
784DB_PORT=5432
785DB_USER=admin
786DB_PASSWORD="secret password"
787
788# API configuration
789API_KEY=abc123
790API_SECRET=xyz789
791API_URL=https://api.example.com
792
793# Feature flags
794FEATURE_NEW_UI=true
795FEATURE_BETA=false
796
797# Paths
798APP_PATH=/usr/local/app
799LOG_PATH=/var/log/app
800"#;
801
802        let file = create_temp_file(content, ".env");
803
804        let mut importer = Importer::new();
805        importer
806            .import_from_file(file.path().to_str().unwrap(), ImportFormat::DotEnv)
807            .unwrap();
808
809        // Test initial import
810        assert_eq!(importer.get_variables().len(), 11);
811
812        // Filter to only DB variables
813        importer.filter_by_patterns(&["DB_*".to_string()]);
814        assert_eq!(importer.get_variables().len(), 4);
815
816        // Add prefix
817        importer.add_prefix("TEST_");
818
819        let vars = importer.get_variables();
820        let vars_map: HashMap<_, _> = vars.into_iter().collect();
821
822        assert!(vars_map.contains_key("TEST_DB_HOST"));
823        assert!(vars_map.contains_key("TEST_DB_PORT"));
824        assert!(vars_map.contains_key("TEST_DB_USER"));
825        assert!(vars_map.contains_key("TEST_DB_PASSWORD"));
826        assert_eq!(vars_map.get("TEST_DB_PASSWORD").unwrap(), "secret password");
827    }
828
829    #[test]
830    fn test_parse_text_format() {
831        let mut importer = Importer::new();
832        // Text format should behave like dotenv
833        let content = "KEY1=value1\nKEY2=value2";
834
835        importer.parse_text(content);
836        let vars = importer.get_variables();
837
838        assert_eq!(vars.len(), 2);
839    }
840
841    #[test]
842    fn test_empty_content() {
843        let mut importer = Importer::new();
844
845        importer.parse_dotenv("");
846        assert_eq!(importer.get_variables().len(), 0);
847
848        importer.parse_json("{}").unwrap();
849        assert_eq!(importer.get_variables().len(), 0);
850
851        importer.parse_yaml("");
852        assert_eq!(importer.get_variables().len(), 0);
853    }
854
855    #[test]
856    fn test_file_not_found() {
857        let mut importer = Importer::new();
858        let result = importer.import_from_file("/non/existent/file.env", ImportFormat::DotEnv);
859        assert!(result.is_err());
860    }
861}