1use crate::error::{CliError, CliResult};
8use std::path::{Path, PathBuf};
9
10pub fn resolve_env_file(
16 explicit: Option<&Path>,
17 no_env_file: bool,
18 dir: &Path,
19) -> CliResult<Option<PathBuf>> {
20 if no_env_file {
21 return Ok(None);
22 }
23 if let Some(p) = explicit {
24 if !p.exists() {
25 return Err(CliError::EnvFileNotFound {
26 path: p.to_path_buf(),
27 });
28 }
29 return Ok(Some(p.to_path_buf()));
30 }
31 let candidate = dir.join(".env");
32 Ok(candidate.exists().then_some(candidate))
33}
34
35pub fn load_env_file_if_present(path: Option<&Path>) -> CliResult<()> {
39 if let Some(p) = path {
40 dotenvy::from_path(p).map_err(|source| CliError::ReadConfig {
41 path: p.to_path_buf(),
42 source: std::io::Error::other(source),
43 })?;
44 }
45 Ok(())
46}
47
48pub fn discover_config_path(dir: &Path) -> Option<PathBuf> {
53 const CANDIDATES: &[&str] = &["faucet.yaml", "faucet.yml", "faucet.json"];
54 let mut chosen: Option<PathBuf> = None;
55 let mut also: Vec<PathBuf> = Vec::new();
56 for name in CANDIDATES {
57 let p = dir.join(name);
58 if p.exists() {
59 if chosen.is_none() {
60 chosen = Some(p);
61 } else {
62 also.push(p);
63 }
64 }
65 }
66 if !also.is_empty()
67 && let Some(ref c) = chosen
68 {
69 let extras: Vec<String> = also.iter().map(|p| p.display().to_string()).collect();
70 tracing::warn!(
71 chosen = %c.display(),
72 also_present = ?extras,
73 "multiple faucet.* config files in cwd; picking the first in priority order (yaml > yml > json)"
74 );
75 }
76 chosen
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use tempfile::tempdir;
83
84 #[test]
85 fn resolve_env_file_explicit_wins() {
86 let dir = tempdir().unwrap();
87 let p = dir.path().join("secrets.env");
88 std::fs::write(&p, "X=1\n").unwrap();
89 let resolved = resolve_env_file(Some(&p), false, dir.path()).unwrap();
90 assert_eq!(resolved, Some(p));
91 }
92
93 #[test]
94 fn resolve_env_file_explicit_missing_errors() {
95 let dir = tempdir().unwrap();
96 let missing = dir.path().join("nope.env");
97 let err = resolve_env_file(Some(&missing), false, dir.path()).unwrap_err();
98 assert!(matches!(err, CliError::EnvFileNotFound { .. }));
99 }
100
101 #[test]
102 fn resolve_env_file_auto_finds_dotenv_in_cwd() {
103 let dir = tempdir().unwrap();
104 let dotenv = dir.path().join(".env");
105 std::fs::write(&dotenv, "X=1\n").unwrap();
106 assert_eq!(
107 resolve_env_file(None, false, dir.path()).unwrap(),
108 Some(dotenv)
109 );
110 }
111
112 #[test]
113 fn resolve_env_file_none_when_missing_and_not_explicit() {
114 let dir = tempdir().unwrap();
115 assert_eq!(resolve_env_file(None, false, dir.path()).unwrap(), None);
116 }
117
118 #[test]
119 fn resolve_env_file_no_env_file_skips_auto_discovery() {
120 let dir = tempdir().unwrap();
121 std::fs::write(dir.path().join(".env"), "X=1\n").unwrap();
122 assert_eq!(resolve_env_file(None, true, dir.path()).unwrap(), None);
123 }
124
125 #[test]
126 fn resolve_env_file_no_env_file_also_skips_explicit_path() {
127 let dir = tempdir().unwrap();
130 let p = dir.path().join("explicit.env");
131 std::fs::write(&p, "X=1\n").unwrap();
132 assert_eq!(resolve_env_file(Some(&p), true, dir.path()).unwrap(), None);
133 }
134
135 #[test]
136 fn discover_config_path_prefers_yaml() {
137 let dir = tempdir().unwrap();
138 std::fs::write(dir.path().join("faucet.yaml"), "version: 1\n").unwrap();
139 std::fs::write(dir.path().join("faucet.yml"), "version: 1\n").unwrap();
140 std::fs::write(dir.path().join("faucet.json"), "{}").unwrap();
141 let found = discover_config_path(dir.path());
142 assert_eq!(found, Some(dir.path().join("faucet.yaml")));
143 }
144
145 #[test]
146 fn discover_config_path_falls_through_to_yml() {
147 let dir = tempdir().unwrap();
148 std::fs::write(dir.path().join("faucet.yml"), "v: 1\n").unwrap();
149 std::fs::write(dir.path().join("faucet.json"), "{}").unwrap();
150 assert_eq!(
151 discover_config_path(dir.path()),
152 Some(dir.path().join("faucet.yml"))
153 );
154 }
155
156 #[test]
157 fn discover_config_path_falls_through_to_json() {
158 let dir = tempdir().unwrap();
159 std::fs::write(dir.path().join("faucet.json"), "{}").unwrap();
160 assert_eq!(
161 discover_config_path(dir.path()),
162 Some(dir.path().join("faucet.json"))
163 );
164 }
165
166 #[test]
167 fn discover_config_path_none_when_missing() {
168 let dir = tempdir().unwrap();
169 assert_eq!(discover_config_path(dir.path()), None);
170 }
171
172 #[test]
173 fn load_env_file_if_present_is_noop_for_none() {
174 load_env_file_if_present(None).unwrap();
175 }
176
177 #[test]
178 fn load_env_file_if_present_propagates_parse_failure_as_readconfig() {
179 let dir = tempdir().unwrap();
181 let p = dir.path().join("bad.env");
182 std::fs::write(&p, "no equals here\n").unwrap();
183 let err = load_env_file_if_present(Some(&p)).unwrap_err();
184 assert!(matches!(err, CliError::ReadConfig { .. }));
185 }
186}