1use crate::error::{Error, Result};
7use crate::parsers;
8use crate::value::Value;
9use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11
12#[cfg(feature = "schema")]
13use crate::schema::Schema;
14
15pub struct Config {
51 values: Value,
53
54 file_path: Option<PathBuf>,
56
57 format: String,
59
60 modified: bool,
62
63 #[cfg(feature = "noml")]
65 noml_document: Option<noml::Document>,
66}
67
68impl Config {
69 pub fn new() -> Self {
71 Self {
72 values: Value::table(BTreeMap::new()),
73 file_path: None,
74 format: "conf".to_string(),
75 modified: false,
76 #[cfg(feature = "noml")]
77 noml_document: None,
78 }
79 }
80
81 pub fn from_string(source: &str, format: Option<&str>) -> Result<Self> {
83 let detected_format = format.unwrap_or_else(|| {
84 parsers::detect_format(source)
85 });
86
87 let values = parsers::parse_string(source, Some(detected_format))?;
88
89 let config = Self {
90 values,
91 file_path: None,
92 format: detected_format.to_string(),
93 modified: false,
94 #[cfg(feature = "noml")]
95 noml_document: None,
96 };
97
98 #[cfg(feature = "noml")]
100 if detected_format == "noml" || detected_format == "toml" {
101 if let Ok(document) = noml::parse_string(source, None) {
102 config.noml_document = Some(document);
103 }
104 }
105
106 Ok(config)
107 }
108
109 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
111 let path = path.as_ref();
112 let content = std::fs::read_to_string(path)
113 .map_err(|e| Error::io(path.display().to_string(), e))?;
114
115 let format = parsers::detect_format_from_path(path)
116 .unwrap_or_else(|| parsers::detect_format(&content));
117
118 let mut config = Self::from_string(&content, Some(format))?;
119 config.file_path = Some(path.to_path_buf());
120
121 Ok(config)
122 }
123
124 #[cfg(feature = "async")]
126 pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
127 let path = path.as_ref();
128 let content = tokio::fs::read_to_string(path)
129 .await
130 .map_err(|e| Error::io(path.display().to_string(), e))?;
131
132 let format = parsers::detect_format_from_path(path)
133 .unwrap_or_else(|| parsers::detect_format(&content));
134
135 let mut config = Self::from_string(&content, Some(format))?;
136 config.file_path = Some(path.to_path_buf());
137
138 Ok(config)
139 }
140
141 pub fn get(&self, path: &str) -> Option<&Value> {
143 self.values.get(path)
144 }
145
146 pub fn get_mut(&mut self, path: &str) -> Result<&mut Value> {
148 self.values.get_mut_nested(path)
149 }
150
151 pub fn set<V: Into<Value>>(&mut self, path: &str, value: V) -> Result<()> {
153 self.values.set_nested(path, value.into())?;
154 self.modified = true;
155 Ok(())
156 }
157
158 pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
160 let result = self.values.remove(path)?;
161 if result.is_some() {
162 self.modified = true;
163 }
164 Ok(result)
165 }
166
167 pub fn contains_key(&self, path: &str) -> bool {
169 self.values.contains_key(path)
170 }
171
172 pub fn keys(&self) -> Result<Vec<&str>> {
174 self.values.keys()
175 }
176
177 pub fn is_modified(&self) -> bool {
179 self.modified
180 }
181
182 pub fn mark_clean(&mut self) {
184 self.modified = false;
185 }
186
187 pub fn format(&self) -> &str {
189 &self.format
190 }
191
192 pub fn file_path(&self) -> Option<&Path> {
194 self.file_path.as_deref()
195 }
196
197 pub fn save(&mut self) -> Result<()> {
199 match &self.file_path {
200 Some(path) => {
201 self.save_to_file(path.clone())?;
202 self.modified = false;
203 Ok(())
204 },
205 None => Err(Error::internal(
206 "Cannot save configuration that wasn't loaded from a file"
207 )),
208 }
209 }
210
211 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
213 let serialized = self.serialize()?;
214 std::fs::write(path, serialized)
215 .map_err(|e| Error::io("save".to_string(), e))?;
216 Ok(())
217 }
218
219 #[cfg(feature = "async")]
221 pub async fn save_async(&mut self) -> Result<()> {
222 match &self.file_path {
223 Some(path) => {
224 self.save_to_file_async(path.clone()).await?;
225 self.modified = false;
226 Ok(())
227 },
228 None => Err(Error::internal(
229 "Cannot save configuration that wasn't loaded from a file"
230 )),
231 }
232 }
233
234 #[cfg(feature = "async")]
236 pub async fn save_to_file_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
237 let serialized = self.serialize()?;
238 tokio::fs::write(path, serialized)
239 .await
240 .map_err(|e| Error::io("save".to_string(), e))?;
241 Ok(())
242 }
243
244 pub fn serialize(&self) -> Result<String> {
246 match self.format.as_str() {
247 "json" => {
248 #[cfg(feature = "json")]
249 return crate::parsers::json_parser::serialize(&self.values);
250 #[cfg(not(feature = "json"))]
251 return Err(Error::feature_not_enabled("json"));
252 }
253 "toml" => {
254 #[cfg(feature = "toml")]
255 {
256 if let Some(ref document) = self.noml_document {
258 return Ok(noml::serialize_document(document));
259 } else {
260 return self.serialize_as_toml();
262 }
263 }
264 #[cfg(not(feature = "toml"))]
265 return Err(Error::feature_not_enabled("toml"));
266 }
267 "noml" => {
268 #[cfg(feature = "noml")]
269 {
270 if let Some(ref document) = self.noml_document {
271 return Ok(noml::serialize_document(document));
272 } else {
273 return Err(Error::internal("NOML document not preserved"));
274 }
275 }
276 #[cfg(not(feature = "noml"))]
277 return Err(Error::feature_not_enabled("noml"));
278 }
279 "conf" => self.serialize_as_conf(),
280 _ => Err(Error::unknown_format(&self.format)),
281 }
282 }
283
284 fn serialize_as_conf(&self) -> Result<String> {
286 let mut output = String::new();
287 if let Value::Table(table) = &self.values {
288 self.write_conf_table(&mut output, table, "")?;
289 }
290 Ok(output)
291 }
292
293 fn write_conf_table(
295 &self,
296 output: &mut String,
297 table: &BTreeMap<String, Value>,
298 section_prefix: &str,
299 ) -> Result<()> {
300 for (key, value) in table {
302 if !value.is_table() {
303 let formatted_value = self.format_conf_value(value)?;
304 output.push_str(&format!("{} = {}\n", key, formatted_value));
305 }
306 }
307
308 for (key, value) in table {
310 if let Value::Table(nested_table) = value {
311 let section_name = if section_prefix.is_empty() {
312 key.clone()
313 } else {
314 format!("{}.{}", section_prefix, key)
315 };
316
317 output.push_str(&format!("\n[{}]\n", section_name));
318 self.write_conf_table(output, nested_table, §ion_name)?;
319 }
320 }
321
322 Ok(())
323 }
324
325 fn format_conf_value(&self, value: &Value) -> Result<String> {
327 match value {
328 Value::Null => Ok("null".to_string()),
329 Value::Bool(b) => Ok(b.to_string()),
330 Value::Integer(i) => Ok(i.to_string()),
331 Value::Float(f) => Ok(f.to_string()),
332 Value::String(s) => {
333 if s.contains(' ') || s.contains('\t') || s.contains('\n') {
334 Ok(format!("\"{}\"", s.replace('"', "\\\"")))
335 } else {
336 Ok(s.clone())
337 }
338 }
339 Value::Array(arr) => {
340 let items: Result<Vec<String>> = arr
341 .iter()
342 .map(|v| self.format_conf_value(v))
343 .collect();
344 Ok(items?.join(" "))
345 }
346 Value::Table(_) => Err(Error::type_error(
347 "Cannot serialize nested table as value",
348 "primitive",
349 "table",
350 )),
351 #[cfg(feature = "chrono")]
352 Value::DateTime(dt) => Ok(dt.to_rfc3339()),
353 }
354 }
355
356 #[cfg(feature = "toml")]
358 fn serialize_as_toml(&self) -> Result<String> {
359 Err(Error::internal(
362 "Basic TOML serialization not implemented - use NOML library"
363 ))
364 }
365
366 #[cfg(feature = "schema")]
368 pub fn validate_schema(&self, schema: &Schema) -> Result<()> {
369 schema.validate(&self.values)
370 }
371
372 pub fn as_value(&self) -> &Value {
374 &self.values
375 }
376
377 pub fn merge(&mut self, other: &Config) -> Result<()> {
379 self.merge_value(&other.values)?;
380 self.modified = true;
381 Ok(())
382 }
383
384 fn merge_value(&mut self, other: &Value) -> Result<()> {
386 match (&mut self.values, other) {
387 (Value::Table(self_table), Value::Table(other_table)) => {
388 for (key, other_value) in other_table {
389 match self_table.get_mut(key) {
390 Some(self_value) => {
391 if let (Value::Table(_), Value::Table(_)) = (&*self_value, other_value) {
392 let mut temp_config = Config::new();
394 temp_config.values = self_value.clone();
395 temp_config.merge_value(other_value)?;
396 *self_value = temp_config.values;
397 } else {
398 *self_value = other_value.clone();
400 }
401 }
402 None => {
403 self_table.insert(key.clone(), other_value.clone());
405 }
406 }
407 }
408 }
409 _ => {
410 self.values = other.clone();
412 }
413 }
414 Ok(())
415 }
416}
417
418impl Default for Config {
419 fn default() -> Self {
420 Self::new()
421 }
422}
423
424impl From<Value> for Config {
426 fn from(value: Value) -> Self {
427 Self {
428 values: value,
429 file_path: None,
430 format: "conf".to_string(),
431 modified: false,
432 #[cfg(feature = "noml")]
433 noml_document: None,
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_config_creation() {
444 let config = Config::new();
445 assert!(!config.is_modified());
446 assert_eq!(config.format(), "conf");
447 }
448
449 #[test]
450 fn test_config_from_string() {
451 let config = Config::from_string(
452 "key = value\nport = 8080",
453 Some("conf")
454 ).unwrap();
455
456 assert_eq!(config.get("key").unwrap().as_string().unwrap(), "value");
457 assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
458 }
459
460 #[test]
461 fn test_config_modification() {
462 let mut config = Config::new();
463 assert!(!config.is_modified());
464
465 config.set("key", "value").unwrap();
466 assert!(config.is_modified());
467
468 config.mark_clean();
469 assert!(!config.is_modified());
470 }
471
472 #[test]
473 fn test_config_merge() {
474 let mut config1 = Config::new();
475 config1.set("a", 1).unwrap();
476 config1.set("b.x", 2).unwrap();
477
478 let mut config2 = Config::new();
479 config2.set("b.y", 3).unwrap();
480 config2.set("c", 4).unwrap();
481
482 config1.merge(&config2).unwrap();
483
484 assert_eq!(config1.get("a").unwrap().as_integer().unwrap(), 1);
485 assert_eq!(config1.get("b.x").unwrap().as_integer().unwrap(), 2);
486 assert_eq!(config1.get("b.y").unwrap().as_integer().unwrap(), 3);
487 assert_eq!(config1.get("c").unwrap().as_integer().unwrap(), 4);
488 }
489}