config_ro/lib.rs
1//! A thread-safe configuration management library with JSON file support.
2//!
3//! This module provides a simple way to load and access configuration values from JSON files
4//! stored in a `configs/` directory. Configurations are cached globally for efficient access.
5//!
6//! # Examples
7//!
8//! ```
9//! use config_ro::Config;
10//!
11//! // Load the "database" configuration (reads from "configs/database.json")
12//! let config = Config::new("database");
13//!
14//! // Access nested values using dot notation
15//! let port: u16 = config.get("database.port").unwrap();
16//! let host: String = config.get("database.host").unwrap();
17//! ```
18use std::fs;
19
20use lazy_static::lazy_static;
21use serde_json::Value;
22use std::collections::HashMap;
23use std::sync::RwLock;
24use serde::de::DeserializeOwned;
25
26pub trait ConfigModule {
27 fn get(&self, name: &str) -> Option<&str>;
28}
29
30// Global config cache
31lazy_static! {
32 static ref CONFIGS: RwLock<HashMap<String, Value>> = RwLock::new(HashMap::new());
33}
34
35/// Configuration instance that provides access to cached configuration values
36///
37/// Each `Config` instance is associated with a specific configuration file
38/// and provides type-safe access to its values.
39pub struct Config {
40 name: String,
41}
42
43impl Config {
44 /// Creates or retrieves a cached configuration instance
45 ///
46 /// The configuration is loaded from `configs/{name}.json`. The file is parsed
47 /// only once and then cached for subsequent accesses.
48 ///
49 /// # Arguments
50 /// * `name` - Name of the configuration file (without extension)
51 ///
52 /// # Panics
53 /// - If the configuration file doesn't exist in `configs/` directory
54 /// - If the file contains invalid JSON
55 ///
56 /// # Examples
57 /// ```
58 /// use config_ro::Config;
59 /// let config = Config::new("app_settings");
60 /// ```
61 pub fn new(name: &str) -> Self {
62 let has = {
63 let configs = CONFIGS.read().unwrap();
64 configs.get(name).is_some()
65 };
66 if !has {
67 let mut configs = CONFIGS.write().unwrap();
68 configs.insert(name.to_string(), from_name(name));
69 }
70 Config {
71 name: name.to_string(),
72 }
73 }
74
75 /// Retrieves a configuration value by its path, supporting nested structures
76 ///
77 /// Uses dot notation to access nested values (e.g., "database.connection.port").
78 /// The value is automatically deserialized to the requested type.
79 ///
80 /// # Arguments
81 /// * `path` - Dot-separated path to the configuration value
82 ///
83 /// # Returns
84 /// `Some(T)` if the value exists and can be deserialized, `None` otherwise
85 ///
86 /// # Examples
87 /// ```
88 /// use config_ro::Config;
89 /// let config = Config::new("app");
90 ///
91 /// // Flat structure
92 /// let timeout: u32 = config.get("timeout").unwrap();
93 ///
94 /// // Nested structure
95 /// let db_port: u16 = config.get("database.connection.port").unwrap();
96 ///
97 /// // Optional values
98 /// let retry_count: Option<u8> = config.get("retries.count");
99 /// ```
100 pub fn get<T: DeserializeOwned>(&self, path: &str) -> Option<T> {
101 let configs = CONFIGS.read().unwrap();
102 let value = configs.get(&self.name)?;
103
104 let mut current = value;
105 for key in path.split('.') {
106 current = match current.get(key) {
107 Some(v) => v,
108 None => return None,
109 };
110 }
111
112 serde_json::from_value(current.clone()).ok()
113 }
114}
115
116fn from_name(name: &str) -> Value {
117 let filename = format!("configs/{}.json", name);
118 let content = fs::read_to_string(&filename)
119 .unwrap_or_else(|_| panic!("Failed to read config file: {}", filename));
120
121 serde_json::from_str(&content).unwrap_or_else(|_| panic!("Invalid JSON format in {}", filename))
122}
123