pub mod schema;
use crate::{InternalError, InternalErrorOrigin};
use schema::ConfigSchemaError;
use std::{cell::RefCell, sync::Arc};
use thiserror::Error as ThisError;
pub use schema::ConfigModel;
#[cfg(any(not(target_arch = "wasm32"), test))]
use schema::Validate;
struct InstalledConfig {
model: Arc<ConfigModel>,
source_toml: Arc<str>,
}
thread_local! {
static CONFIG: RefCell<Option<InstalledConfig>> = const { RefCell::new(None) };
}
#[derive(Debug, ThisError)]
pub enum ConfigError {
#[error("config has already been initialized")]
AlreadyInitialized,
#[error("config has not been initialized")]
NotInitialized,
#[error("toml error: {0}")]
CannotParseToml(String),
#[error(transparent)]
ConfigSchema(#[from] ConfigSchemaError),
}
impl From<ConfigError> for InternalError {
fn from(err: ConfigError) -> Self {
Self::domain(InternalErrorOrigin::Config, err.to_string())
}
}
pub struct Config {}
impl Config {
pub(crate) fn get() -> Result<Arc<ConfigModel>, InternalError> {
CONFIG.with(|cfg| {
if let Some(config) = cfg.borrow().as_ref() {
return Ok(config.model.clone());
}
#[cfg(test)]
{
Ok(Self::init_for_tests())
}
#[cfg(not(test))]
{
Err(ConfigError::NotInitialized.into())
}
})
}
#[must_use]
pub(crate) fn try_get() -> Option<Arc<ConfigModel>> {
CONFIG.with(|cfg| {
if let Some(config) = cfg.borrow().as_ref() {
return Some(config.model.clone());
}
#[cfg(test)]
{
Some(Self::init_for_tests())
}
#[cfg(not(test))]
{
None
}
})
}
#[cfg(any(not(target_arch = "wasm32"), test))]
pub fn parse_toml(config_str: &str) -> Result<ConfigModel, ConfigError> {
let config: ConfigModel =
toml::from_str(config_str).map_err(|e| ConfigError::CannotParseToml(e.to_string()))?;
config.validate().map_err(ConfigError::from)?;
Ok(config)
}
pub(crate) fn init_from_model(
config: ConfigModel,
source_toml: &str,
) -> Result<Arc<ConfigModel>, ConfigError> {
CONFIG.with(|cfg| {
let mut borrow = cfg.borrow_mut();
if borrow.is_some() {
return Err(ConfigError::AlreadyInitialized);
}
let model = Arc::new(config);
*borrow = Some(InstalledConfig {
model: model.clone(),
source_toml: Arc::<str>::from(source_toml),
});
Ok(model)
})
}
#[cfg(test)]
pub fn init_from_model_for_tests(config: ConfigModel) -> Result<Arc<ConfigModel>, ConfigError> {
config.validate().map_err(ConfigError::from)?;
let source_toml = toml::to_string_pretty(&config)
.map_err(|e| ConfigError::CannotParseToml(e.to_string()))?;
Self::init_from_model(config, &source_toml)
}
pub(crate) fn to_toml() -> Result<String, InternalError> {
CONFIG.with(|cfg| {
cfg.borrow()
.as_ref()
.map(|config| config.source_toml.to_string())
.ok_or_else(|| ConfigError::NotInitialized.into())
})
}
#[cfg(test)]
pub fn reset_for_tests() {
CONFIG.with(|cfg| {
*cfg.borrow_mut() = None;
});
}
#[cfg(test)]
#[must_use]
pub fn init_for_tests() -> Arc<ConfigModel> {
CONFIG.with(|cfg| {
let mut borrow = cfg.borrow_mut();
if let Some(existing) = borrow.as_ref() {
return existing.model.clone();
}
let config = ConfigModel::test_default();
config.validate().expect("test config must validate");
let model = Arc::new(config);
*borrow = Some(InstalledConfig {
model: model.clone(),
source_toml: Arc::<str>::from(""),
});
model
})
}
}