ferro_cli/doctor/checks/
local_env_parity.rs1use crate::deploy::parse_env_entries;
4use crate::doctor::check::{CheckResult, DoctorCheck};
5use std::collections::HashSet;
6use std::fs;
7use std::path::Path;
8
9pub struct LocalEnvParityCheck;
10
11const NAME: &str = "local_env_parity";
12
13impl DoctorCheck for LocalEnvParityCheck {
14 fn name(&self) -> &'static str {
15 NAME
16 }
17 fn run(&self, root: &Path) -> CheckResult {
18 check_impl(root)
19 }
20}
21
22pub(crate) fn check_impl(root: &Path) -> CheckResult {
23 let example_path = root.join(".env.example");
24 let env_path = root.join(".env");
25 let example_exists = example_path.is_file();
26 let env_exists = env_path.is_file();
27
28 if !example_exists && !env_exists {
29 return CheckResult::warn(NAME, ".env and .env.example both missing");
30 }
31 if !example_exists {
32 return CheckResult::warn(NAME, ".env.example missing");
33 }
34 if !env_exists {
35 return CheckResult::error(NAME, ".env missing — required by .env.example")
36 .with_details("Copy .env.example to .env and fill in real values");
37 }
38
39 let example = fs::read_to_string(&example_path).unwrap_or_default();
40 let env = fs::read_to_string(&env_path).unwrap_or_default();
41
42 let example_keys: Vec<String> = parse_env_entries(&example)
43 .into_iter()
44 .map(|e| e.key)
45 .collect();
46 let env_keys: HashSet<String> = parse_env_entries(&env).into_iter().map(|e| e.key).collect();
47
48 let missing: Vec<&String> = example_keys
49 .iter()
50 .filter(|k| !env_keys.contains(*k))
51 .collect();
52
53 if missing.is_empty() {
54 CheckResult::ok(NAME, format!("{} keys present", example_keys.len()))
55 } else {
56 let list = missing
57 .iter()
58 .map(|s| s.as_str())
59 .collect::<Vec<_>>()
60 .join(", ");
61 CheckResult::error(NAME, format!("{} key(s) missing from .env", missing.len()))
62 .with_details(format!("missing: {list}"))
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use tempfile::TempDir;
70
71 fn write(root: &Path, name: &str, content: &str) {
72 fs::write(root.join(name), content).unwrap();
73 }
74
75 #[test]
76 fn name_is_local_env_parity() {
77 assert_eq!(LocalEnvParityCheck.name(), "local_env_parity");
78 }
79
80 #[test]
81 fn matching_keys_returns_ok() {
82 let tmp = TempDir::new().unwrap();
83 write(tmp.path(), ".env.example", "KEY1=\nKEY2=\n");
84 write(tmp.path(), ".env", "KEY1=a\nKEY2=b\n");
85 let r = check_impl(tmp.path());
86 assert_eq!(r.status, crate::doctor::check::CheckStatus::Ok);
87 }
88
89 #[test]
90 fn missing_key_returns_error_with_details() {
91 let tmp = TempDir::new().unwrap();
92 write(tmp.path(), ".env.example", "KEY1=\nKEY2=\n");
93 write(tmp.path(), ".env", "KEY1=a\n");
94 let r = check_impl(tmp.path());
95 assert_eq!(r.status, crate::doctor::check::CheckStatus::Error);
96 assert!(r.details.unwrap().contains("KEY2"));
97 }
98}