canic_core/config/
mod.rs

1pub mod schema;
2
3use crate::Error;
4use schema::{ConfigSchemaError, Validate};
5use std::{cell::RefCell, sync::Arc};
6use thiserror::Error as ThisError;
7
8pub use schema::ConfigModel;
9
10//
11// CONFIG
12//
13// Even though a canister executes single‑threaded, there are a few practical reasons to favor Arc:
14// APIs & trait bounds: Lots of ecosystem code (caches, services, executors, middleware) takes
15// Arc<T> or requires Send + Sync. Rc<T> is neither Send nor Sync, so it won’t fit.
16//
17// Host-side tests & tools: Your crate likely builds for non‑wasm targets too (integration tests,
18// benches, local tooling). Those can be multi‑threaded; Arc “just works” across targets without
19// cfg gymnastics.
20//
21// Globals need Sync: If you ever move away from thread_local! or want to tuck the config behind
22// a global static, Rc can’t participate; Arc<T> is Sync (when T: Send + Sync).
23//
24
25thread_local! {
26    static CONFIG: RefCell<Option<Arc<ConfigModel>>> = const { RefCell::new(None) };
27}
28
29/// Errors related to configuration lifecycle and parsing.
30#[derive(Debug, ThisError)]
31pub enum ConfigError {
32    #[error("config has already been initialized")]
33    AlreadyInitialized,
34
35    /// TOML could not be parsed into the expected structure.
36    #[error("toml error: {0}")]
37    CannotParseToml(String),
38
39    /// Wrapper for data schema-level errors.
40    #[error(transparent)]
41    ConfigSchemaError(#[from] ConfigSchemaError),
42}
43
44///
45/// Config
46///
47
48pub struct Config {}
49
50impl Config {
51    // use an Arc to avoid repeatedly cloning
52    #[must_use]
53    pub fn get() -> Arc<ConfigModel> {
54        CONFIG.with(|cfg| {
55            cfg.borrow()
56                .as_ref()
57                .cloned()
58                .expect("⚠️ Config must be initialized before use")
59        })
60    }
61
62    /// Return the config if initialized, otherwise `None`.
63    #[must_use]
64    pub fn try_get() -> Option<Arc<ConfigModel>> {
65        CONFIG.with(|cfg| cfg.borrow().as_ref().cloned())
66    }
67
68    /// Initialize the global configuration from a TOML string.
69    /// return the config as it is read at build time
70    pub fn init_from_toml(config_str: &str) -> Result<Arc<ConfigModel>, Error> {
71        let config: ConfigModel =
72            toml::from_str(config_str).map_err(|e| ConfigError::CannotParseToml(e.to_string()))?;
73
74        // validate
75        config.validate().map_err(ConfigError::from)?;
76
77        CONFIG.with(|cfg| {
78            let mut borrow = cfg.borrow_mut();
79            if borrow.is_some() {
80                return Err(ConfigError::AlreadyInitialized.into());
81            }
82            let arc = Arc::new(config);
83            *borrow = Some(arc.clone());
84
85            Ok(arc)
86        })
87    }
88
89    /// Return the current config as a TOML string.
90    pub fn to_toml() -> Result<String, Error> {
91        let cfg = Self::get();
92
93        toml::to_string_pretty(&*cfg)
94            .map_err(|e| ConfigError::CannotParseToml(e.to_string()).into())
95    }
96
97    /// Test-only: reset the global config so tests can reinitialize with a fresh TOML.
98    #[cfg(test)]
99    pub fn reset_for_tests() {
100        CONFIG.with(|cfg| {
101            *cfg.borrow_mut() = None;
102        });
103    }
104}