pub mod schema;
#[cfg(any(not(target_arch = "wasm32"), test))]
mod validation;
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;
#[cfg(any(not(target_arch = "wasm32"), test))]
use serde_path_to_error::{Path as SerdePath, Segment as SerdePathSegment};
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 {issue}: {detail}")]
CannotParseToml {
issue: ConfigTomlIssue,
detail: String,
},
#[error(transparent)]
ConfigSchema(#[from] ConfigSchemaError),
#[error("runtime IC root key error: {0}")]
RuntimeRootKey(String),
}
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
pub enum ConfigTomlIssue {
#[error("document is invalid")]
InvalidDocument,
#[error("value at {logical_path} is invalid")]
InvalidValue { logical_path: String },
#[error("contains unknown field {unknown_field} at {logical_path}")]
UnknownField {
logical_path: String,
unknown_field: String,
},
}
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 deserializer = toml::Deserializer::parse(config_str).map_err(|source| {
ConfigError::CannotParseToml {
issue: ConfigTomlIssue::InvalidDocument,
detail: source.to_string(),
}
})?;
let config: ConfigModel =
serde_path_to_error::deserialize(deserializer).map_err(|error| {
let issue = classify_toml_issue(config_str, error.path(), error.inner());
ConfigError::CannotParseToml {
issue,
detail: error.into_inner().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(|source| ConfigError::CannotParseToml {
issue: ConfigTomlIssue::InvalidDocument,
detail: source.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
})
}
}
#[cfg(any(not(target_arch = "wasm32"), test))]
fn classify_toml_issue(
source_toml: &str,
path: &SerdePath,
source: &toml::de::Error,
) -> ConfigTomlIssue {
let logical_path = path.to_string();
let unknown_field = path.iter().next_back().and_then(|segment| match segment {
SerdePathSegment::Map { key } => Some(key),
SerdePathSegment::Seq { .. }
| SerdePathSegment::Enum { .. }
| SerdePathSegment::Unknown => None,
});
if let (Some(unknown_field), Some(span)) = (unknown_field, source.span())
&& source_toml
.get(span)
.is_some_and(|token| token == unknown_field)
{
return ConfigTomlIssue::UnknownField {
logical_path,
unknown_field: unknown_field.clone(),
};
}
if logical_path == "." {
ConfigTomlIssue::InvalidDocument
} else {
ConfigTomlIssue::InvalidValue { logical_path }
}
}