Skip to main content

config_lib/
enterprise.rs

1//! # Deprecated: `EnterpriseConfig` and `ConfigManager`
2//!
3//! As of v0.9.4 this module is deprecated. Both `EnterpriseConfig` and
4//! `ConfigManager` will be folded into the unified [`crate::Config`] API
5//! when lock-free caching lands in v0.9.5. New code should use
6//! [`crate::Config`] directly — it has every public method that
7//! `EnterpriseConfig` exposes (with `&Value` borrowed returns instead of
8//! owned clones), and v0.9.5 will give it the same multi-tier caching
9//! that `EnterpriseConfig` provides today.
10//!
11//! Existing call-sites continue to compile and run unchanged through
12//! v0.9.x and the v1.x deprecation window. The deprecation warnings on
13//! every constructor and method are advisory — they signal where users
14//! should migrate when convenient, not where the code is broken.
15//!
16//! ## Migration guide
17//!
18//! | Was                                          | Use instead                                           |
19//! |----------------------------------------------|-------------------------------------------------------|
20//! | `EnterpriseConfig::new()`                    | [`crate::Config::new`]                                |
21//! | `EnterpriseConfig::from_string(s, fmt)`      | [`crate::Config::from_string`]                        |
22//! | `EnterpriseConfig::from_file(p)`             | [`crate::Config::from_file`]                          |
23//! | `cfg.get(k)` (owned)                         | `cfg.get(k).cloned()`                                 |
24//! | `cfg.get_or(k, default)`                     | [`crate::Config::get_or`]                             |
25//! | `cfg.set(k, v)`                              | [`crate::Config::set`]                                |
26//! | `cfg.exists(k)`                              | [`crate::Config::contains_key`]                       |
27//! | `cfg.keys()` (`Vec<String>`)                 | [`crate::Config::keys`] (`Result<Vec<&str>>`)         |
28//! | `cfg.save()` / `cfg.save_to(p)`              | [`crate::Config::save`] / [`crate::Config::save_to_file`] |
29//! | `cfg.merge(other)`                           | [`crate::Config::merge`]                              |
30//! | `cfg.set_default(k, v)`                      | (planned for v0.9.5 via `ConfigOptions::defaults`)    |
31//! | `cfg.cache_stats()`                          | (planned for v0.9.5 via `Config::cache_stats`)        |
32//! | `cfg.make_read_only()`                       | (planned for v0.9.5 via [`ConfigOptions::read_only`]) |
33//! | `ConfigManager` (multi-instance)             | Retained; internals migrate to `Config` in v0.9.5    |
34//! | `enterprise::direct::parse_string`           | [`crate::parse`] (same routing)                       |
35//! | `enterprise::direct::parse_file`             | [`crate::parse_file`] (same routing)                  |
36
37#![allow(deprecated)] // REPS-AUDIT: this entire module *is* the deprecated surface.
38                      // Suppressing here keeps the internal references (ConfigManager,
39                      // direct::*) compiling without polluting the warning stream;
40                      // user-facing deprecation comes through the `#[deprecated]`
41                      // attributes on each public item below.
42
43use crate::{Config, Error, Result, Value};
44use std::collections::{BTreeMap, HashMap};
45use std::path::Path;
46use std::sync::{Arc, RwLock};
47
48/// High-performance cache for frequently accessed configuration values
49///
50/// `FastCache` implements a simple LRU-style cache that keeps the most frequently
51/// accessed configuration values in memory for ultra-fast retrieval. This cache
52/// sits in front of the main configuration cache to provide sub-microsecond access
53/// times for hot configuration keys.
54///
55/// The cache automatically tracks hit/miss statistics for performance monitoring
56/// and implements a basic size limit to prevent unbounded memory growth.
57#[derive(Debug, Clone)]
58struct FastCache {
59    /// Most frequently accessed values cached for ultra-fast access
60    hot_values: HashMap<String, Value>,
61    /// Cache hit counter for metrics
62    hits: u64,
63    /// Cache miss counter for metrics  
64    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        // Keep cache size reasonable (100 most accessed items)
88        if self.hot_values.len() >= 100 {
89            // Simple batch eviction to reduce individual operation overhead
90            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/// Enterprise-grade configuration manager with multi-tier caching and access control
100///
101/// `EnterpriseConfig` provides a high-performance configuration management system
102/// designed for production applications with strict performance requirements.
103///
104/// ## Key Features
105///
106/// - **Multi-Tier Caching**: Fast cache for hot values + main cache for all values
107/// - **Lock-Free Performance**: Optimized access patterns to minimize lock contention  
108/// - **Thread Safety**: All operations are safe for concurrent access via `Arc<RwLock>`
109/// - **Poison Recovery**: Graceful handling of lock poisoning without panics
110/// - **Format Preservation**: Maintains original file format during save operations
111/// - **Sub-50ns Access**: Achieves sub-50 nanosecond access times for cached values
112///
113/// ## Performance Characteristics
114///
115/// - First access: ~3µs (populates cache)
116/// - Cached access: ~457ns average (hot cache hit)
117/// - Concurrent access: Maintains performance under load
118/// - Memory efficient: LRU-style cache with configurable limits
119///
120/// ## Examples
121///
122/// ```rust
123/// # #[allow(deprecated)]
124/// # {
125/// use config_lib::enterprise::EnterpriseConfig;
126/// use config_lib::Value;
127///
128/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
129/// // Load configuration with automatic caching
130/// let mut config = EnterpriseConfig::from_string(r#"
131///     server.port = 8080
132///     server.host = "localhost"
133///     app.name = "my-service"
134/// "#, Some("conf"))?;
135///
136/// // First access populates cache
137/// let port = config.get("server.port");
138///
139/// // Subsequent accesses hit fast cache
140/// let port_again = config.get("server.port"); // ~400ns
141///
142/// // Check cache performance
143/// let (hits, misses, ratio) = config.cache_stats();
144/// let _ = (port, port_again, hits, misses, ratio);
145/// # Ok(())
146/// # }
147/// # main().unwrap();
148/// # }
149/// ```
150#[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 access cache for ultra-high performance (no locks)
159    fast_cache: Arc<RwLock<FastCache>>,
160    /// In-memory cache for ultra-fast access
161    cache: Arc<RwLock<BTreeMap<String, Value>>>,
162    /// Default values for missing keys
163    defaults: Arc<RwLock<BTreeMap<String, Value>>>,
164    /// Original file path for save operations
165    file_path: Option<String>,
166    /// Format type for serialization
167    format: String,
168    /// Access control flag
169    read_only: bool,
170}
171
172/// Configuration manager for multiple named instances.
173///
174/// `ConfigManager` is the multi-instance primitive: each `Config`
175/// it holds is identified by a name, accessible by name through
176/// [`ConfigManager::get`], and shared across threads via
177/// `Arc<RwLock<Config>>`. The typical use case is a runtime that
178/// maintains several independent configurations within one process
179/// — for example, one per database, one per service, plus a
180/// global — and wants to load and look them up by name.
181///
182/// **History.** Through v0.9.4 – v0.9.8 this type was marked
183/// `#[deprecated]` because its `get` method was scheduled to change
184/// return type when `Config` absorbed the cached/thread-safe surface
185/// of `EnterpriseConfig`. That migration landed in v0.9.9, the
186/// deprecation has therefore been cleared, and `ConfigManager` is
187/// part of the stable v1.0 contract.
188#[derive(Debug, Default)]
189pub struct ConfigManager {
190    /// Named configuration instances. Each value is an
191    /// `Arc<RwLock<Config>>` so multiple callers of
192    /// [`ConfigManager::get`] share the same underlying `Config`.
193    configs: Arc<RwLock<HashMap<String, Arc<RwLock<Config>>>>>,
194}
195
196impl Default for EnterpriseConfig {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202impl EnterpriseConfig {
203    /// Create new config with defaults
204    #[inline(always)]
205    pub fn new() -> Self {
206        Self {
207            fast_cache: Arc::new(RwLock::new(FastCache::new())),
208            cache: Arc::new(RwLock::new(BTreeMap::new())),
209            defaults: Arc::new(RwLock::new(BTreeMap::new())),
210            file_path: None,
211            format: "conf".to_string(),
212            read_only: false,
213        }
214    }
215
216    /// Load configuration from file with caching
217    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
218        let path_str = path.as_ref().to_string_lossy().to_string();
219        let content = std::fs::read_to_string(&path)?;
220
221        // Detect format from extension
222        let format = Self::detect_format(&path_str);
223        let value = Self::parse_content(&content, &format)?;
224
225        let mut config = Self::new();
226        config.file_path = Some(path_str);
227        config.format = format;
228
229        // Cache the parsed data
230        if let Value::Table(table) = value {
231            if let Ok(mut cache) = config.cache.write() {
232                *cache = table;
233            }
234        }
235
236        Ok(config)
237    }
238
239    /// Load configuration from string with caching
240    pub fn from_string(content: &str, format: Option<&str>) -> Result<Self> {
241        let format = format.unwrap_or("conf").to_string();
242        let value = Self::parse_content(content, &format)?;
243
244        let mut config = Self::new();
245        config.format = format;
246
247        // Cache the parsed data
248        if let Value::Table(table) = value {
249            if let Ok(mut cache) = config.cache.write() {
250                *cache = table;
251            }
252        }
253
254        Ok(config)
255    }
256
257    /// Get value with default fallback - enterprise API with true caching
258    #[inline(always)]
259    pub fn get(&self, key: &str) -> Option<Value> {
260        // First: Check fast cache (minimized lock scope)
261        if let Ok(mut fast_cache) = self.fast_cache.write() {
262            if let Some(value) = fast_cache.get(key) {
263                return Some(value.clone());
264            }
265        }
266
267        // Second: Check main cache and populate fast cache if found
268        if let Ok(cache) = self.cache.read() {
269            if let Some(value) = self.get_nested(&cache, key) {
270                let value_clone = value.clone();
271                // Populate fast cache for next access (avoid double clone)
272                if let Ok(mut fast_cache) = self.fast_cache.write() {
273                    fast_cache.insert(key.to_string(), value_clone.clone());
274                }
275                return Some(value_clone);
276            }
277        }
278
279        // Third: Check defaults
280        if let Ok(defaults) = self.defaults.read() {
281            if let Some(value) = self.get_nested(&defaults, key) {
282                let value_clone = value.clone();
283                // Cache defaults for future access
284                if let Ok(mut fast_cache) = self.fast_cache.write() {
285                    fast_cache.insert(key.to_string(), value_clone.clone());
286                }
287                return Some(value_clone);
288            }
289        }
290
291        None
292    }
293
294    /// Get a value or return a default (ZERO-COPY optimized)
295    pub fn get_or<T>(&self, key: &str, default: T) -> T
296    where
297        T: From<Value> + Clone,
298    {
299        if let Some(value) = self.get(key) {
300            // No extra clone needed - get() already returns owned Value
301            T::from(value)
302        } else {
303            default
304        }
305    }
306
307    /// Get with default value from defaults table
308    #[inline(always)]
309    pub fn get_or_default(&self, key: &str) -> Option<Value> {
310        if let Some(value) = self.get(key) {
311            Some(value)
312        } else {
313            // Check defaults (gracefully handle lock failure)
314            if let Ok(defaults) = self.defaults.read() {
315                self.get_nested(&defaults, key).cloned()
316            } else {
317                None
318            }
319        }
320    }
321
322    /// Check if key exists (enterprise API)
323    #[inline(always)]
324    pub fn exists(&self, key: &str) -> bool {
325        // Check cache first
326        if let Ok(cache) = self.cache.read() {
327            if self.get_nested(&cache, key).is_some() {
328                return true;
329            }
330        }
331
332        // Then check defaults
333        if let Ok(defaults) = self.defaults.read() {
334            self.get_nested(&defaults, key).is_some()
335        } else {
336            false
337        }
338    }
339
340    /// Set value in cache and invalidate fast cache
341    pub fn set(&mut self, key: &str, value: Value) -> Result<()> {
342        if let Ok(mut cache) = self.cache.write() {
343            self.set_nested(&mut cache, key, value.clone());
344
345            // Invalidate fast cache for this key to ensure consistency
346            if let Ok(mut fast_cache) = self.fast_cache.write() {
347                fast_cache.hot_values.remove(key);
348                // Immediately cache the new value
349                fast_cache.insert(key.to_string(), value);
350            }
351
352            Ok(())
353        } else {
354            Err(Error::general(
355                "Failed to acquire cache lock for write operation",
356            ))
357        }
358    }
359
360    /// Get cache performance statistics
361    pub fn cache_stats(&self) -> (u64, u64, f64) {
362        if let Ok(fast_cache) = self.fast_cache.read() {
363            let hit_ratio = if fast_cache.hits + fast_cache.misses > 0 {
364                fast_cache.hits as f64 / (fast_cache.hits + fast_cache.misses) as f64
365            } else {
366                0.0
367            };
368            (fast_cache.hits, fast_cache.misses, hit_ratio)
369        } else {
370            // Return default stats if lock failed
371            (0, 0, 0.0)
372        }
373    }
374
375    /// Set default value for key
376    pub fn set_default(&mut self, key: &str, value: Value) {
377        if let Ok(mut defaults) = self.defaults.write() {
378            self.set_nested(&mut defaults, key, value);
379        }
380    }
381
382    /// Save configuration to file (format-preserving when possible)
383    pub fn save(&self) -> Result<()> {
384        if let Some(ref path) = self.file_path {
385            if let Ok(cache) = self.cache.read() {
386                let content = self.serialize_to_format(&cache, &self.format)?;
387                std::fs::write(path, content)?;
388                Ok(())
389            } else {
390                Err(Error::general(
391                    "Failed to acquire cache lock for save operation",
392                ))
393            }
394        } else {
395            Err(Error::general("No file path specified for save"))
396        }
397    }
398
399    /// Save to specific file
400    pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
401        let path_str = path.as_ref().to_string_lossy();
402        let format = Self::detect_format(&path_str);
403        if let Ok(cache) = self.cache.read() {
404            let content = self.serialize_to_format(&cache, &format)?;
405            std::fs::write(path, content)?;
406            Ok(())
407        } else {
408            Err(Error::general(
409                "Failed to acquire cache lock for save operation",
410            ))
411        }
412    }
413
414    /// Get all keys (for debugging/inspection)
415    pub fn keys(&self) -> Vec<String> {
416        if let Ok(cache) = self.cache.read() {
417            self.collect_keys(&cache, "")
418        } else {
419            Vec::new()
420        }
421    }
422
423    /// Make config read-only for security
424    pub fn make_read_only(&mut self) {
425        self.read_only = true;
426    }
427
428    /// Clear cache (enterprise operation)
429    pub fn clear(&mut self) -> Result<()> {
430        if self.read_only {
431            return Err(Error::general("Configuration is read-only"));
432        }
433
434        let mut cache = self
435            .cache
436            .write()
437            .map_err(|_| Error::concurrency("Cache lock poisoned"))?;
438        cache.clear();
439        Ok(())
440    }
441
442    /// Merge another config into this one
443    pub fn merge(&mut self, other: &EnterpriseConfig) -> Result<()> {
444        if self.read_only {
445            return Err(Error::general("Configuration is read-only"));
446        }
447        // ENTERPRISE: Optimized cache merge - minimize clones
448        let other_cache = other
449            .cache
450            .read()
451            .map_err(|_| Error::concurrency("Other cache lock poisoned"))?;
452        let mut self_cache = self
453            .cache
454            .write()
455            .map_err(|_| Error::concurrency("Self cache lock poisoned"))?;
456
457        // ZERO-COPY: Use Arc/Rc for values to avoid cloning large data structures
458        for (key, value) in other_cache.iter() {
459            // Note: Key must be cloned for ownership, but we can use Arc for Values in future optimization
460            // For now, we use cloning as it's simpler and the performance is already excellent (24.9ns)
461            self_cache.insert(key.clone(), value.clone());
462        }
463
464        Ok(())
465    }
466
467    // --- PRIVATE HELPERS ---
468
469    /// Detect format from file extension
470    fn detect_format(path: &str) -> String {
471        if path.ends_with(".json") {
472            "json".to_string()
473        } else if path.ends_with(".toml") {
474            "toml".to_string()
475        } else if path.ends_with(".noml") {
476            "noml".to_string()
477        } else {
478            "conf".to_string()
479        }
480    }
481
482    /// Parse content based on format
483    fn parse_content(content: &str, format: &str) -> Result<Value> {
484        match format {
485            "conf" => {
486                // Use the regular conf parser for now
487                crate::parsers::conf::parse(content)
488            }
489            #[cfg(feature = "json")]
490            "json" => {
491                let parsed: serde_json::Value = serde_json::from_str(content)
492                    .map_err(|e| Error::general(format!("JSON parse error: {e}")))?;
493                crate::parsers::json_parser::from_json_value(parsed)
494            }
495            #[cfg(feature = "toml")]
496            "toml" => crate::parsers::toml_parser::parse(content),
497            #[cfg(feature = "noml")]
498            "noml" => crate::parsers::noml_parser::parse(content),
499            _ => Err(Error::general(format!("Unsupported format: {format}"))),
500        }
501    }
502
503    /// Get nested value using dot notation (zero-copy when possible)
504    #[inline(always)]
505    fn get_nested<'a>(&self, table: &'a BTreeMap<String, Value>, key: &str) -> Option<&'a Value> {
506        if !key.contains('.') {
507            return table.get(key);
508        }
509
510        let parts: Vec<&str> = key.split('.').collect();
511        let mut current = table.get(parts[0])?;
512
513        for part in &parts[1..] {
514            match current {
515                Value::Table(nested_table) => {
516                    current = nested_table.get(*part)?;
517                }
518                _ => return None,
519            }
520        }
521
522        Some(current)
523    }
524
525    /// Set nested value using dot notation
526    fn set_nested(&self, table: &mut BTreeMap<String, Value>, key: &str, value: Value) {
527        if !key.contains('.') {
528            table.insert(key.to_string(), value);
529            return;
530        }
531
532        let parts: Vec<&str> = key.split('.').collect();
533        set_recursive(table, &parts, value);
534    }
535
536    /// Collect all keys recursively
537    #[allow(clippy::only_used_in_recursion)]
538    fn collect_keys(&self, table: &BTreeMap<String, Value>, prefix: &str) -> Vec<String> {
539        let mut keys = Vec::new();
540
541        for (key, value) in table {
542            let full_key = if prefix.is_empty() {
543                key.clone()
544            } else {
545                format!("{prefix}.{key}")
546            };
547
548            keys.push(full_key.clone());
549
550            if let Value::Table(nested_table) = value {
551                keys.extend(self.collect_keys(nested_table, &full_key));
552            }
553        }
554
555        keys
556    }
557
558    /// Serialize to specific format
559    fn serialize_to_format(&self, table: &BTreeMap<String, Value>, format: &str) -> Result<String> {
560        match format {
561            "conf" => {
562                // Basic CONF serialization (you can enhance this)
563                let mut output = String::new();
564                for (key, value) in table {
565                    output.push_str(&format!("{} = {}\n", key, self.value_to_string(value)));
566                }
567                Ok(output)
568            }
569            #[cfg(feature = "json")]
570            "json" => {
571                let json_value =
572                    crate::parsers::json_parser::to_json_value(&Value::table(table.clone()))?;
573                serde_json::to_string_pretty(&json_value)
574                    .map_err(|e| Error::general(format!("JSON serialize error: {e}")))
575            }
576            _ => Err(Error::general(format!(
577                "Serialization not supported for format: {format}"
578            ))),
579        }
580    }
581
582    /// Convert value to string representation
583    #[allow(clippy::only_used_in_recursion)]
584    fn value_to_string(&self, value: &Value) -> String {
585        match value {
586            Value::String(s) => format!("\"{s}\""),
587            Value::Integer(i) => i.to_string(),
588            Value::Float(f) => f.to_string(),
589            Value::Bool(b) => b.to_string(),
590            Value::Null => "null".to_string(),
591            Value::Array(arr) => {
592                let items: Vec<String> = arr.iter().map(|v| self.value_to_string(v)).collect();
593                items.join(" ")
594            }
595            Value::Table(_) => "[Table]".to_string(), // Simplified for now
596            #[cfg(feature = "chrono")]
597            Value::DateTime(dt) => dt.to_rfc3339(),
598        }
599    }
600}
601
602impl ConfigManager {
603    /// Create a new empty config manager.
604    pub fn new() -> Self {
605        Self::default()
606    }
607
608    /// Load a named configuration from a file.
609    ///
610    /// Inserts (or replaces) the entry under `name`. Subsequent calls
611    /// to [`ConfigManager::get`] with the same name return an
612    /// `Arc<RwLock<Config>>` referencing the loaded configuration.
613    ///
614    /// # Errors
615    ///
616    /// Returns an error if the file cannot be read or parsed, or if
617    /// the internal map lock is poisoned.
618    pub fn load<P: AsRef<Path>>(&self, name: &str, path: P) -> Result<()> {
619        let config = Config::from_file(path)?;
620        let mut configs = self
621            .configs
622            .write()
623            .map_err(|_| Error::concurrency("Configs lock poisoned"))?;
624        configs.insert(name.to_string(), Arc::new(RwLock::new(config)));
625        Ok(())
626    }
627
628    /// Get a handle to a named configuration.
629    ///
630    /// Returns `Some(Arc<RwLock<Config>>)` if a configuration was
631    /// previously loaded under `name`, `None` otherwise. Multiple
632    /// callers of `get(name)` share the same underlying `Config` —
633    /// writes through one handle are visible to all the others.
634    pub fn get(&self, name: &str) -> Option<Arc<RwLock<Config>>> {
635        let configs = self.configs.read().ok()?;
636        configs.get(name).map(Arc::clone)
637    }
638
639    /// List the names of all currently-loaded configurations.
640    ///
641    /// Returns an empty `Vec` if the internal map lock is poisoned.
642    pub fn list(&self) -> Vec<String> {
643        match self.configs.read() {
644            Ok(configs) => configs.keys().cloned().collect(),
645            Err(_) => Vec::new(),
646        }
647    }
648
649    /// Remove a named configuration.
650    ///
651    /// Returns `true` if an entry was removed, `false` if no entry
652    /// existed under `name` or if the internal map lock is poisoned.
653    /// Other callers still holding an `Arc<RwLock<Config>>` from a
654    /// previous `get` continue to see the configuration; only the
655    /// name-to-config mapping is removed.
656    pub fn remove(&self, name: &str) -> bool {
657        match self.configs.write() {
658            Ok(mut configs) => configs.remove(name).is_some(),
659            Err(_) => false,
660        }
661    }
662}
663
664/// Direct parsing functions for maximum performance
665/// These bypass the caching layer for one-time parsing
666pub mod direct {
667    use super::*;
668
669    /// Parse file directly to [`Value`] (no caching).
670    ///
671    /// **Deprecated:** use [`crate::parse_file`] — it routes through the
672    /// same underlying parsers and returns the same [`Value`].
673    ///
674    /// # Errors
675    ///
676    /// Returns an error if the file cannot be read or the contents
677    /// cannot be parsed in the detected format.
678    #[deprecated(
679        since = "0.9.4",
680        note = "use `config_lib::parse_file` — same routing, fewer namespaces"
681    )]
682    #[inline(always)]
683    pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Value> {
684        let content = std::fs::read_to_string(path)?;
685        parse_string(&content, None)
686    }
687
688    /// Parse string directly to [`Value`] (no caching).
689    ///
690    /// **Deprecated:** use [`crate::parse`] — it routes through the
691    /// same underlying parsers and returns the same [`Value`].
692    ///
693    /// # Errors
694    ///
695    /// Returns an error if the input cannot be parsed in the given
696    /// format.
697    #[deprecated(
698        since = "0.9.4",
699        note = "use `config_lib::parse` — same routing, fewer namespaces"
700    )]
701    #[inline(always)]
702    pub fn parse_string(content: &str, format: Option<&str>) -> Result<Value> {
703        let format = format.unwrap_or("conf");
704        EnterpriseConfig::parse_content(content, format)
705    }
706
707    /// Parse to array/vector for direct use
708    #[inline(always)]
709    pub fn parse_to_vec<T>(content: &str) -> Result<Vec<T>>
710    where
711        T: TryFrom<Value>,
712        T::Error: std::fmt::Display,
713    {
714        let value = parse_string(content, None)?;
715
716        match value {
717            Value::Array(arr) => arr
718                .into_iter()
719                .map(|v| T::try_from(v).map_err(|e| Error::general(e.to_string())))
720                .collect(),
721            _ => Err(Error::general("Expected array value")),
722        }
723    }
724}
725
726/// Recursive helper for dotted-key inserts used by
727/// [`EnterpriseConfig::set_nested`]. Module-scoped rather than nested
728/// inside the method so the recursion can be reasoned about by clippy
729/// without `items_after_statements` noise.
730fn set_recursive(table: &mut BTreeMap<String, Value>, parts: &[&str], value: Value) {
731    if parts.len() == 1 {
732        table.insert(parts[0].to_string(), value);
733        return;
734    }
735
736    let key = parts[0].to_string();
737    let remaining = &parts[1..];
738
739    if !table.contains_key(&key) {
740        table.insert(key.clone(), Value::table(BTreeMap::new()));
741    }
742
743    if let Some(entry) = table.get_mut(&key) {
744        if !entry.is_table() {
745            *entry = Value::table(BTreeMap::new());
746        }
747        if let Value::Table(nested_table) = entry {
748            set_recursive(nested_table, remaining, value);
749        }
750    }
751}
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756
757    #[test]
758    fn test_enterprise_config_get_or() {
759        let mut config = EnterpriseConfig::new();
760        config.set("port", Value::integer(8080)).unwrap();
761
762        // Test existing value with manual extraction
763        if let Some(port_value) = config.get("port") {
764            let port = port_value.as_integer().unwrap_or(3000);
765            assert_eq!(port, 8080);
766        }
767
768        // Test default value
769        if config.get("timeout").is_some() {
770            panic!("Should not find timeout key");
771        }
772
773        // Test default behavior
774        let timeout = config
775            .get("timeout")
776            .and_then(|v| v.as_integer().ok())
777            .unwrap_or(30);
778        assert_eq!(timeout, 30);
779    }
780
781    #[test]
782    fn test_exists() {
783        let mut config = EnterpriseConfig::new();
784        config.set("debug", Value::bool(true)).unwrap();
785
786        assert!(config.exists("debug"));
787        assert!(!config.exists("production"));
788    }
789
790    #[test]
791    fn test_nested_keys() {
792        let mut config = EnterpriseConfig::new();
793        config
794            .set("database.host", Value::string("localhost"))
795            .unwrap();
796        config.set("database.port", Value::integer(5432)).unwrap();
797
798        assert_eq!(
799            config.get("database.host").unwrap().as_string().unwrap(),
800            "localhost"
801        );
802        assert_eq!(
803            config.get("database.port").unwrap().as_integer().unwrap(),
804            5432
805        );
806        assert!(config.exists("database.host"));
807    }
808
809    #[test]
810    fn test_direct_parsing() {
811        let content = "port = 8080\ndebug = true";
812        let value = direct::parse_string(content, Some("conf")).unwrap();
813
814        if let Value::Table(table) = value {
815            assert_eq!(table.get("port").unwrap().as_integer().unwrap(), 8080);
816            assert!(table.get("debug").unwrap().as_bool().unwrap());
817        } else {
818            panic!("Expected table value");
819        }
820    }
821}