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    #[error("config has not been initialized")]
35    NotInitialized,
36
37    /// TOML could not be parsed into the expected structure.
38    #[error("toml error: {0}")]
39    CannotParseToml(String),
40
41    /// Wrapper for data schema-level errors.
42    #[error(transparent)]
43    ConfigSchemaError(#[from] ConfigSchemaError),
44}
45
46///
47/// Config
48///
49
50pub struct Config {}
51
52impl Config {
53    pub fn get() -> Result<Arc<ConfigModel>, Error> {
54        CONFIG.with(|cfg| {
55            if let Some(config) = cfg.borrow().as_ref() {
56                return Ok(config.clone());
57            }
58
59            #[cfg(test)]
60            {
61                Ok(Self::init_for_tests())
62            }
63
64            #[cfg(not(test))]
65            {
66                Err(ConfigError::NotInitialized.into())
67            }
68        })
69    }
70
71    /// Initialize the global configuration from a TOML string.
72    /// return the config as it is read at build time
73    pub fn init_from_toml(config_str: &str) -> Result<Arc<ConfigModel>, Error> {
74        let config: ConfigModel =
75            toml::from_str(config_str).map_err(|e| ConfigError::CannotParseToml(e.to_string()))?;
76
77        // validate
78        config.validate().map_err(ConfigError::from)?;
79
80        CONFIG.with(|cfg| {
81            let mut borrow = cfg.borrow_mut();
82            if borrow.is_some() {
83                return Err(ConfigError::AlreadyInitialized.into());
84            }
85            let arc = Arc::new(config);
86            *borrow = Some(arc.clone());
87
88            Ok(arc)
89        })
90    }
91
92    /// Return the current config as a TOML string.
93    pub fn to_toml() -> Result<String, Error> {
94        let cfg = Self::get()?;
95
96        toml::to_string_pretty(&*cfg)
97            .map_err(|e| ConfigError::CannotParseToml(e.to_string()).into())
98    }
99
100    /// Test-only: reset the global config so tests can reinitialize with a fresh TOML.
101    #[cfg(test)]
102    pub fn reset_for_tests() {
103        CONFIG.with(|cfg| {
104            *cfg.borrow_mut() = None;
105        });
106    }
107
108    /// Test-only: ensure a minimal validated config is available.
109    #[cfg(test)]
110    #[must_use]
111    pub fn init_for_tests() -> Arc<ConfigModel> {
112        CONFIG.with(|cfg| {
113            let mut borrow = cfg.borrow_mut();
114            if let Some(existing) = borrow.as_ref() {
115                return existing.clone();
116            }
117
118            let config = ConfigModel::test_default();
119            config.validate().expect("test config must validate");
120
121            let arc = Arc::new(config);
122            *borrow = Some(arc.clone());
123            arc
124        })
125    }
126}