1use crate::*;
4use std::collections::BTreeMap;
5use std::sync::{Arc, Mutex};
6
7fn tc<S: serde::Serialize, D: serde::de::DeserializeOwned>(
9 s: &S,
10) -> K2Result<D> {
11 serde_json::from_str(
12 &serde_json::to_string(s)
13 .map_err(|e| K2Error::other_src("encode", e))?,
14 )
15 .map_err(|e| K2Error::other_src("decode", e))
16}
17
18pub type ConfigUpdateCb =
20 Arc<dyn Fn(serde_json::Value) + 'static + Send + Sync>;
21
22#[derive(Clone, serde::Serialize, serde::Deserialize)]
23#[serde(transparent, rename_all = "camelCase")]
24struct ConfigEntry {
25 pub value: serde_json::Value,
26 #[serde(skip, default)]
27 pub update_cb: Option<ConfigUpdateCb>,
28}
29
30impl std::fmt::Debug for ConfigEntry {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("ConfigEntry")
33 .field("value", &self.value)
34 .field("has_update_cb", &self.update_cb.is_some())
35 .finish()
36 }
37}
38
39#[derive(Debug, serde::Serialize, serde::Deserialize)]
40#[serde(untagged, rename_all = "camelCase")]
41enum ConfigMap {
42 ConfigMap(BTreeMap<String, Box<Self>>),
43 ConfigEntry(ConfigEntry),
44}
45
46impl Default for ConfigMap {
47 fn default() -> Self {
48 Self::ConfigMap(BTreeMap::new())
49 }
50}
51
52struct Inner {
53 map: ConfigMap,
54 are_defaults_set: bool,
55 did_validate: bool,
56 is_runtime: bool,
57}
58
59pub struct Config(Mutex<Inner>);
61
62impl serde::Serialize for Config {
63 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64 where
65 S: serde::Serializer,
66 {
67 self.0.lock().unwrap().map.serialize(serializer)
68 }
69}
70
71impl std::fmt::Debug for Config {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 self.0.lock().unwrap().map.fmt(f)
74 }
75}
76
77impl Default for Config {
78 fn default() -> Self {
79 Self(Mutex::new(Inner {
80 map: ConfigMap::default(),
81 are_defaults_set: false,
82 did_validate: false,
83 is_runtime: false,
84 }))
85 }
86}
87
88impl Config {
89 pub fn mark_defaults_set(&self) {
93 self.0.lock().unwrap().are_defaults_set = true;
94 }
95
96 pub fn mark_validated(&self) -> bool {
99 let mut lock = self.0.lock().unwrap();
100 let out = lock.did_validate;
101 lock.did_validate = true;
102 out
103 }
104
105 pub fn mark_runtime(&self) {
109 self.0.lock().unwrap().is_runtime = true;
110 }
111
112 pub fn get_module_config<D: serde::de::DeserializeOwned>(
114 &self,
115 ) -> K2Result<D> {
116 let lock = self.0.lock().unwrap();
117 tc(&lock.map)
118 }
119
120 pub fn set_module_config<S: serde::Serialize>(
125 &self,
126 config: &S,
127 ) -> K2Result<()> {
128 let in_map: ConfigMap = tc(config)?;
129 let debug_path = format!("{in_map:?}");
130 let mut updates = Vec::new();
131 {
132 let mut lock = self.0.lock().unwrap();
133 let are_defaults_set = lock.are_defaults_set;
134 let is_runtime = lock.is_runtime;
135 let old_map: &mut ConfigMap = &mut lock.map;
136 let new_map: &ConfigMap = &in_map;
137 fn apply_map(
138 debug_path: &str,
139 are_defaults_set: bool,
140 is_runtime: bool,
141 updates: &mut Vec<(ConfigUpdateCb, serde_json::Value)>,
142 old_map: &mut ConfigMap,
143 new_map: &ConfigMap,
144 ) -> K2Result<()> {
145 match new_map {
146 ConfigMap::ConfigMap(new_map) => match old_map {
147 ConfigMap::ConfigMap(old_map) => {
148 for (key, new_map) in new_map.iter() {
149 if are_defaults_set
150 && !old_map.contains_key(key)
151 {
152 tracing::warn!(
153 debug_path,
154 "this config parameter may be unused"
155 );
156 }
157 let old_map =
158 old_map.entry(key.clone()).or_default();
159 apply_map(
160 debug_path,
161 are_defaults_set,
162 is_runtime,
163 updates,
164 old_map,
165 new_map,
166 )?;
167 }
168 }
169 ConfigMap::ConfigEntry(_) => {
170 return Err(K2Error::other(format!(
171 "{debug_path} attempted to insert a map where an entry exists",
172 )));
173 }
174 },
175 ConfigMap::ConfigEntry(new_entry) => match old_map {
176 ConfigMap::ConfigMap(m) => {
177 if !m.is_empty() {
178 return Err(K2Error::other(format!(
179 "{debug_path} attempted to insert an entry where a map exists",
180 )));
181 }
182 *old_map =
183 ConfigMap::ConfigEntry(new_entry.clone());
184 if is_runtime {
185 tracing::warn!(
186 debug_path,
187 "no update callback for runtime config alteration"
188 );
189 }
190 }
191 ConfigMap::ConfigEntry(old_entry) => {
192 old_entry.value = new_entry.value.clone();
193 if let Some(update_cb) = &old_entry.update_cb {
194 updates.push((
195 update_cb.clone(),
196 new_entry.value.clone(),
197 ));
198 } else if is_runtime {
199 tracing::warn!(
200 debug_path,
201 "no update callback for runtime config alteration"
202 );
203 }
204 }
205 },
206 }
207 Ok(())
208 }
209 apply_map(
210 &debug_path,
211 are_defaults_set,
212 is_runtime,
213 &mut updates,
214 old_map,
215 new_map,
216 )?;
217 }
218 for (update_cb, value) in updates {
219 update_cb(value);
220 }
221 Ok(())
222 }
223
224 pub fn register_entry_update_cb<D: std::fmt::Display>(
230 &self,
231 path: &[D],
232 update_cb: ConfigUpdateCb,
233 ) -> K2Result<()> {
234 let value = {
235 let mut lock = self.0.lock().unwrap();
236 let mut cur: &mut ConfigMap = &mut lock.map;
237 for path in path {
238 let key = path.to_string();
239 match cur {
240 ConfigMap::ConfigMap(m) => cur = m.entry(key).or_default(),
241 ConfigMap::ConfigEntry(_) => {
242 return Err(K2Error::other(
243 "attempted to insert a map where an entry exists",
244 ));
245 }
246 }
247 }
248 match cur {
249 ConfigMap::ConfigMap(m) => {
250 if !m.is_empty() {
251 return Err(K2Error::other(
252 "attempted to insert an entry where a map exists",
253 ));
254 }
255 *cur = ConfigMap::ConfigEntry(ConfigEntry {
256 value: serde_json::Value::Null,
257 update_cb: Some(update_cb.clone()),
258 });
259 serde_json::Value::Null
260 }
261 ConfigMap::ConfigEntry(e) => {
262 e.update_cb = Some(update_cb.clone());
263 e.value.clone()
264 }
265 }
266 };
267 update_cb(value);
268 Ok(())
269 }
270}
271
272#[cfg(test)]
273mod test {
274 use super::*;
275
276 #[test]
277 fn warns_unused() {
278 kitsune2_test_utils::enable_tracing();
282
283 let c = Config::default();
284 c.set_module_config(&serde_json::json!({"apples": "red"}))
285 .unwrap();
286 c.mark_defaults_set();
287 c.set_module_config(&serde_json::json!({"apples": "green"}))
288 .unwrap();
289 c.set_module_config(&serde_json::json!({"bananas": 42}))
290 .unwrap();
291 }
292
293 #[test]
294 fn warns_no_runtime_cb() {
295 kitsune2_test_utils::enable_tracing();
299
300 let c = Config::default();
301 c.set_module_config(&serde_json::json!({"apples": "red"}))
302 .unwrap();
303 c.mark_runtime();
304 c.set_module_config(&serde_json::json!({"apples": "green"}))
305 .unwrap();
306 c.set_module_config(&serde_json::json!({"bananas": 42}))
307 .unwrap();
308 }
309
310 #[test]
311 fn config_usage_example() {
312 #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
313 #[serde(rename_all = "camelCase")]
314 struct SubConfig {
315 pub apples: String,
316 pub bananas: u32,
317 }
318
319 #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
320 #[serde(rename_all = "camelCase")]
321 struct ModConfig {
322 pub my_module: SubConfig,
323 }
324
325 let c = Config::default();
326
327 let expect = ModConfig {
328 my_module: SubConfig {
329 apples: "red".to_string(),
330 bananas: 42,
331 },
332 };
333
334 c.set_module_config(&expect).unwrap();
335
336 println!("{}", serde_json::to_string_pretty(&c).unwrap());
337
338 let resp: ModConfig = c.get_module_config().unwrap();
339 assert_eq!(expect, resp);
340
341 use std::sync::atomic::*;
342 let update = Arc::new(AtomicU32::new(0));
343 let update2 = update.clone();
344 c.register_entry_update_cb(
345 &["myModule", "bananas"],
346 Arc::new(move |v| {
347 let v: u32 =
348 serde_json::from_str(&serde_json::to_string(&v).unwrap())
349 .unwrap();
350 update2.store(v, Ordering::SeqCst);
351 }),
352 )
353 .unwrap();
354
355 c.set_module_config(&serde_json::json!({
356 "myModule": {
357 "bananas": 99,
358 }
359 }))
360 .unwrap();
361
362 assert_eq!(99, update.load(Ordering::SeqCst));
363 }
364}