kitsune2_api/
config.rs

1//! Types for use when configuring kitsune2 modules.
2
3use crate::*;
4use std::collections::BTreeMap;
5use std::sync::{Arc, Mutex};
6
7/// helper transcode function
8fn 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
18/// A callback to be invoked if the config value is updated at runtime.
19pub 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
59/// Kitsune configuration.
60pub 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    /// Once defaults are set, generate warnings for any values
90    /// set beyond this list. So that we can identify no-longer-used
91    /// config parameters.
92    pub fn mark_defaults_set(&self) {
93        self.0.lock().unwrap().are_defaults_set = true;
94    }
95
96    /// Validate this config before using it in runtime.
97    /// Returns the previous validation state.
98    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    /// Once we are done setting initial config, generate warnings for
106    /// any runtime alterations that do not have update callbacks registered.
107    /// This way we can tell if runtime config changes are being ignored.
108    pub fn mark_runtime(&self) {
109        self.0.lock().unwrap().is_runtime = true;
110    }
111
112    /// Get a set of module config values from this config instance.
113    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    /// Set any number of module config values on this config instance.
121    ///
122    /// This will error if trying to write an entry where a map currently
123    /// resides or visa-versa.
124    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    /// Call this in your module constructor once for every parameter for
225    /// which you would like to receive runtime updates. This will immediately
226    /// invoke the callback with the current value to ensure this is atomic.
227    /// (If this is called before default initialization, that initial value
228    /// will be json Null.)
229    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        // this test will never fail,
279        // but we can check it traces correctly manually
280
281        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        // this test will never fail,
296        // but we can check it traces correctly manually
297
298        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}