1use crate::error::FaucetError;
33use serde::de::DeserializeOwned;
34use std::path::Path;
35
36pub fn load_json<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T, FaucetError> {
40 let path = path.as_ref();
41 let contents = std::fs::read_to_string(path).map_err(|e| {
42 FaucetError::Config(format!(
43 "failed to read config file '{}': {e}",
44 path.display()
45 ))
46 })?;
47 serde_json::from_str(&contents).map_err(|e| {
48 FaucetError::Config(format!(
49 "failed to parse JSON config from '{}': {e}",
50 path.display()
51 ))
52 })
53}
54
55pub fn load_env<T: DeserializeOwned>(prefix: &str) -> Result<T, FaucetError> {
64 envy::prefixed(format!("{prefix}_"))
65 .from_env()
66 .map_err(|e| FaucetError::Config(format!("failed to load config from env vars: {e}")))
67}
68
69pub fn load_env_file<T: DeserializeOwned>(
78 env_path: impl AsRef<Path>,
79 prefix: &str,
80) -> Result<T, FaucetError> {
81 let env_path = env_path.as_ref();
82 dotenvy::from_path(env_path).map_err(|e| {
83 FaucetError::Config(format!(
84 "failed to load .env file '{}': {e}",
85 env_path.display()
86 ))
87 })?;
88 load_env(prefix)
89}
90
91pub mod duration_secs {
96 use serde::{Deserialize, Deserializer, Serializer};
97 use std::time::Duration;
98
99 pub fn serialize<S: Serializer>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
100 serializer.serialize_u64(duration.as_secs())
101 }
102
103 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
104 let secs = u64::deserialize(deserializer)?;
105 Ok(Duration::from_secs(secs))
106 }
107}
108
109pub mod duration_secs_option {
111 use serde::{Deserialize, Deserializer, Serializer};
112 use std::time::Duration;
113
114 pub fn serialize<S: Serializer>(
115 duration: &Option<Duration>,
116 serializer: S,
117 ) -> Result<S::Ok, S::Error> {
118 match duration {
119 Some(d) => serializer.serialize_some(&d.as_secs()),
120 None => serializer.serialize_none(),
121 }
122 }
123
124 pub fn deserialize<'de, D: Deserializer<'de>>(
125 deserializer: D,
126 ) -> Result<Option<Duration>, D::Error> {
127 let opt = Option::<u64>::deserialize(deserializer)?;
128 Ok(opt.map(Duration::from_secs))
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use serde::Deserialize;
136 use std::io::Write;
137
138 #[derive(Debug, Deserialize, PartialEq)]
139 struct TestConfig {
140 url: String,
141 #[serde(default)]
142 batch_size: Option<usize>,
143 }
144
145 #[test]
146 fn load_json_works() {
147 let dir = std::env::temp_dir();
148 let path = dir.join("faucet_test_config.json");
149 let mut f = std::fs::File::create(&path).unwrap();
150 write!(f, r#"{{"url": "https://example.com", "batch_size": 100}}"#).unwrap();
151
152 let config: TestConfig = load_json(&path).unwrap();
153 assert_eq!(config.url, "https://example.com");
154 assert_eq!(config.batch_size, Some(100));
155
156 std::fs::remove_file(&path).ok();
157 }
158
159 #[test]
160 fn load_json_missing_file() {
161 let result = load_json::<TestConfig>("/nonexistent/path.json");
162 assert!(result.is_err());
163 assert!(result.unwrap_err().to_string().contains("failed to read"));
164 }
165
166 #[test]
167 fn load_json_invalid_json() {
168 let dir = std::env::temp_dir();
169 let path = dir.join("faucet_test_bad.json");
170 std::fs::write(&path, "not json").unwrap();
171
172 let result = load_json::<TestConfig>(&path);
173 assert!(result.is_err());
174 assert!(result.unwrap_err().to_string().contains("failed to parse"));
175
176 std::fs::remove_file(&path).ok();
177 }
178
179 #[test]
180 fn load_json_with_defaults() {
181 let dir = std::env::temp_dir();
182 let path = dir.join("faucet_test_defaults.json");
183 std::fs::write(&path, r#"{"url": "https://example.com"}"#).unwrap();
184
185 let config: TestConfig = load_json(&path).unwrap();
186 assert_eq!(config.url, "https://example.com");
187 assert_eq!(config.batch_size, None);
188
189 std::fs::remove_file(&path).ok();
190 }
191
192 #[test]
193 fn duration_secs_roundtrip() {
194 use std::time::Duration;
195
196 #[derive(Debug, serde::Serialize, Deserialize, PartialEq)]
197 struct D {
198 #[serde(with = "super::duration_secs")]
199 timeout: Duration,
200 }
201
202 let d = D {
203 timeout: Duration::from_secs(30),
204 };
205 let json = serde_json::to_string(&d).unwrap();
206 assert_eq!(json, r#"{"timeout":30}"#);
207
208 let parsed: D = serde_json::from_str(&json).unwrap();
209 assert_eq!(parsed, d);
210 }
211}