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}