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
15#[cfg(feature = "validation")]
16use crate::validation::{ValidationError, ValidationRuleSet};
17
18pub struct Config {
54 values: Value,
56
57 file_path: Option<PathBuf>,
59
60 format: String,
62
63 modified: bool,
65
66 #[cfg(feature = "noml")]
68 noml_document: Option<noml::Document>,
69
70 #[cfg(feature = "validation")]
72 validation_rules: Option<ValidationRuleSet>,
73}
74
75impl Config {
76 pub fn new() -> Self {
78 Self {
79 values: Value::table(BTreeMap::new()),
80 file_path: None,
81 format: "conf".to_string(),
82 modified: false,
83 #[cfg(feature = "noml")]
84 noml_document: None,
85 #[cfg(feature = "validation")]
86 validation_rules: None,
87 }
88 }
89
90 pub fn from_string(source: &str, format: Option<&str>) -> Result<Self> {
92 let detected_format = format.unwrap_or_else(|| parsers::detect_format(source));
93
94 let values = parsers::parse_string(source, Some(detected_format))?;
95
96 #[cfg(feature = "noml")]
97 let mut config = Self {
98 values,
99 file_path: None,
100 format: detected_format.to_string(),
101 modified: false,
102 noml_document: None,
103 #[cfg(feature = "validation")]
104 validation_rules: None,
105 };
106
107 #[cfg(not(feature = "noml"))]
108 let config = Self {
109 values,
110 file_path: None,
111 format: detected_format.to_string(),
112 modified: false,
113 #[cfg(feature = "validation")]
114 validation_rules: None,
115 };
116
117 #[cfg(feature = "noml")]
119 if detected_format == "noml" || detected_format == "toml" {
120 if let Ok(document) = noml::parse_string(source, None) {
121 config.noml_document = Some(document);
122 }
123 }
124
125 Ok(config)
126 }
127
128 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
130 let path = path.as_ref();
131 let content =
132 std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
133
134 let format = parsers::detect_format_from_path(path)
135 .unwrap_or_else(|| parsers::detect_format(&content));
136
137 let mut config = Self::from_string(&content, Some(format))?;
138 config.file_path = Some(path.to_path_buf());
139
140 Ok(config)
141 }
142
143 #[cfg(feature = "async")]
145 pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
146 let path = path.as_ref();
147 let content = tokio::fs::read_to_string(path)
148 .await
149 .map_err(|e| Error::io(path.display().to_string(), e))?;
150
151 let format = parsers::detect_format_from_path(path)
152 .unwrap_or_else(|| parsers::detect_format(&content));
153
154 let mut config = Self::from_string(&content, Some(format))?;
155 config.file_path = Some(path.to_path_buf());
156
157 Ok(config)
158 }
159
160 pub fn get(&self, path: &str) -> Option<&Value> {
162 self.values.get(path)
163 }
164
165 pub fn get_mut(&mut self, path: &str) -> Result<&mut Value> {
167 self.values.get_mut_nested(path)
168 }
169
170 pub fn set<V: Into<Value>>(&mut self, path: &str, value: V) -> Result<()> {
172 self.values.set_nested(path, value.into())?;
173 self.modified = true;
174 Ok(())
175 }
176
177 pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
179 let result = self.values.remove(path)?;
180 if result.is_some() {
181 self.modified = true;
182 }
183 Ok(result)
184 }
185
186 pub fn contains_key(&self, path: &str) -> bool {
188 self.values.contains_key(path)
189 }
190
191 pub fn keys(&self) -> Result<Vec<&str>> {
193 self.values.keys()
194 }
195
196 pub fn is_modified(&self) -> bool {
198 self.modified
199 }
200
201 pub fn mark_clean(&mut self) {
203 self.modified = false;
204 }
205
206 pub fn format(&self) -> &str {
208 &self.format
209 }
210
211 pub fn file_path(&self) -> Option<&Path> {
213 self.file_path.as_deref()
214 }
215
216 pub fn save(&mut self) -> Result<()> {
218 match &self.file_path {
219 Some(path) => {
220 self.save_to_file(path.clone())?;
221 self.modified = false;
222 Ok(())
223 }
224 None => Err(Error::internal(
225 "Cannot save configuration that wasn't loaded from a file",
226 )),
227 }
228 }
229
230 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
232 let serialized = self.serialize()?;
233 std::fs::write(path, serialized).map_err(|e| Error::io("save".to_string(), e))?;
234 Ok(())
235 }
236
237 #[cfg(feature = "async")]
239 pub async fn save_async(&mut self) -> Result<()> {
240 match &self.file_path {
241 Some(path) => {
242 self.save_to_file_async(path.clone()).await?;
243 self.modified = false;
244 Ok(())
245 }
246 None => Err(Error::internal(
247 "Cannot save configuration that wasn't loaded from a file",
248 )),
249 }
250 }
251
252 #[cfg(feature = "async")]
254 pub async fn save_to_file_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
255 let serialized = self.serialize()?;
256 tokio::fs::write(path, serialized)
257 .await
258 .map_err(|e| Error::io("save".to_string(), e))?;
259 Ok(())
260 }
261
262 pub fn serialize(&self) -> Result<String> {
264 match self.format.as_str() {
265 "json" => {
266 #[cfg(feature = "json")]
267 return crate::parsers::json_parser::serialize(&self.values);
268 #[cfg(not(feature = "json"))]
269 return Err(Error::feature_not_enabled("json"));
270 }
271 "toml" => {
272 #[cfg(feature = "toml")]
273 {
274 #[cfg(feature = "noml")]
276 if let Some(ref document) = self.noml_document {
277 return Ok(noml::serialize_document(document)?);
278 }
279 self.serialize_as_toml()
281 }
282 #[cfg(not(feature = "toml"))]
283 return Err(Error::feature_not_enabled("toml"));
284 }
285 "noml" => {
286 #[cfg(feature = "noml")]
287 {
288 if let Some(ref document) = self.noml_document {
289 Ok(noml::serialize_document(document)?)
290 } else {
291 Err(Error::internal("NOML document not preserved"))
292 }
293 }
294 #[cfg(not(feature = "noml"))]
295 return Err(Error::feature_not_enabled("noml"));
296 }
297 "conf" => self.serialize_as_conf(),
298 _ => Err(Error::unknown_format(&self.format)),
299 }
300 }
301
302 fn serialize_as_conf(&self) -> Result<String> {
304 let mut output = String::new();
305 if let Value::Table(table) = &self.values {
306 self.write_conf_table(&mut output, table, "")?;
307 }
308 Ok(output)
309 }
310
311 fn write_conf_table(
313 &self,
314 output: &mut String,
315 table: &BTreeMap<String, Value>,
316 section_prefix: &str,
317 ) -> Result<()> {
318 for (key, value) in table {
320 if !value.is_table() {
321 let formatted_value = self.format_conf_value(value)?;
322 output.push_str(&format!("{key} = {formatted_value}\n"));
323 }
324 }
325
326 for (key, value) in table {
328 if let Value::Table(nested_table) = value {
329 let section_name = if section_prefix.is_empty() {
330 key.clone()
331 } else {
332 format!("{section_prefix}.{key}")
333 };
334
335 output.push_str(&format!("\n[{section_name}]\n"));
336 self.write_conf_table(output, nested_table, §ion_name)?;
337 }
338 }
339
340 Ok(())
341 }
342
343 #[allow(clippy::only_used_in_recursion)]
345 fn format_conf_value(&self, value: &Value) -> Result<String> {
346 match value {
347 Value::Null => Ok("null".to_string()),
348 Value::Bool(b) => Ok(b.to_string()),
349 Value::Integer(i) => Ok(i.to_string()),
350 Value::Float(f) => Ok(f.to_string()),
351 Value::String(s) => {
352 if s.contains(' ') || s.contains('\t') || s.contains('\n') {
353 Ok(format!("\"{}\"", s.replace('"', "\\\"")))
354 } else {
355 Ok(s.clone())
356 }
357 }
358 Value::Array(arr) => {
359 let items: Result<Vec<String>> =
360 arr.iter().map(|v| self.format_conf_value(v)).collect();
361 Ok(items?.join(" "))
362 }
363 Value::Table(_) => Err(Error::type_error(
364 "Cannot serialize nested table as value",
365 "primitive",
366 "table",
367 )),
368 #[cfg(feature = "chrono")]
369 Value::DateTime(dt) => Ok(dt.to_rfc3339()),
370 }
371 }
372
373 #[cfg(feature = "toml")]
375 fn serialize_as_toml(&self) -> Result<String> {
376 Err(Error::internal(
379 "Basic TOML serialization not implemented - use NOML library",
380 ))
381 }
382
383 #[cfg(feature = "schema")]
385 pub fn validate_schema(&self, schema: &Schema) -> Result<()> {
386 schema.validate(&self.values)
387 }
388
389 pub fn as_value(&self) -> &Value {
391 &self.values
392 }
393
394 pub fn merge(&mut self, other: &Config) -> Result<()> {
396 self.merge_value(&other.values)?;
397 self.modified = true;
398 Ok(())
399 }
400
401 fn merge_value(&mut self, other: &Value) -> Result<()> {
403 match (&mut self.values, other) {
404 (Value::Table(self_table), Value::Table(other_table)) => {
405 for (key, other_value) in other_table {
406 match self_table.get_mut(key) {
407 Some(self_value) => {
408 if let (Value::Table(_), Value::Table(_)) = (&*self_value, other_value)
409 {
410 let mut temp_config = Config::new();
412 temp_config.values = self_value.clone();
413 temp_config.merge_value(other_value)?;
414 *self_value = temp_config.values;
415 } else {
416 *self_value = other_value.clone();
418 }
419 }
420 None => {
421 self_table.insert(key.clone(), other_value.clone());
423 }
424 }
425 }
426 }
427 _ => {
428 self.values = other.clone();
430 }
431 }
432 Ok(())
433 }
434
435 pub fn key(&self, path: &str) -> ConfigValue {
443 ConfigValue::new(self.get(path))
444 }
445
446 pub fn has(&self, path: &str) -> bool {
448 self.contains_key(path)
449 }
450
451 pub fn get_or<V>(&self, path: &str, default: V) -> V
453 where
454 V: TryFrom<Value> + Clone,
455 V::Error: std::fmt::Debug,
456 {
457 self.get(path)
458 .and_then(|v| V::try_from(v.clone()).ok())
459 .unwrap_or(default)
460 }
461
462 #[cfg(feature = "validation")]
466 pub fn set_validation_rules(&mut self, rules: ValidationRuleSet) {
467 self.validation_rules = Some(rules);
468 }
469
470 #[cfg(feature = "validation")]
472 pub fn validate(&mut self) -> Result<Vec<ValidationError>> {
473 match &mut self.validation_rules {
474 Some(rules) => {
475 if let Value::Table(table) = &self.values {
476 let mut errors = Vec::new();
477
478 for (key, value) in table {
480 errors.extend(rules.validate(key, value));
481 }
482
483 Ok(errors)
487 } else {
488 Err(Error::validation(
489 "Configuration root must be a table for validation",
490 ))
491 }
492 }
493 None => Ok(Vec::new()), }
495 }
496
497 #[cfg(feature = "validation")]
499 pub fn validate_critical_only(&mut self) -> Result<Vec<ValidationError>> {
500 let all_errors = self.validate()?;
501 Ok(all_errors
502 .into_iter()
503 .filter(|e| e.severity == crate::validation::ValidationSeverity::Critical)
504 .collect())
505 }
506
507 #[cfg(feature = "validation")]
509 pub fn is_valid(&mut self) -> Result<bool> {
510 let critical_errors = self.validate_critical_only()?;
511 Ok(critical_errors.is_empty())
512 }
513
514 #[cfg(feature = "validation")]
516 pub fn validate_path(&mut self, path: &str) -> Result<Vec<ValidationError>> {
517 let value = self
519 .get(path)
520 .ok_or_else(|| Error::key_not_found(path))?
521 .clone();
522
523 match &mut self.validation_rules {
524 Some(rules) => Ok(rules.validate(path, &value)),
525 None => Ok(Vec::new()),
526 }
527 }
528}
529
530impl Default for Config {
531 fn default() -> Self {
532 Self::new()
533 }
534}
535
536pub struct ConfigValue<'a> {
538 value: Option<&'a Value>,
539}
540
541impl<'a> ConfigValue<'a> {
542 fn new(value: Option<&'a Value>) -> Self {
543 Self { value }
544 }
545
546 pub fn as_string(&self) -> Result<String> {
548 match self.value {
549 Some(v) => v.as_string().map(|s| s.to_string()),
550 None => Err(Error::key_not_found("value not found")),
551 }
552 }
553
554 pub fn as_string_or(&self, default: &str) -> String {
556 self.value
557 .and_then(|v| v.as_string().ok())
558 .map(|s| s.to_string())
559 .unwrap_or_else(|| default.to_string())
560 }
561
562 pub fn as_integer(&self) -> Result<i64> {
564 match self.value {
565 Some(v) => v.as_integer(),
566 None => Err(Error::key_not_found("value not found")),
567 }
568 }
569
570 pub fn as_integer_or(&self, default: i64) -> i64 {
572 self.value
573 .and_then(|v| v.as_integer().ok())
574 .unwrap_or(default)
575 }
576
577 pub fn as_bool(&self) -> Result<bool> {
579 match self.value {
580 Some(v) => v.as_bool(),
581 None => Err(Error::key_not_found("value not found")),
582 }
583 }
584
585 pub fn as_bool_or(&self, default: bool) -> bool {
587 self.value.and_then(|v| v.as_bool().ok()).unwrap_or(default)
588 }
589
590 pub fn exists(&self) -> bool {
592 self.value.is_some()
593 }
594
595 pub fn value(&self) -> Option<&'a Value> {
597 self.value
598 }
599}
600
601pub struct ConfigBuilder {
603 format: Option<String>,
604 #[cfg(feature = "validation")]
605 validation_rules: Option<ValidationRuleSet>,
606}
607
608impl ConfigBuilder {
609 pub fn new() -> Self {
611 Self {
612 format: None,
613 #[cfg(feature = "validation")]
614 validation_rules: None,
615 }
616 }
617
618 pub fn format<S: Into<String>>(mut self, format: S) -> Self {
620 self.format = Some(format.into());
621 self
622 }
623
624 #[cfg(feature = "validation")]
626 pub fn validation_rules(mut self, rules: ValidationRuleSet) -> Self {
627 self.validation_rules = Some(rules);
628 self
629 }
630
631 pub fn from_string(self, source: &str) -> Result<Config> {
633 #[cfg(feature = "validation")]
634 let mut config = Config::from_string(source, self.format.as_deref())?;
635 #[cfg(not(feature = "validation"))]
636 let config = Config::from_string(source, self.format.as_deref())?;
637
638 #[cfg(feature = "validation")]
639 if let Some(rules) = self.validation_rules {
640 config.set_validation_rules(rules);
641 }
642
643 Ok(config)
644 }
645
646 pub fn from_file<P: AsRef<Path>>(self, path: P) -> Result<Config> {
648 #[cfg(feature = "validation")]
649 let mut config = Config::from_file(path)?;
650 #[cfg(not(feature = "validation"))]
651 let config = Config::from_file(path)?;
652
653 #[cfg(feature = "validation")]
654 if let Some(rules) = self.validation_rules {
655 config.set_validation_rules(rules);
656 }
657
658 Ok(config)
659 }
660}
661
662impl Default for ConfigBuilder {
663 fn default() -> Self {
664 Self::new()
665 }
666}
667
668impl From<Value> for Config {
670 fn from(value: Value) -> Self {
671 Self {
672 values: value,
673 file_path: None,
674 format: "conf".to_string(),
675 modified: false,
676 #[cfg(feature = "noml")]
677 noml_document: None,
678 #[cfg(feature = "validation")]
679 validation_rules: None,
680 }
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn test_config_creation() {
690 let config = Config::new();
691 assert!(!config.is_modified());
692 assert_eq!(config.format(), "conf");
693 }
694
695 #[test]
696 fn test_config_from_string() {
697 let config = Config::from_string("key = value\nport = 8080", Some("conf")).unwrap();
698
699 assert_eq!(config.get("key").unwrap().as_string().unwrap(), "value");
700 assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
701 }
702
703 #[test]
704 fn test_config_modification() {
705 let mut config = Config::new();
706 assert!(!config.is_modified());
707
708 config.set("key", "value").unwrap();
709 assert!(config.is_modified());
710
711 config.mark_clean();
712 assert!(!config.is_modified());
713 }
714
715 #[test]
716 fn test_config_merge() {
717 let mut config1 = Config::new();
718 config1.set("a", 1).unwrap();
719 config1.set("b.x", 2).unwrap();
720
721 let mut config2 = Config::new();
722 config2.set("b.y", 3).unwrap();
723 config2.set("c", 4).unwrap();
724
725 config1.merge(&config2).unwrap();
726
727 assert_eq!(config1.get("a").unwrap().as_integer().unwrap(), 1);
728 assert_eq!(config1.get("b.x").unwrap().as_integer().unwrap(), 2);
729 assert_eq!(config1.get("b.y").unwrap().as_integer().unwrap(), 3);
730 assert_eq!(config1.get("c").unwrap().as_integer().unwrap(), 4);
731 }
732}