1use crate::error::{Error, Result};
7use crate::parsers;
8use crate::value::Value;
9use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11use std::sync::atomic::{AtomicU64, Ordering};
12
13#[cfg(feature = "schema")]
14use crate::schema::Schema;
15
16#[cfg(feature = "validation")]
17use crate::validation::{ValidationError, ValidationRuleSet};
18
19pub struct Config {
55 values: Value,
57
58 file_path: Option<PathBuf>,
60
61 format: String,
63
64 modified: bool,
66
67 options: ConfigOptions,
70
71 cache_hits: AtomicU64,
78
79 cache_misses: AtomicU64,
81
82 #[cfg(feature = "noml")]
84 noml_document: Option<noml::Document>,
85
86 #[cfg(feature = "validation")]
88 validation_rules: Option<ValidationRuleSet>,
89}
90
91impl Config {
92 pub fn new() -> Self {
94 Self {
95 values: Value::table(BTreeMap::new()),
96 file_path: None,
97 format: "conf".to_string(),
98 modified: false,
99 options: ConfigOptions::default(),
100 cache_hits: AtomicU64::new(0),
101 cache_misses: AtomicU64::new(0),
102 #[cfg(feature = "noml")]
103 noml_document: None,
104 #[cfg(feature = "validation")]
105 validation_rules: None,
106 }
107 }
108
109 pub fn from_string(source: &str, format: Option<&str>) -> Result<Self> {
111 let detected_format = format.unwrap_or_else(|| parsers::detect_format(source));
112
113 let values = parsers::parse_string(source, Some(detected_format))?;
114
115 #[cfg(feature = "noml")]
116 let mut config = Self {
117 values,
118 file_path: None,
119 format: detected_format.to_string(),
120 modified: false,
121 options: ConfigOptions::default(),
122 cache_hits: AtomicU64::new(0),
123 cache_misses: AtomicU64::new(0),
124 noml_document: None,
125 #[cfg(feature = "validation")]
126 validation_rules: None,
127 };
128
129 #[cfg(not(feature = "noml"))]
130 let config = Self {
131 values,
132 file_path: None,
133 format: detected_format.to_string(),
134 modified: false,
135 options: ConfigOptions::default(),
136 cache_hits: AtomicU64::new(0),
137 cache_misses: AtomicU64::new(0),
138 #[cfg(feature = "validation")]
139 validation_rules: None,
140 };
141
142 #[cfg(feature = "noml")]
144 if detected_format == "noml" || detected_format == "toml" {
145 if let Ok(document) = noml::parse_string(source, None) {
146 config.noml_document = Some(document);
147 }
148 }
149
150 Ok(config)
151 }
152
153 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
155 let path = path.as_ref();
156 let content =
157 std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
158
159 let format = parsers::detect_format_from_path(path)
160 .unwrap_or_else(|| parsers::detect_format(&content));
161
162 let mut config = Self::from_string(&content, Some(format))?;
163 config.file_path = Some(path.to_path_buf());
164
165 Ok(config)
166 }
167
168 #[cfg(feature = "async")]
170 pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
171 let path = path.as_ref();
172 let content = tokio::fs::read_to_string(path)
173 .await
174 .map_err(|e| Error::io(path.display().to_string(), e))?;
175
176 let format = parsers::detect_format_from_path(path)
177 .unwrap_or_else(|| parsers::detect_format(&content));
178
179 let mut config = Self::from_string(&content, Some(format))?;
180 config.file_path = Some(path.to_path_buf());
181
182 Ok(config)
183 }
184
185 pub fn get(&self, path: &str) -> Option<&Value> {
187 self.values.get(path)
188 }
189
190 pub fn get_mut(&mut self, path: &str) -> Result<&mut Value> {
192 self.values.get_mut_nested(path)
193 }
194
195 pub fn set<V: Into<Value>>(&mut self, path: &str, value: V) -> Result<()> {
203 self.ensure_writable()?;
204 self.values.set_nested(path, value.into())?;
205 self.modified = true;
206 Ok(())
207 }
208
209 pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
217 self.ensure_writable()?;
218 let result = self.values.remove(path)?;
219 if result.is_some() {
220 self.modified = true;
221 }
222 Ok(result)
223 }
224
225 pub fn contains_key(&self, path: &str) -> bool {
227 self.values.contains_key(path)
228 }
229
230 pub fn keys(&self) -> Result<Vec<&str>> {
232 self.values.keys()
233 }
234
235 pub fn is_modified(&self) -> bool {
237 self.modified
238 }
239
240 pub fn mark_clean(&mut self) {
242 self.modified = false;
243 }
244
245 pub fn format(&self) -> &str {
247 &self.format
248 }
249
250 pub fn file_path(&self) -> Option<&Path> {
252 self.file_path.as_deref()
253 }
254
255 pub fn save(&mut self) -> Result<()> {
257 match &self.file_path {
258 Some(path) => {
259 self.save_to_file(path.clone())?;
260 self.modified = false;
261 Ok(())
262 }
263 None => Err(Error::internal(
264 "Cannot save configuration that wasn't loaded from a file",
265 )),
266 }
267 }
268
269 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
271 let serialized = self.serialize()?;
272 std::fs::write(path, serialized).map_err(|e| Error::io("save".to_string(), e))?;
273 Ok(())
274 }
275
276 #[cfg(feature = "async")]
278 pub async fn save_async(&mut self) -> Result<()> {
279 match &self.file_path {
280 Some(path) => {
281 self.save_to_file_async(path.clone()).await?;
282 self.modified = false;
283 Ok(())
284 }
285 None => Err(Error::internal(
286 "Cannot save configuration that wasn't loaded from a file",
287 )),
288 }
289 }
290
291 #[cfg(feature = "async")]
293 pub async fn save_to_file_async<P: AsRef<Path>>(&self, path: P) -> Result<()> {
294 let serialized = self.serialize()?;
295 tokio::fs::write(path, serialized)
296 .await
297 .map_err(|e| Error::io("save".to_string(), e))?;
298 Ok(())
299 }
300
301 pub fn serialize(&self) -> Result<String> {
303 match self.format.as_str() {
304 "json" => {
305 #[cfg(feature = "json")]
306 return crate::parsers::json_parser::serialize(&self.values);
307 #[cfg(not(feature = "json"))]
308 return Err(Error::feature_not_enabled("json"));
309 }
310 "toml" => {
311 #[cfg(feature = "toml")]
312 {
313 #[cfg(feature = "noml")]
315 if let Some(ref document) = self.noml_document {
316 return Ok(noml::serialize_document(document)?);
317 }
318 self.serialize_as_toml()
320 }
321 #[cfg(not(feature = "toml"))]
322 return Err(Error::feature_not_enabled("toml"));
323 }
324 "noml" => {
325 #[cfg(feature = "noml")]
326 {
327 if let Some(ref document) = self.noml_document {
328 Ok(noml::serialize_document(document)?)
329 } else {
330 Err(Error::internal("NOML document not preserved"))
331 }
332 }
333 #[cfg(not(feature = "noml"))]
334 return Err(Error::feature_not_enabled("noml"));
335 }
336 "conf" => self.serialize_as_conf(),
337 _ => Err(Error::unknown_format(&self.format)),
338 }
339 }
340
341 fn serialize_as_conf(&self) -> Result<String> {
343 let mut output = String::new();
344 if let Value::Table(table) = &self.values {
345 self.write_conf_table(&mut output, table, "")?;
346 }
347 Ok(output)
348 }
349
350 fn write_conf_table(
352 &self,
353 output: &mut String,
354 table: &BTreeMap<String, Value>,
355 section_prefix: &str,
356 ) -> Result<()> {
357 for (key, value) in table {
359 if !value.is_table() {
360 let formatted_value = self.format_conf_value(value)?;
361 output.push_str(&format!("{key} = {formatted_value}\n"));
362 }
363 }
364
365 for (key, value) in table {
367 if let Value::Table(nested_table) = value {
368 let section_name = if section_prefix.is_empty() {
369 key.clone()
370 } else {
371 format!("{section_prefix}.{key}")
372 };
373
374 output.push_str(&format!("\n[{section_name}]\n"));
375 self.write_conf_table(output, nested_table, §ion_name)?;
376 }
377 }
378
379 Ok(())
380 }
381
382 #[allow(clippy::only_used_in_recursion)]
384 fn format_conf_value(&self, value: &Value) -> Result<String> {
385 match value {
386 Value::Null => Ok("null".to_string()),
387 Value::Bool(b) => Ok(b.to_string()),
388 Value::Integer(i) => Ok(i.to_string()),
389 Value::Float(f) => Ok(f.to_string()),
390 Value::String(s) => {
391 if s.contains(' ') || s.contains('\t') || s.contains('\n') {
392 Ok(format!("\"{}\"", s.replace('"', "\\\"")))
393 } else {
394 Ok(s.clone())
395 }
396 }
397 Value::Array(arr) => {
398 let items: Result<Vec<String>> =
399 arr.iter().map(|v| self.format_conf_value(v)).collect();
400 Ok(items?.join(" "))
401 }
402 Value::Table(_) => Err(Error::type_error(
403 "Cannot serialize nested table as value",
404 "primitive",
405 "table",
406 )),
407 #[cfg(feature = "chrono")]
408 Value::DateTime(dt) => Ok(dt.to_rfc3339()),
409 }
410 }
411
412 #[cfg(feature = "toml")]
414 fn serialize_as_toml(&self) -> Result<String> {
415 Err(Error::internal(
418 "Basic TOML serialization not implemented - use NOML library",
419 ))
420 }
421
422 #[cfg(feature = "schema")]
424 pub fn validate_schema(&self, schema: &Schema) -> Result<()> {
425 schema.validate(&self.values)
426 }
427
428 pub fn as_value(&self) -> &Value {
430 &self.values
431 }
432
433 pub fn merge(&mut self, other: &Config) -> Result<()> {
440 self.ensure_writable()?;
441 self.merge_value(&other.values)?;
442 self.modified = true;
443 Ok(())
444 }
445
446 fn merge_value(&mut self, other: &Value) -> Result<()> {
448 match (&mut self.values, other) {
449 (Value::Table(self_table), Value::Table(other_table)) => {
450 for (key, other_value) in other_table {
451 match self_table.get_mut(key) {
452 Some(self_value) => {
453 if let (Value::Table(_), Value::Table(_)) = (&*self_value, other_value)
454 {
455 let mut temp_config = Config::new();
457 temp_config.values = self_value.clone();
458 temp_config.merge_value(other_value)?;
459 *self_value = temp_config.values;
460 } else {
461 *self_value = other_value.clone();
463 }
464 }
465 None => {
466 self_table.insert(key.clone(), other_value.clone());
468 }
469 }
470 }
471 }
472 _ => {
473 self.values = other.clone();
475 }
476 }
477 Ok(())
478 }
479
480 pub fn key(&self, path: &str) -> ConfigValue<'_> {
488 ConfigValue::new(self.get(path))
489 }
490
491 pub fn has(&self, path: &str) -> bool {
493 self.contains_key(path)
494 }
495
496 pub fn get_or<V>(&self, path: &str, default: V) -> V
498 where
499 V: TryFrom<Value> + Clone,
500 V::Error: std::fmt::Debug,
501 {
502 self.get(path)
503 .and_then(|v| V::try_from(v.clone()).ok())
504 .unwrap_or(default)
505 }
506
507 #[cfg(feature = "validation")]
511 pub fn set_validation_rules(&mut self, rules: ValidationRuleSet) {
512 self.validation_rules = Some(rules);
513 }
514
515 #[cfg(feature = "validation")]
517 pub fn validate(&mut self) -> Result<Vec<ValidationError>> {
518 match &mut self.validation_rules {
519 Some(rules) => {
520 if let Value::Table(table) = &self.values {
521 let mut errors = Vec::new();
522
523 for (key, value) in table {
525 errors.extend(rules.validate(key, value));
526 }
527
528 Ok(errors)
532 } else {
533 Err(Error::validation(
534 "Configuration root must be a table for validation",
535 ))
536 }
537 }
538 None => Ok(Vec::new()), }
540 }
541
542 #[cfg(feature = "validation")]
544 pub fn validate_critical_only(&mut self) -> Result<Vec<ValidationError>> {
545 let all_errors = self.validate()?;
546 Ok(all_errors
547 .into_iter()
548 .filter(|e| e.severity == crate::validation::ValidationSeverity::Critical)
549 .collect())
550 }
551
552 #[cfg(feature = "validation")]
554 pub fn is_valid(&mut self) -> Result<bool> {
555 let critical_errors = self.validate_critical_only()?;
556 Ok(critical_errors.is_empty())
557 }
558
559 #[cfg(feature = "validation")]
561 pub fn validate_path(&mut self, path: &str) -> Result<Vec<ValidationError>> {
562 let value = self
564 .get(path)
565 .ok_or_else(|| Error::key_not_found(path))?
566 .clone();
567
568 match &mut self.validation_rules {
569 Some(rules) => Ok(rules.validate(path, &value)),
570 None => Ok(Vec::new()),
571 }
572 }
573}
574
575impl Default for Config {
576 fn default() -> Self {
577 Self::new()
578 }
579}
580
581#[derive(Debug, Clone)]
613#[non_exhaustive]
614pub struct ConfigOptions {
615 pub read_only: bool,
619
620 pub cache_enabled: bool,
625
626 pub cache_capacity: usize,
629}
630
631impl Default for ConfigOptions {
632 fn default() -> Self {
636 Self {
637 read_only: false,
638 cache_enabled: true,
639 cache_capacity: 1024,
640 }
641 }
642}
643
644impl ConfigOptions {
645 pub fn new() -> Self {
648 Self::default()
649 }
650
651 pub fn read_only(mut self, read_only: bool) -> Self {
653 self.read_only = read_only;
654 self
655 }
656
657 pub fn cache_enabled(mut self, cache_enabled: bool) -> Self {
661 self.cache_enabled = cache_enabled;
662 self
663 }
664
665 pub fn cache_capacity(mut self, cache_capacity: usize) -> Self {
668 self.cache_capacity = cache_capacity;
669 self
670 }
671}
672
673impl Config {
674 pub fn with_options(options: ConfigOptions) -> Self {
680 let mut config = Self::new();
681 config.options = options;
682 config
683 }
684
685 pub fn options(&self) -> &ConfigOptions {
687 &self.options
688 }
689
690 pub fn is_read_only(&self) -> bool {
693 self.options.read_only
694 }
695
696 fn ensure_writable(&self) -> Result<()> {
699 if self.options.read_only {
700 Err(Error::general("Configuration is read-only"))
701 } else {
702 Ok(())
703 }
704 }
705
706 pub fn cache_stats(&self) -> CacheStats {
720 let hits = self.cache_hits.load(Ordering::Relaxed);
721 let misses = self.cache_misses.load(Ordering::Relaxed);
722 let total = hits.saturating_add(misses);
723 let hit_ratio = if total == 0 {
724 0.0
725 } else {
726 hits as f64 / total as f64
727 };
728 CacheStats {
729 hits,
730 misses,
731 hit_ratio,
732 }
733 }
734}
735
736#[derive(Debug, Clone, Copy)]
767#[non_exhaustive]
768pub struct CacheStats {
769 pub hits: u64,
771 pub misses: u64,
774 pub hit_ratio: f64,
777}
778
779pub struct ConfigValue<'a> {
781 value: Option<&'a Value>,
782}
783
784impl<'a> ConfigValue<'a> {
785 fn new(value: Option<&'a Value>) -> Self {
786 Self { value }
787 }
788
789 pub fn as_string(&self) -> Result<String> {
791 match self.value {
792 Some(v) => v.as_string().map(|s| s.to_string()),
793 None => Err(Error::key_not_found("value not found")),
794 }
795 }
796
797 pub fn as_string_or(&self, default: &str) -> String {
799 self.value
800 .and_then(|v| v.as_string().ok())
801 .map(|s| s.to_string())
802 .unwrap_or_else(|| default.to_string())
803 }
804
805 pub fn as_integer(&self) -> Result<i64> {
807 match self.value {
808 Some(v) => v.as_integer(),
809 None => Err(Error::key_not_found("value not found")),
810 }
811 }
812
813 pub fn as_integer_or(&self, default: i64) -> i64 {
815 self.value
816 .and_then(|v| v.as_integer().ok())
817 .unwrap_or(default)
818 }
819
820 pub fn as_bool(&self) -> Result<bool> {
822 match self.value {
823 Some(v) => v.as_bool(),
824 None => Err(Error::key_not_found("value not found")),
825 }
826 }
827
828 pub fn as_bool_or(&self, default: bool) -> bool {
830 self.value.and_then(|v| v.as_bool().ok()).unwrap_or(default)
831 }
832
833 pub fn exists(&self) -> bool {
835 self.value.is_some()
836 }
837
838 pub fn value(&self) -> Option<&'a Value> {
840 self.value
841 }
842}
843
844pub struct ConfigBuilder {
846 format: Option<String>,
847 #[cfg(feature = "validation")]
848 validation_rules: Option<ValidationRuleSet>,
849}
850
851impl ConfigBuilder {
852 pub fn new() -> Self {
854 Self {
855 format: None,
856 #[cfg(feature = "validation")]
857 validation_rules: None,
858 }
859 }
860
861 pub fn format<S: Into<String>>(mut self, format: S) -> Self {
863 self.format = Some(format.into());
864 self
865 }
866
867 #[cfg(feature = "validation")]
869 pub fn validation_rules(mut self, rules: ValidationRuleSet) -> Self {
870 self.validation_rules = Some(rules);
871 self
872 }
873
874 pub fn from_string(self, source: &str) -> Result<Config> {
876 #[cfg(feature = "validation")]
877 let mut config = Config::from_string(source, self.format.as_deref())?;
878 #[cfg(not(feature = "validation"))]
879 let config = Config::from_string(source, self.format.as_deref())?;
880
881 #[cfg(feature = "validation")]
882 if let Some(rules) = self.validation_rules {
883 config.set_validation_rules(rules);
884 }
885
886 Ok(config)
887 }
888
889 pub fn from_file<P: AsRef<Path>>(self, path: P) -> Result<Config> {
891 #[cfg(feature = "validation")]
892 let mut config = Config::from_file(path)?;
893 #[cfg(not(feature = "validation"))]
894 let config = Config::from_file(path)?;
895
896 #[cfg(feature = "validation")]
897 if let Some(rules) = self.validation_rules {
898 config.set_validation_rules(rules);
899 }
900
901 Ok(config)
902 }
903}
904
905impl Default for ConfigBuilder {
906 fn default() -> Self {
907 Self::new()
908 }
909}
910
911impl From<Value> for Config {
913 fn from(value: Value) -> Self {
914 Self {
915 values: value,
916 file_path: None,
917 format: "conf".to_string(),
918 modified: false,
919 options: ConfigOptions::default(),
920 cache_hits: AtomicU64::new(0),
921 cache_misses: AtomicU64::new(0),
922 #[cfg(feature = "noml")]
923 noml_document: None,
924 #[cfg(feature = "validation")]
925 validation_rules: None,
926 }
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
935 fn test_config_creation() {
936 let config = Config::new();
937 assert!(!config.is_modified());
938 assert_eq!(config.format(), "conf");
939 }
940
941 #[test]
942 fn test_config_from_string() {
943 let config = Config::from_string("key = value\nport = 8080", Some("conf")).unwrap();
944
945 assert_eq!(config.get("key").unwrap().as_string().unwrap(), "value");
946 assert_eq!(config.get("port").unwrap().as_integer().unwrap(), 8080);
947 }
948
949 #[test]
950 fn test_config_modification() {
951 let mut config = Config::new();
952 assert!(!config.is_modified());
953
954 config.set("key", "value").unwrap();
955 assert!(config.is_modified());
956
957 config.mark_clean();
958 assert!(!config.is_modified());
959 }
960
961 #[test]
962 fn test_config_merge() {
963 let mut config1 = Config::new();
964 config1.set("a", 1).unwrap();
965 config1.set("b.x", 2).unwrap();
966
967 let mut config2 = Config::new();
968 config2.set("b.y", 3).unwrap();
969 config2.set("c", 4).unwrap();
970
971 config1.merge(&config2).unwrap();
972
973 assert_eq!(config1.get("a").unwrap().as_integer().unwrap(), 1);
974 assert_eq!(config1.get("b.x").unwrap().as_integer().unwrap(), 2);
975 assert_eq!(config1.get("b.y").unwrap().as_integer().unwrap(), 3);
976 assert_eq!(config1.get("c").unwrap().as_integer().unwrap(), 4);
977 }
978}