zirv_config/
lib.rs

1//! zirv-config library
2//!
3//! Provides an expandable configuration system where configuration is built up
4//! from multiple subsystems (such as `server`, `logging`, etc.). The configuration
5//! can be accessed as a whole or by specific keys using the `read_config!` macro.
6
7pub mod config;
8
9#[macro_export]
10/// Retrieves the configuration from the global store with optional type conversion.
11/// 
12/// Usage:
13/// 
14/// - `read_config!()` returns the entire configuration as a `serde_json::Value`.
15/// - `read_config!("some.key")` returns an `Option<serde_json::Value>` for the specified dot-separated key.
16/// - `read_config!("some.key", Type)` attempts to convert the value to `Type`, returning an `Option<Type>`.
17/// 
18/// # Examples
19/// 
20/// ```rust
21/// # use zirv_config::read_config;
22/// // Get full config:
23/// let full_config = read_config!();
24/// println!("Config: {:?}", full_config);
25/// 
26/// // Get a specific key as a JSON value:
27/// if let Some(val) = read_config!("server.port") {
28///     println!("Server port (JSON): {}", val);
29/// }
30/// 
31/// // Get a specific key and convert it to a u16:
32/// if let Some(port) = read_config!("server.port", u16) {
33///     println!("Server port: {}", port);
34/// }
35/// ```
36macro_rules! read_config {
37    () => {
38        $crate::config::get_config()
39    };
40    ($key:expr) => {
41        $crate::config::get_config_by_key($key)
42    };
43    ($key:expr, $t:ty) => {{
44        let value_opt = $crate::config::get_config_by_key($key);
45        match value_opt {
46            Some(v) => match serde_json::from_value::<$t>(v) {
47                Ok(val) => Some(val),
48                Err(err) => {
49                    eprintln!(
50                        "Failed to parse config key {} into type {}: {:?}",
51                        $key,
52                        stringify!($t),
53                        err
54                    );
55                    None
56                }
57            },
58            None => None,
59        }
60    }};
61}
62
63#[macro_export]
64/// Registers a configuration block under a given namespace.
65///
66/// This macro is a thin wrapper around the underlying
67/// `config::register_config(namespace, config)` function.
68///
69/// # Examples
70///
71/// ```rust
72/// # use zirv_config::register_config;
73/// #[derive(serde::Serialize)]
74/// struct ServerConfig {
75///     port: u16,
76///     host: String,
77/// }
78///
79/// let server_config = ServerConfig { port: 3000, host: "0.0.0.0".to_string() };
80/// register_config!("server", server_config);
81/// ```
82macro_rules! register_config {
83    ($namespace:expr, $config:expr) => {{
84        $crate::config::register_config($namespace, $config);
85    }};
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use serde::Serialize;
92    use serde_json::{Value, json};
93
94    // A dummy configuration struct for testing.
95    #[derive(Serialize)]
96    struct DummyConfig {
97        port: u16,
98        host: String,
99    }
100
101    /// Before running tests, we initialize the global configuration.
102    /// Note: Since GLOBAL_CONFIG is a OnceLock, once it's set it cannot be cleared.
103    /// These tests assume a fresh process or that they run serially.
104    fn setup() {
105        // Force initialization.
106        config::init_config();
107    }
108
109    #[test]
110    fn test_register_and_read_full_config() {
111        setup();
112        // Register a dummy server configuration.
113        register_config!(
114            "server",
115            DummyConfig {
116                port: 3000,
117                host: "0.0.0.0".into()
118            }
119        );
120
121        // Retrieve the full configuration.
122        let full = read_config!();
123        // It should be a JSON object containing a key "server".
124        if let Value::Object(map) = full {
125            assert!(
126                map.contains_key("server"),
127                "Expected key 'server' not found"
128            );
129            if let Some(Value::Object(server_obj)) = map.get("server") {
130                // Verify the values.
131                assert_eq!(server_obj.get("port").unwrap(), &json!(3000));
132                assert_eq!(server_obj.get("host").unwrap(), &json!("0.0.0.0"));
133            } else {
134                panic!("'server' is not an object");
135            }
136        } else {
137            panic!("Global config is not an object");
138        }
139    }
140
141    #[test]
142    fn test_read_config_by_key() {
143        setup();
144
145        // Register a dummy server configuration.
146        register_config!(
147            "server",
148            DummyConfig {
149                port: 3000,
150                host: "0.0.0.0".into()
151            }
152        );
153
154        // Assume "server" was registered in a previous test.
155        // Retrieve a specific key:
156        let port = read_config!("server.port");
157        assert_eq!(port, Some(json!(3000)));
158
159        let host = read_config!("server.host");
160        assert_eq!(host, Some(json!("0.0.0.0")));
161
162        // Request a non-existent key:
163        let missing = read_config!("server.nonexistent");
164        assert!(missing.is_none());
165    }
166
167    #[test]
168    fn test_read_config_with_type_success() {
169        setup();
170
171        register_config!(
172            "server",
173            DummyConfig {
174                port: 3000,
175                host: "127.0.0.1".into()
176            }
177        );
178
179        // Try to retrieve "server.port" as a u16.
180        let port: Option<u16> = read_config!("server.port", u16);
181        assert_eq!(port, Some(3000));
182
183        // Retrieve "server.host" as a String.
184        let host: Option<String> = read_config!("server.host", String);
185        assert_eq!(host, Some("127.0.0.1".to_string()));
186    }
187}