1#![allow(deprecated)] use crate::{Error, Result, Value};
44use std::collections::{BTreeMap, HashMap};
45use std::path::Path;
46use std::sync::{Arc, RwLock};
47
48#[derive(Debug, Clone)]
58struct FastCache {
59 hot_values: HashMap<String, Value>,
61 hits: u64,
63 misses: u64,
65}
66
67impl FastCache {
68 fn new() -> Self {
69 Self {
70 hot_values: HashMap::new(),
71 hits: 0,
72 misses: 0,
73 }
74 }
75
76 fn get(&mut self, key: &str) -> Option<&Value> {
77 if let Some(value) = self.hot_values.get(key) {
78 self.hits += 1;
79 Some(value)
80 } else {
81 self.misses += 1;
82 None
83 }
84 }
85
86 fn insert(&mut self, key: String, value: Value) {
87 if self.hot_values.len() >= 100 {
89 let keys_to_remove: Vec<_> = self.hot_values.keys().take(20).cloned().collect();
91 for k in keys_to_remove {
92 self.hot_values.remove(&k);
93 }
94 }
95 self.hot_values.insert(key, value);
96 }
97}
98
99#[deprecated(
151 since = "0.9.4",
152 note = "use `config_lib::Config` directly. `EnterpriseConfig` will be folded \
153 into `Config` when lock-free caching lands in v0.9.5. \
154 See the migration table in the `enterprise` module docs."
155)]
156#[derive(Debug)]
157pub struct EnterpriseConfig {
158 fast_cache: Arc<RwLock<FastCache>>,
160 cache: Arc<RwLock<BTreeMap<String, Value>>>,
162 defaults: Arc<RwLock<BTreeMap<String, Value>>>,
164 file_path: Option<String>,
166 format: String,
168 read_only: bool,
170}
171
172#[deprecated(
189 since = "0.9.4",
190 note = "`ConfigManager::get` returns `Arc<RwLock<EnterpriseConfig>>` today; \
191 in v0.9.5 it returns `Arc<RwLock<Config>>` once `Config` absorbs \
192 the cached/thread-safe surface. `ConfigManager` itself is retained."
193)]
194#[derive(Debug, Default)]
195pub struct ConfigManager {
196 configs: Arc<RwLock<HashMap<String, EnterpriseConfig>>>,
198}
199
200impl Default for EnterpriseConfig {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206impl EnterpriseConfig {
207 #[inline(always)]
209 pub fn new() -> Self {
210 Self {
211 fast_cache: Arc::new(RwLock::new(FastCache::new())),
212 cache: Arc::new(RwLock::new(BTreeMap::new())),
213 defaults: Arc::new(RwLock::new(BTreeMap::new())),
214 file_path: None,
215 format: "conf".to_string(),
216 read_only: false,
217 }
218 }
219
220 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
222 let path_str = path.as_ref().to_string_lossy().to_string();
223 let content = std::fs::read_to_string(&path)?;
224
225 let format = Self::detect_format(&path_str);
227 let value = Self::parse_content(&content, &format)?;
228
229 let mut config = Self::new();
230 config.file_path = Some(path_str);
231 config.format = format;
232
233 if let Value::Table(table) = value {
235 if let Ok(mut cache) = config.cache.write() {
236 *cache = table;
237 }
238 }
239
240 Ok(config)
241 }
242
243 pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
245 let format = format.unwrap_or("conf").to_string();
246 let value = Self::parse_content(content, &format)?;
247
248 let mut config = Self::new();
249 config.format = format;
250
251 if let Value::Table(table) = value {
253 if let Ok(mut cache) = config.cache.write() {
254 *cache = table;
255 }
256 }
257
258 Ok(config)
259 }
260
261 #[inline(always)]
263 pub fn get(&self, key: &str) -> Option<Value> {
264 if let Ok(mut fast_cache) = self.fast_cache.write() {
266 if let Some(value) = fast_cache.get(key) {
267 return Some(value.clone());
268 }
269 }
270
271 if let Ok(cache) = self.cache.read() {
273 if let Some(value) = self.get_nested(&cache, key) {
274 let value_clone = value.clone();
275 if let Ok(mut fast_cache) = self.fast_cache.write() {
277 fast_cache.insert(key.to_string(), value_clone.clone());
278 }
279 return Some(value_clone);
280 }
281 }
282
283 if let Ok(defaults) = self.defaults.read() {
285 if let Some(value) = self.get_nested(&defaults, key) {
286 let value_clone = value.clone();
287 if let Ok(mut fast_cache) = self.fast_cache.write() {
289 fast_cache.insert(key.to_string(), value_clone.clone());
290 }
291 return Some(value_clone);
292 }
293 }
294
295 None
296 }
297
298 pub fn get_or<T>(&self, key: &str, default: T) -> T
300 where
301 T: From<Value> + Clone,
302 {
303 if let Some(value) = self.get(key) {
304 T::from(value)
306 } else {
307 default
308 }
309 }
310
311 #[inline(always)]
313 pub fn get_or_default(&self, key: &str) -> Option<Value> {
314 if let Some(value) = self.get(key) {
315 Some(value)
316 } else {
317 if let Ok(defaults) = self.defaults.read() {
319 self.get_nested(&defaults, key).cloned()
320 } else {
321 None
322 }
323 }
324 }
325
326 #[inline(always)]
328 pub fn exists(&self, key: &str) -> bool {
329 if let Ok(cache) = self.cache.read() {
331 if self.get_nested(&cache, key).is_some() {
332 return true;
333 }
334 }
335
336 if let Ok(defaults) = self.defaults.read() {
338 self.get_nested(&defaults, key).is_some()
339 } else {
340 false
341 }
342 }
343
344 pub fn set(&mut self, key: &str, value: Value) -> Result<()> {
346 if let Ok(mut cache) = self.cache.write() {
347 self.set_nested(&mut cache, key, value.clone());
348
349 if let Ok(mut fast_cache) = self.fast_cache.write() {
351 fast_cache.hot_values.remove(key);
352 fast_cache.insert(key.to_string(), value);
354 }
355
356 Ok(())
357 } else {
358 Err(Error::general(
359 "Failed to acquire cache lock for write operation",
360 ))
361 }
362 }
363
364 pub fn cache_stats(&self) -> (u64, u64, f64) {
366 if let Ok(fast_cache) = self.fast_cache.read() {
367 let hit_ratio = if fast_cache.hits + fast_cache.misses > 0 {
368 fast_cache.hits as f64 / (fast_cache.hits + fast_cache.misses) as f64
369 } else {
370 0.0
371 };
372 (fast_cache.hits, fast_cache.misses, hit_ratio)
373 } else {
374 (0, 0, 0.0)
376 }
377 }
378
379 pub fn set_default(&mut self, key: &str, value: Value) {
381 if let Ok(mut defaults) = self.defaults.write() {
382 self.set_nested(&mut defaults, key, value);
383 }
384 }
385
386 pub fn save(&self) -> Result<()> {
388 if let Some(ref path) = self.file_path {
389 if let Ok(cache) = self.cache.read() {
390 let content = self.serialize_to_format(&cache, &self.format)?;
391 std::fs::write(path, content)?;
392 Ok(())
393 } else {
394 Err(Error::general(
395 "Failed to acquire cache lock for save operation",
396 ))
397 }
398 } else {
399 Err(Error::general("No file path specified for save"))
400 }
401 }
402
403 pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
405 let path_str = path.as_ref().to_string_lossy();
406 let format = Self::detect_format(&path_str);
407 if let Ok(cache) = self.cache.read() {
408 let content = self.serialize_to_format(&cache, &format)?;
409 std::fs::write(path, content)?;
410 Ok(())
411 } else {
412 Err(Error::general(
413 "Failed to acquire cache lock for save operation",
414 ))
415 }
416 }
417
418 pub fn keys(&self) -> Vec<String> {
420 if let Ok(cache) = self.cache.read() {
421 self.collect_keys(&cache, "")
422 } else {
423 Vec::new()
424 }
425 }
426
427 pub fn make_read_only(&mut self) {
429 self.read_only = true;
430 }
431
432 pub fn clear(&mut self) -> Result<()> {
434 if self.read_only {
435 return Err(Error::general("Configuration is read-only"));
436 }
437
438 let mut cache = self
439 .cache
440 .write()
441 .map_err(|_| Error::concurrency("Cache lock poisoned"))?;
442 cache.clear();
443 Ok(())
444 }
445
446 pub fn merge(&mut self, other: &EnterpriseConfig) -> Result<()> {
448 if self.read_only {
449 return Err(Error::general("Configuration is read-only"));
450 }
451 let other_cache = other
453 .cache
454 .read()
455 .map_err(|_| Error::concurrency("Other cache lock poisoned"))?;
456 let mut self_cache = self
457 .cache
458 .write()
459 .map_err(|_| Error::concurrency("Self cache lock poisoned"))?;
460
461 for (key, value) in other_cache.iter() {
463 self_cache.insert(key.clone(), value.clone());
466 }
467
468 Ok(())
469 }
470
471 fn detect_format(path: &str) -> String {
475 if path.ends_with(".json") {
476 "json".to_string()
477 } else if path.ends_with(".toml") {
478 "toml".to_string()
479 } else if path.ends_with(".noml") {
480 "noml".to_string()
481 } else {
482 "conf".to_string()
483 }
484 }
485
486 fn parse_content(content: &str, format: &str) -> Result<Value> {
488 match format {
489 "conf" => {
490 crate::parsers::conf::parse(content)
492 }
493 #[cfg(feature = "json")]
494 "json" => {
495 let parsed: serde_json::Value = serde_json::from_str(content)
496 .map_err(|e| Error::general(format!("JSON parse error: {e}")))?;
497 crate::parsers::json_parser::from_json_value(parsed)
498 }
499 #[cfg(feature = "toml")]
500 "toml" => crate::parsers::toml_parser::parse(content),
501 #[cfg(feature = "noml")]
502 "noml" => crate::parsers::noml_parser::parse(content),
503 _ => Err(Error::general(format!("Unsupported format: {format}"))),
504 }
505 }
506
507 #[inline(always)]
509 fn get_nested<'a>(&self, table: &'a BTreeMap<String, Value>, key: &str) -> Option<&'a Value> {
510 if !key.contains('.') {
511 return table.get(key);
512 }
513
514 let parts: Vec<&str> = key.split('.').collect();
515 let mut current = table.get(parts[0])?;
516
517 for part in &parts[1..] {
518 match current {
519 Value::Table(nested_table) => {
520 current = nested_table.get(*part)?;
521 }
522 _ => return None,
523 }
524 }
525
526 Some(current)
527 }
528
529 fn set_nested(&self, table: &mut BTreeMap<String, Value>, key: &str, value: Value) {
531 if !key.contains('.') {
532 table.insert(key.to_string(), value);
533 return;
534 }
535
536 let parts: Vec<&str> = key.split('.').collect();
537 set_recursive(table, &parts, value);
538 }
539
540 #[allow(clippy::only_used_in_recursion)]
542 fn collect_keys(&self, table: &BTreeMap<String, Value>, prefix: &str) -> Vec<String> {
543 let mut keys = Vec::new();
544
545 for (key, value) in table {
546 let full_key = if prefix.is_empty() {
547 key.clone()
548 } else {
549 format!("{prefix}.{key}")
550 };
551
552 keys.push(full_key.clone());
553
554 if let Value::Table(nested_table) = value {
555 keys.extend(self.collect_keys(nested_table, &full_key));
556 }
557 }
558
559 keys
560 }
561
562 fn serialize_to_format(&self, table: &BTreeMap<String, Value>, format: &str) -> Result<String> {
564 match format {
565 "conf" => {
566 let mut output = String::new();
568 for (key, value) in table {
569 output.push_str(&format!("{} = {}\n", key, self.value_to_string(value)));
570 }
571 Ok(output)
572 }
573 #[cfg(feature = "json")]
574 "json" => {
575 let json_value =
576 crate::parsers::json_parser::to_json_value(&Value::table(table.clone()))?;
577 serde_json::to_string_pretty(&json_value)
578 .map_err(|e| Error::general(format!("JSON serialize error: {e}")))
579 }
580 _ => Err(Error::general(format!(
581 "Serialization not supported for format: {format}"
582 ))),
583 }
584 }
585
586 #[allow(clippy::only_used_in_recursion)]
588 fn value_to_string(&self, value: &Value) -> String {
589 match value {
590 Value::String(s) => format!("\"{s}\""),
591 Value::Integer(i) => i.to_string(),
592 Value::Float(f) => f.to_string(),
593 Value::Bool(b) => b.to_string(),
594 Value::Null => "null".to_string(),
595 Value::Array(arr) => {
596 let items: Vec<String> = arr.iter().map(|v| self.value_to_string(v)).collect();
597 items.join(" ")
598 }
599 Value::Table(_) => "[Table]".to_string(), #[cfg(feature = "chrono")]
601 Value::DateTime(dt) => dt.to_rfc3339(),
602 }
603 }
604}
605
606impl ConfigManager {
607 pub fn new() -> Self {
609 Self::default()
610 }
611
612 pub fn load<P: AsRef<Path>>(&self, name: &str, path: P) -> Result<()> {
614 let config = EnterpriseConfig::from_file(path)?;
615 let mut configs = self
616 .configs
617 .write()
618 .map_err(|_| Error::concurrency("Configs lock poisoned"))?;
619 configs.insert(name.to_string(), config);
620 Ok(())
621 }
622
623 pub fn get(&self, name: &str) -> Option<Arc<RwLock<EnterpriseConfig>>> {
625 let configs = self.configs.read().ok()?;
626 configs.get(name).map(|config| {
627 Arc::new(RwLock::new(EnterpriseConfig {
629 fast_cache: config.fast_cache.clone(),
630 cache: config.cache.clone(),
631 defaults: config.defaults.clone(),
632 file_path: config.file_path.clone(),
633 format: config.format.clone(),
634 read_only: config.read_only,
635 }))
636 })
637 }
638
639 pub fn list(&self) -> Vec<String> {
641 match self.configs.read() {
642 Ok(configs) => configs.keys().cloned().collect(),
643 Err(_) => Vec::new(), }
645 }
646
647 pub fn remove(&self, name: &str) -> bool {
649 match self.configs.write() {
650 Ok(mut configs) => configs.remove(name).is_some(),
651 Err(_) => false, }
653 }
654}
655
656pub mod direct {
659 use super::*;
660
661 #[deprecated(
671 since = "0.9.4",
672 note = "use `config_lib::parse_file` — same routing, fewer namespaces"
673 )]
674 #[inline(always)]
675 pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Value> {
676 let content = std::fs::read_to_string(path)?;
677 parse_string(&content, None)
678 }
679
680 #[deprecated(
690 since = "0.9.4",
691 note = "use `config_lib::parse` — same routing, fewer namespaces"
692 )]
693 #[inline(always)]
694 pub fn parse_string(content: &str, format: Option<&str>) -> Result<Value> {
695 let format = format.unwrap_or("conf");
696 EnterpriseConfig::parse_content(content, format)
697 }
698
699 #[inline(always)]
701 pub fn parse_to_vec<T>(content: &str) -> Result<Vec<T>>
702 where
703 T: TryFrom<Value>,
704 T::Error: std::fmt::Display,
705 {
706 let value = parse_string(content, None)?;
707
708 match value {
709 Value::Array(arr) => arr
710 .into_iter()
711 .map(|v| T::try_from(v).map_err(|e| Error::general(e.to_string())))
712 .collect(),
713 _ => Err(Error::general("Expected array value")),
714 }
715 }
716}
717
718fn set_recursive(table: &mut BTreeMap<String, Value>, parts: &[&str], value: Value) {
723 if parts.len() == 1 {
724 table.insert(parts[0].to_string(), value);
725 return;
726 }
727
728 let key = parts[0].to_string();
729 let remaining = &parts[1..];
730
731 if !table.contains_key(&key) {
732 table.insert(key.clone(), Value::table(BTreeMap::new()));
733 }
734
735 if let Some(entry) = table.get_mut(&key) {
736 if !entry.is_table() {
737 *entry = Value::table(BTreeMap::new());
738 }
739 if let Value::Table(nested_table) = entry {
740 set_recursive(nested_table, remaining, value);
741 }
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
750 fn test_enterprise_config_get_or() {
751 let mut config = EnterpriseConfig::new();
752 config.set("port", Value::integer(8080)).unwrap();
753
754 if let Some(port_value) = config.get("port") {
756 let port = port_value.as_integer().unwrap_or(3000);
757 assert_eq!(port, 8080);
758 }
759
760 if config.get("timeout").is_some() {
762 panic!("Should not find timeout key");
763 }
764
765 let timeout = config
767 .get("timeout")
768 .and_then(|v| v.as_integer().ok())
769 .unwrap_or(30);
770 assert_eq!(timeout, 30);
771 }
772
773 #[test]
774 fn test_exists() {
775 let mut config = EnterpriseConfig::new();
776 config.set("debug", Value::bool(true)).unwrap();
777
778 assert!(config.exists("debug"));
779 assert!(!config.exists("production"));
780 }
781
782 #[test]
783 fn test_nested_keys() {
784 let mut config = EnterpriseConfig::new();
785 config
786 .set("database.host", Value::string("localhost"))
787 .unwrap();
788 config.set("database.port", Value::integer(5432)).unwrap();
789
790 assert_eq!(
791 config.get("database.host").unwrap().as_string().unwrap(),
792 "localhost"
793 );
794 assert_eq!(
795 config.get("database.port").unwrap().as_integer().unwrap(),
796 5432
797 );
798 assert!(config.exists("database.host"));
799 }
800
801 #[test]
802 fn test_direct_parsing() {
803 let content = "port = 8080\ndebug = true";
804 let value = direct::parse_string(content, Some("conf")).unwrap();
805
806 if let Value::Table(table) = value {
807 assert_eq!(table.get("port").unwrap().as_integer().unwrap(), 8080);
808 assert!(table.get("debug").unwrap().as_bool().unwrap());
809 } else {
810 panic!("Expected table value");
811 }
812 }
813}