canic_core/config/
mod.rs

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