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::{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` itself survives the v0.9.4 → v0.9.5 transition — it
175/// is a useful primitive for runtimes that maintain several independent
176/// configurations within one process. Its **internal storage type**
177/// changes in v0.9.5: today it holds [`EnterpriseConfig`] instances;
178/// v0.9.5 swaps that to [`crate::Config`] once the latter has
179/// equivalent caching semantics. The public method surface
180/// (`load` / `get` / `list` / `remove`) does not change.
181///
182/// The struct carries a `#[deprecated]` notice through v0.9.4 to
183/// signal that the **return type of `get`** changes shape in v0.9.5
184/// (it will hand back a `Config` handle, not an `EnterpriseConfig`).
185/// Callers that only use `load` / `list` / `remove` can ignore the
186/// warning; callers that consume the `Arc<RwLock<EnterpriseConfig>>`
187/// returned by `get` should plan to migrate when v0.9.5 lands.
188#[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    /// Named configuration instances
197    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    /// Create new config with defaults
208    #[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    /// Load configuration from file with caching
221    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        // Detect format from extension
226        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        // Cache the parsed data
234        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    /// Load configuration from string with caching
244    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        // Cache the parsed data
252        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    /// Get value with default fallback - enterprise API with true caching
262    #[inline(always)]
263    pub fn get(&self, key: &str) -> Option<Value> {
264        // First: Check fast cache (minimized lock scope)
265        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        // Second: Check main cache and populate fast cache if found
272        if let Ok(cache) = self.cache.read() {
273            if let Some(value) = self.get_nested(&cache, key) {
274                let value_clone = value.clone();
275                // Populate fast cache for next access (avoid double clone)
276                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        // Third: Check defaults
284        if let Ok(defaults) = self.defaults.read() {
285            if let Some(value) = self.get_nested(&defaults, key) {
286                let value_clone = value.clone();
287                // Cache defaults for future access
288                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    /// Get a value or return a default (ZERO-COPY optimized)
299    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            // No extra clone needed - get() already returns owned Value
305            T::from(value)
306        } else {
307            default
308        }
309    }
310
311    /// Get with default value from defaults table
312    #[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            // Check defaults (gracefully handle lock failure)
318            if let Ok(defaults) = self.defaults.read() {
319                self.get_nested(&defaults, key).cloned()
320            } else {
321                None
322            }
323        }
324    }
325
326    /// Check if key exists (enterprise API)
327    #[inline(always)]
328    pub fn exists(&self, key: &str) -> bool {
329        // Check cache first
330        if let Ok(cache) = self.cache.read() {
331            if self.get_nested(&cache, key).is_some() {
332                return true;
333            }
334        }
335
336        // Then check defaults
337        if let Ok(defaults) = self.defaults.read() {
338            self.get_nested(&defaults, key).is_some()
339        } else {
340            false
341        }
342    }
343
344    /// Set value in cache and invalidate fast cache
345    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            // Invalidate fast cache for this key to ensure consistency
350            if let Ok(mut fast_cache) = self.fast_cache.write() {
351                fast_cache.hot_values.remove(key);
352                // Immediately cache the new value
353                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    /// Get cache performance statistics
365    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            // Return default stats if lock failed
375            (0, 0, 0.0)
376        }
377    }
378
379    /// Set default value for key
380    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    /// Save configuration to file (format-preserving when possible)
387    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    /// Save to specific file
404    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    /// Get all keys (for debugging/inspection)
419    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    /// Make config read-only for security
428    pub fn make_read_only(&mut self) {
429        self.read_only = true;
430    }
431
432    /// Clear cache (enterprise operation)
433    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    /// Merge another config into this one
447    pub fn merge(&mut self, other: &EnterpriseConfig) -> Result<()> {
448        if self.read_only {
449            return Err(Error::general("Configuration is read-only"));
450        }
451        // ENTERPRISE: Optimized cache merge - minimize clones
452        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        // ZERO-COPY: Use Arc/Rc for values to avoid cloning large data structures
462        for (key, value) in other_cache.iter() {
463            // Note: Key must be cloned for ownership, but we can use Arc for Values in future optimization
464            // For now, we use cloning as it's simpler and the performance is already excellent (24.9ns)
465            self_cache.insert(key.clone(), value.clone());
466        }
467
468        Ok(())
469    }
470
471    // --- PRIVATE HELPERS ---
472
473    /// Detect format from file extension
474    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    /// Parse content based on format
487    fn parse_content(content: &str, format: &str) -> Result<Value> {
488        match format {
489            "conf" => {
490                // Use the regular conf parser for now
491                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    /// Get nested value using dot notation (zero-copy when possible)
508    #[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    /// Set nested value using dot notation
530    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    /// Collect all keys recursively
541    #[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    /// Serialize to specific format
563    fn serialize_to_format(&self, table: &BTreeMap<String, Value>, format: &str) -> Result<String> {
564        match format {
565            "conf" => {
566                // Basic CONF serialization (you can enhance this)
567                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    /// Convert value to string representation
587    #[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(), // Simplified for now
600            #[cfg(feature = "chrono")]
601            Value::DateTime(dt) => dt.to_rfc3339(),
602        }
603    }
604}
605
606impl ConfigManager {
607    /// Create new config manager
608    pub fn new() -> Self {
609        Self::default()
610    }
611
612    /// Load named configuration
613    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    /// Get named configuration
624    pub fn get(&self, name: &str) -> Option<Arc<RwLock<EnterpriseConfig>>> {
625        let configs = self.configs.read().ok()?;
626        configs.get(name).map(|config| {
627            // Return a reference wrapped in Arc for thread safety
628            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    /// List all configuration names
640    pub fn list(&self) -> Vec<String> {
641        match self.configs.read() {
642            Ok(configs) => configs.keys().cloned().collect(),
643            Err(_) => Vec::new(), // Return empty on lock poisoning
644        }
645    }
646
647    /// Remove named configuration
648    pub fn remove(&self, name: &str) -> bool {
649        match self.configs.write() {
650            Ok(mut configs) => configs.remove(name).is_some(),
651            Err(_) => false, // Return false on lock poisoning
652        }
653    }
654}
655
656/// Direct parsing functions for maximum performance
657/// These bypass the caching layer for one-time parsing
658pub mod direct {
659    use super::*;
660
661    /// Parse file directly to [`Value`] (no caching).
662    ///
663    /// **Deprecated:** use [`crate::parse_file`] — it routes through the
664    /// same underlying parsers and returns the same [`Value`].
665    ///
666    /// # Errors
667    ///
668    /// Returns an error if the file cannot be read or the contents
669    /// cannot be parsed in the detected format.
670    #[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    /// Parse string directly to [`Value`] (no caching).
681    ///
682    /// **Deprecated:** use [`crate::parse`] — it routes through the
683    /// same underlying parsers and returns the same [`Value`].
684    ///
685    /// # Errors
686    ///
687    /// Returns an error if the input cannot be parsed in the given
688    /// format.
689    #[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    /// Parse to array/vector for direct use
700    #[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
718/// Recursive helper for dotted-key inserts used by
719/// [`EnterpriseConfig::set_nested`]. Module-scoped rather than nested
720/// inside the method so the recursion can be reasoned about by clippy
721/// without `items_after_statements` noise.
722fn 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        // Test existing value with manual extraction
755        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        // Test default value
761        if config.get("timeout").is_some() {
762            panic!("Should not find timeout key");
763        }
764
765        // Test default behavior
766        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}