1use serde::de::DeserializeOwned;
23use std::path::Path;
24
25#[derive(Debug, Clone)]
27pub struct Config {
28 content: String,
30}
31
32impl Config {
33 #[must_use]
50 pub fn new(content: &str) -> Self {
51 Self {
52 content: content.to_string(),
53 }
54 }
55
56 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
74 let content = std::fs::read_to_string(path.as_ref())
75 .map_err(|e| ConfigError::FileRead(format!("{}: {}", path.as_ref().display(), e)))?;
76 Ok(Self { content })
77 }
78
79 pub fn parse<T: DeserializeOwned>(&self) -> Result<T, ConfigError> {
101 toml::from_str(&self.content).map_err(|e| ConfigError::Parse(e.to_string()))
102 }
103
104 #[must_use]
121 pub fn get<T: FromTomlValue>(&self, key: &str) -> Option<T> {
122 let value: toml::Value = toml::from_str(&self.content).ok()?;
123 let mut current = &value;
124
125 for part in key.split('.') {
126 current = current.get(part)?;
127 }
128
129 T::from_toml_value(current)
130 }
131
132 #[must_use]
134 pub fn has_key(&self, key: &str) -> bool {
135 self.get::<toml::Value>(key).is_some()
136 }
137
138 #[must_use]
140 pub fn raw(&self) -> &str {
141 &self.content
142 }
143}
144
145#[derive(Debug, thiserror::Error)]
147pub enum ConfigError {
148 #[error("Failed to read config file: {0}")]
150 FileRead(String),
151
152 #[error("Failed to parse config: {0}")]
154 Parse(String),
155
156 #[error("Missing required config key: {0}")]
158 MissingKey(String),
159}
160
161pub trait FromTomlValue: Sized {
163 fn from_toml_value(value: &toml::Value) -> Option<Self>;
165}
166
167impl FromTomlValue for String {
168 fn from_toml_value(value: &toml::Value) -> Option<Self> {
169 value.as_str().map(String::from)
170 }
171}
172
173impl FromTomlValue for i64 {
174 fn from_toml_value(value: &toml::Value) -> Option<Self> {
175 value.as_integer()
176 }
177}
178
179impl FromTomlValue for f64 {
180 fn from_toml_value(value: &toml::Value) -> Option<Self> {
181 value.as_float()
182 }
183}
184
185impl FromTomlValue for bool {
186 fn from_toml_value(value: &toml::Value) -> Option<Self> {
187 value.as_bool()
188 }
189}
190
191impl FromTomlValue for toml::Value {
192 fn from_toml_value(value: &toml::Value) -> Option<Self> {
193 Some(value.clone())
194 }
195}
196
197#[derive(Debug, Default)]
199pub struct ConfigBuilder {
200 values: toml::map::Map<String, toml::Value>,
201}
202
203impl ConfigBuilder {
204 #[must_use]
206 pub fn new() -> Self {
207 Self::default()
208 }
209
210 #[must_use]
212 pub fn set_string(mut self, key: &str, value: &str) -> Self {
213 self.values
214 .insert(key.to_string(), toml::Value::String(value.to_string()));
215 self
216 }
217
218 #[must_use]
220 pub fn set_int(mut self, key: &str, value: i64) -> Self {
221 self.values
222 .insert(key.to_string(), toml::Value::Integer(value));
223 self
224 }
225
226 #[must_use]
228 pub fn set_bool(mut self, key: &str, value: bool) -> Self {
229 self.values
230 .insert(key.to_string(), toml::Value::Boolean(value));
231 self
232 }
233
234 #[must_use]
236 pub fn build(self) -> Config {
237 let value = toml::Value::Table(self.values);
238 Config {
239 content: toml::to_string_pretty(&value).unwrap_or_default(),
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use serde::Deserialize;
248
249 #[derive(Debug, Deserialize, PartialEq)]
250 struct TestConfig {
251 name: String,
252 port: u16,
253 }
254
255 #[test]
256 fn test_parse_config() {
257 let config = Config::new(
258 r#"
259 name = "test"
260 port = 8080
261 "#,
262 );
263 let parsed: TestConfig = config.parse().unwrap();
264 assert_eq!(parsed.name, "test");
265 assert_eq!(parsed.port, 8080);
266 }
267
268 #[test]
269 fn test_get_nested_key() {
270 let config = Config::new(
271 r#"
272 [server]
273 host = "localhost"
274 port = 3000
275 "#,
276 );
277 assert_eq!(
278 config.get::<String>("server.host"),
279 Some("localhost".into())
280 );
281 assert_eq!(config.get::<i64>("server.port"), Some(3000));
282 }
283
284 #[test]
285 fn test_config_builder() {
286 let config = ConfigBuilder::new()
287 .set_string("name", "app")
288 .set_int("port", 8080)
289 .set_bool("debug", true)
290 .build();
291
292 assert_eq!(config.get::<String>("name"), Some("app".into()));
293 assert_eq!(config.get::<i64>("port"), Some(8080));
294 assert_eq!(config.get::<bool>("debug"), Some(true));
295 }
296}