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    // use an Arc to avoid repeatedly cloning
51    #[must_use]
52    pub fn get() -> Arc<ConfigModel> {
53        CONFIG.with(|cfg| {
54            cfg.borrow()
55                .as_ref()
56                .cloned()
57                .expect("⚠️ Config must be initialized before use")
58        })
59    }
60
61    /// Return the config if initialized, otherwise `None`.
62    #[must_use]
63    pub fn try_get() -> Option<Arc<ConfigModel>> {
64        CONFIG.with(|cfg| cfg.borrow().as_ref().cloned())
65    }
66
67    /// Initialize the global configuration from a TOML string.
68    /// return the config as it is read at build time
69    pub fn init_from_toml(config_str: &str) -> Result<Arc<ConfigModel>, Error> {
70        let config: ConfigModel =
71            toml::from_str(config_str).map_err(|e| ConfigError::CannotParseToml(e.to_string()))?;
72
73        // validate
74        config.validate().map_err(ConfigError::from)?;
75
76        CONFIG.with(|cfg| {
77            let mut borrow = cfg.borrow_mut();
78            if borrow.is_some() {
79                return Err(ConfigError::AlreadyInitialized.into());
80            }
81            let arc = Arc::new(config);
82            *borrow = Some(arc.clone());
83
84            Ok(arc)
85        })
86    }
87
88    /// Return the current config as a TOML string.
89    pub fn to_toml() -> Result<String, Error> {
90        let cfg = Self::get();
91
92        toml::to_string_pretty(&*cfg)
93            .map_err(|e| ConfigError::CannotParseToml(e.to_string()).into())
94    }
95
96    /// Test-only: reset the global config so tests can reinitialize with a fresh TOML.
97    #[cfg(test)]
98    pub fn reset_for_tests() {
99        CONFIG.with(|cfg| {
100            *cfg.borrow_mut() = None;
101        });
102    }
103}