1use serde::de::DeserializeOwned;
23use std::path::Path;
24
25#[derive(Debug, Clone)]
31pub struct Config {
32 content: String,
34 parsed: toml::Value,
36}
37
38impl Config {
39 #[must_use]
56 pub fn new(content: &str) -> Self {
57 let parsed =
58 toml::from_str(content).unwrap_or_else(|_| toml::Value::Table(toml::map::Map::new()));
59 Self {
60 content: content.to_string(),
61 parsed,
62 }
63 }
64
65 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
83 let content = std::fs::read_to_string(path.as_ref())
84 .map_err(|e| ConfigError::FileRead(format!("{}: {}", path.as_ref().display(), e)))?;
85 let parsed =
86 toml::from_str(&content).unwrap_or_else(|_| toml::Value::Table(toml::map::Map::new()));
87 Ok(Self { content, parsed })
88 }
89
90 pub fn parse<T: DeserializeOwned>(&self) -> Result<T, ConfigError> {
112 toml::from_str(&self.content).map_err(|e| ConfigError::Parse(e.to_string()))
113 }
114
115 #[must_use]
132 pub fn get<T: FromTomlValue>(&self, key: &str) -> Option<T> {
133 let mut current = &self.parsed;
134
135 for part in key.split('.') {
136 current = current.get(part)?;
137 }
138
139 T::from_toml_value(current)
140 }
141
142 #[must_use]
144 pub fn has_key(&self, key: &str) -> bool {
145 self.get::<toml::Value>(key).is_some()
146 }
147
148 #[must_use]
150 pub fn raw(&self) -> &str {
151 &self.content
152 }
153}
154
155#[derive(Debug, thiserror::Error)]
157pub enum ConfigError {
158 #[error("Failed to read config file: {0}")]
160 FileRead(String),
161
162 #[error("Failed to parse config: {0}")]
164 Parse(String),
165
166 #[error("Missing required config key: {0}")]
168 MissingKey(String),
169}
170
171pub trait FromTomlValue: Sized {
173 fn from_toml_value(value: &toml::Value) -> Option<Self>;
175}
176
177impl FromTomlValue for String {
178 fn from_toml_value(value: &toml::Value) -> Option<Self> {
179 value.as_str().map(Self::from)
180 }
181}
182
183impl FromTomlValue for i64 {
184 fn from_toml_value(value: &toml::Value) -> Option<Self> {
185 value.as_integer()
186 }
187}
188
189impl FromTomlValue for f64 {
190 fn from_toml_value(value: &toml::Value) -> Option<Self> {
191 value.as_float()
192 }
193}
194
195impl FromTomlValue for bool {
196 fn from_toml_value(value: &toml::Value) -> Option<Self> {
197 value.as_bool()
198 }
199}
200
201impl FromTomlValue for toml::Value {
202 fn from_toml_value(value: &toml::Value) -> Option<Self> {
203 Some(value.clone())
204 }
205}
206
207impl<T: FromTomlValue> FromTomlValue for Vec<T> {
208 fn from_toml_value(value: &toml::Value) -> Option<Self> {
209 value
210 .as_array()
211 .map(|arr| arr.iter().filter_map(T::from_toml_value).collect())
212 }
213}
214
215#[derive(Debug, Default)]
217pub struct ConfigBuilder {
218 values: toml::map::Map<String, toml::Value>,
219}
220
221impl ConfigBuilder {
222 #[must_use]
224 pub fn new() -> Self {
225 Self::default()
226 }
227
228 #[must_use]
230 pub fn set_string(mut self, key: &str, value: &str) -> Self {
231 self.values
232 .insert(key.to_string(), toml::Value::String(value.to_string()));
233 self
234 }
235
236 #[must_use]
238 pub fn set_int(mut self, key: &str, value: i64) -> Self {
239 self.values
240 .insert(key.to_string(), toml::Value::Integer(value));
241 self
242 }
243
244 #[must_use]
246 pub fn set_bool(mut self, key: &str, value: bool) -> Self {
247 self.values
248 .insert(key.to_string(), toml::Value::Boolean(value));
249 self
250 }
251
252 #[must_use]
254 pub fn build(self) -> Config {
255 let parsed = toml::Value::Table(self.values);
256 let content = toml::to_string_pretty(&parsed).unwrap_or_default();
257 Config { content, parsed }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use serde::Deserialize;
265
266 #[derive(Debug, Deserialize, PartialEq)]
267 struct TestConfig {
268 name: String,
269 port: u16,
270 }
271
272 #[test]
273 fn test_parse_config() {
274 let config = Config::new(
275 r#"
276 name = "test"
277 port = 8080
278 "#,
279 );
280 let parsed: TestConfig = config.parse().unwrap();
281 assert_eq!(parsed.name, "test");
282 assert_eq!(parsed.port, 8080);
283 }
284
285 #[test]
286 fn test_get_nested_key() {
287 let config = Config::new(
288 r#"
289 [server]
290 host = "localhost"
291 port = 3000
292 "#,
293 );
294 assert_eq!(
295 config.get::<String>("server.host"),
296 Some("localhost".into())
297 );
298 assert_eq!(config.get::<i64>("server.port"), Some(3000));
299 }
300
301 #[test]
302 fn test_get_array() {
303 let config = Config::new(
304 r#"
305 allowed_hosts = ["localhost", "127.0.0.1"]
306 ports = [8080, 8443]
307 "#,
308 );
309 assert_eq!(
310 config.get::<Vec<String>>("allowed_hosts"),
311 Some(vec!["localhost".to_string(), "127.0.0.1".to_string()])
312 );
313 assert_eq!(config.get::<Vec<i64>>("ports"), Some(vec![8080, 8443]));
314 assert_eq!(config.get::<Vec<String>>("missing"), None);
315 }
316
317 #[test]
318 fn test_config_builder() {
319 let config = ConfigBuilder::new()
320 .set_string("name", "app")
321 .set_int("port", 8080)
322 .set_bool("debug", true)
323 .build();
324
325 assert_eq!(config.get::<String>("name"), Some("app".into()));
326 assert_eq!(config.get::<i64>("port"), Some(8080));
327 assert_eq!(config.get::<bool>("debug"), Some(true));
328 }
329}