ferro_cli/deploy/
env_production.rs1use std::fs;
8use std::path::Path;
9
10pub fn read_env_production_keys(path: &Path) -> anyhow::Result<Vec<String>> {
13 let content = fs::read_to_string(path)
14 .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", path.display()))?;
15 let mut keys = Vec::new();
16 for raw in content.lines() {
17 let line = raw.trim();
18 if line.is_empty() || line.starts_with('#') {
19 continue;
20 }
21 if let Some((k, _)) = line.split_once('=') {
22 let key = k.trim();
23 if !key.is_empty() {
24 keys.push(key.to_string());
25 }
26 }
27 }
28 Ok(keys)
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum EnvLine {
35 Key(String),
36 Blank,
37 Comment,
38}
39
40pub fn parse_env_example_structured(contents: &str) -> Vec<EnvLine> {
46 let mut out = Vec::new();
47 for raw in contents.lines() {
48 let line = raw.trim_end();
49 if line.trim().is_empty() {
50 out.push(EnvLine::Blank);
51 continue;
52 }
53 let trimmed = line.trim_start();
54 if trimmed.starts_with('#') {
55 out.push(EnvLine::Comment);
56 continue;
57 }
58 if let Some(eq) = trimmed.find('=') {
59 let key = trimmed[..eq].trim().to_string();
60 if !key.is_empty() {
61 out.push(EnvLine::Key(key));
62 }
63 }
64 }
65 out
66}
67
68#[cfg(test)]
69mod structured_tests {
70 use super::*;
71
72 #[test]
73 fn env_example_parser_preserves_order() {
74 let input = "Z=1\nA=2\nM=3\n";
75 let out = parse_env_example_structured(input);
76 let keys: Vec<_> = out
77 .iter()
78 .filter_map(|l| match l {
79 EnvLine::Key(k) => Some(k.as_str()),
80 _ => None,
81 })
82 .collect();
83 assert_eq!(keys, vec!["Z", "A", "M"]);
84 }
85
86 #[test]
87 fn env_example_parser_preserves_blank_separators() {
88 let input = "A=1\n\nB=2\n";
89 let out = parse_env_example_structured(input);
90 assert_eq!(
91 out,
92 vec![
93 EnvLine::Key("A".into()),
94 EnvLine::Blank,
95 EnvLine::Key("B".into())
96 ]
97 );
98 }
99
100 #[test]
101 fn env_example_parser_skips_comments() {
102 let input = "# header\nA=1\n";
103 let out = parse_env_example_structured(input);
104 assert!(out.iter().any(|l| matches!(l, EnvLine::Key(k) if k == "A")));
105 }
106
107 #[test]
108 fn env_example_parser_trims_keys() {
109 let input = " KEY =val\n";
110 let out = parse_env_example_structured(input);
111 assert!(out
112 .iter()
113 .any(|l| matches!(l, EnvLine::Key(k) if k == "KEY")));
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::fs;
121 use tempfile::TempDir;
122
123 fn write_env(content: &str) -> (TempDir, std::path::PathBuf) {
124 let tmp = TempDir::new().unwrap();
125 let path = tmp.path().join(".env.production");
126 fs::write(&path, content).unwrap();
127 (tmp, path)
128 }
129
130 #[test]
131 fn extracts_keys_in_order() {
132 let (_tmp, path) = write_env("KEY=value\nOTHER=x\n");
133 assert_eq!(
134 read_env_production_keys(&path).unwrap(),
135 vec!["KEY", "OTHER"]
136 );
137 }
138
139 #[test]
140 fn skips_blank_and_comment_lines() {
141 let (_tmp, path) = write_env("\n# a comment\n # indented comment\nA=1\n\nB=2\n");
142 assert_eq!(read_env_production_keys(&path).unwrap(), vec!["A", "B"]);
143 }
144
145 #[test]
146 fn trims_whitespace_around_keys() {
147 let (_tmp, path) = write_env(" KEY = value \n");
148 assert_eq!(read_env_production_keys(&path).unwrap(), vec!["KEY"]);
149 }
150
151 #[test]
152 fn skips_lines_without_equals() {
153 let (_tmp, path) = write_env("not-a-kv-line\nA=1\n");
154 assert_eq!(read_env_production_keys(&path).unwrap(), vec!["A"]);
155 }
156
157 #[test]
158 fn missing_file_errors() {
159 let tmp = TempDir::new().unwrap();
160 let missing = tmp.path().join(".env.production");
161 assert!(read_env_production_keys(&missing).is_err());
162 }
163}