1use anyhow::{Context, Result};
2use serde::Deserialize;
3use std::env;
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Deserialize)]
8pub struct Config {
9 pub cloudflare_api_token: String,
10 pub cloudflare_zone_id: String,
11}
12
13impl Config {
14 pub fn load() -> Result<Self> {
19 if let Ok(config) = Self::load_from_file() {
21 config.validate()?;
22 return Ok(config);
23 }
24
25 dotenvy::dotenv().ok();
27
28 let config = Self::load_from_env()?;
30 config.validate()?;
31 Ok(config)
32 }
33
34 fn validate(&self) -> Result<()> {
36 if self.cloudflare_api_token.is_empty() {
37 anyhow::bail!("Cloudflare API token cannot be empty");
38 }
39
40 if self.cloudflare_zone_id.is_empty() {
41 anyhow::bail!("Cloudflare zone ID cannot be empty");
42 }
43
44 if !self
46 .cloudflare_zone_id
47 .chars()
48 .all(|c| c.is_ascii_hexdigit() || c == '-')
49 {
50 anyhow::bail!("Cloudflare zone ID appears to be in an invalid format");
51 }
52
53 if self.cloudflare_api_token.len() < 20 {
55 anyhow::bail!("Cloudflare API token appears to be in an invalid format (too short)");
56 }
57
58 Ok(())
59 }
60
61 fn load_from_file() -> Result<Self> {
62 let config_dir = dirs::home_dir()
63 .context("Could not determine home directory")?
64 .join(".config")
65 .join("cloudflaredns");
66
67 let config_path = config_dir.join("config.yaml");
68
69 if !config_path.exists() {
70 println!("Config file not found at {}", config_path.display());
71 anyhow::bail!("Config file not found at {}", config_path.display());
72 }
73
74 let content = fs::read_to_string(&config_path)
75 .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
76
77 let config: ConfigFile = serde_yaml::from_str(&content)
78 .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
79
80 Ok(Config {
81 cloudflare_api_token: config.cloudflare.api_token,
82 cloudflare_zone_id: config.cloudflare.zone_id,
83 })
84 }
85
86 pub fn load_from_env() -> Result<Self> {
89 let api_token = env::var("CLOUDFLARE_API_TOKEN").context(
90 "CLOUDFLARE_API_TOKEN not set in config file, .env, or environment variables",
91 )?;
92
93 let zone_id = env::var("CLOUDFLARE_ZONE_ID")
94 .context("CLOUDFLARE_ZONE_ID not set in config file, .env, or environment variables")?;
95
96 Ok(Config {
97 cloudflare_api_token: api_token,
98 cloudflare_zone_id: zone_id,
99 })
100 }
101
102 pub fn config_path() -> PathBuf {
104 dirs::home_dir()
105 .unwrap_or_default()
106 .join(".config")
107 .join("cloudflaredns")
108 .join("config.yaml")
109 }
110}
111
112#[derive(Debug, Deserialize)]
113struct ConfigFile {
114 cloudflare: CloudflareConfig,
115}
116
117#[derive(Debug, Deserialize)]
118struct CloudflareConfig {
119 api_token: String,
120 zone_id: String,
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use std::fs;
127 use std::io::Write;
128
129 fn create_temp_config_file(
130 api_token: &str,
131 zone_id: &str,
132 ) -> (tempfile::TempDir, std::path::PathBuf) {
133 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
134 let config_dir = temp_dir.path().join(".config").join("cloudflaredns");
135 fs::create_dir_all(&config_dir).expect("Failed to create config dir");
136
137 let config_path = config_dir.join("config.yaml");
138 let content = format!(
139 r#"cloudflare:
140 api_token: {}
141 zone_id: {}
142"#,
143 api_token, zone_id
144 );
145
146 let mut file = fs::File::create(&config_path).expect("Failed to create config file");
147 file.write_all(content.as_bytes())
148 .expect("Failed to write config");
149
150 (temp_dir, config_path)
151 }
152
153 #[test]
154 fn test_config_from_env() {
155 unsafe {
157 std::env::set_var("CLOUDFLARE_API_TOKEN", "test_api_token_1234567890abcdef");
158 std::env::set_var("CLOUDFLARE_ZONE_ID", "0123456789abcdef0123456789abcdef");
159 }
160
161 let config = Config::load_from_env().expect("Failed to load config from env");
162 assert_eq!(
163 config.cloudflare_api_token,
164 "test_api_token_1234567890abcdef"
165 );
166 assert_eq!(
167 config.cloudflare_zone_id,
168 "0123456789abcdef0123456789abcdef"
169 );
170
171 unsafe {
173 std::env::remove_var("CLOUDFLARE_API_TOKEN");
174 std::env::remove_var("CLOUDFLARE_ZONE_ID");
175 }
176 }
177
178 #[test]
179 fn test_config_from_env_missing_token() {
180 unsafe {
181 std::env::set_var("CLOUDFLARE_ZONE_ID", "0123456789abcdef0123456789abcdef");
182 std::env::remove_var("CLOUDFLARE_API_TOKEN");
183 }
184
185 let result = Config::load_from_env();
186 assert!(result.is_err());
187
188 unsafe {
189 std::env::remove_var("CLOUDFLARE_ZONE_ID");
190 }
191 }
192
193 #[test]
194 fn test_config_from_env_missing_zone_id() {
195 unsafe {
196 std::env::set_var("CLOUDFLARE_API_TOKEN", "test_api_token_1234567890abcdef");
197 std::env::remove_var("CLOUDFLARE_ZONE_ID");
198 }
199
200 let result = Config::load_from_env();
201 assert!(result.is_err());
202
203 unsafe {
204 std::env::remove_var("CLOUDFLARE_API_TOKEN");
205 }
206 }
207
208 #[test]
209 fn test_config_path_generation() {
210 let config_path = Config::config_path();
211 let path_str = config_path.to_string_lossy();
213 assert!(path_str.contains(".config/cloudflaredns/config.yaml"));
214 }
215
216 #[test]
217 fn test_config_file_parsing() {
218 let (temp_dir, config_path) = create_temp_config_file("file_token_abc", "file_zone_xyz");
219
220 let content = fs::read_to_string(&config_path).expect("Failed to read config file");
222 let config_file: ConfigFile =
223 serde_yaml::from_str(&content).expect("Failed to parse config");
224
225 assert_eq!(config_file.cloudflare.api_token, "file_token_abc");
226 assert_eq!(config_file.cloudflare.zone_id, "file_zone_xyz");
227
228 drop(temp_dir);
230 }
231
232 #[test]
233 fn test_config_file_not_found() {
234 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
236 let fake_config_dir = temp_dir.path().join("nonexistent").join("cloudflaredns");
237
238 let result = fs::read_to_string(fake_config_dir.join("config.yaml"));
240 assert!(result.is_err());
241 }
242
243 #[test]
244 fn test_config_yaml_invalid_format() {
245 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
246 let config_dir = temp_dir
247 .path()
248 .join("test_config_invalid")
249 .join("cloudflaredns");
250 fs::create_dir_all(&config_dir).expect("Failed to create config dir");
251 let config_path = config_dir.join("config.yaml");
252
253 let mut file = fs::File::create(&config_path).expect("Failed to create config file");
255 file.write_all(b"invalid: yaml: : content: [")
256 .expect("Failed to write invalid YAML");
257
258 let content = fs::read_to_string(&config_path).expect("Failed to read config file");
260 let result: Result<ConfigFile, _> = serde_yaml::from_str(&content);
261 assert!(result.is_err());
262 }
263}