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}