use std::{fs, io, path::Path};
use math_core::MathCoreConfig;
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Config {
#[serde(flatten)]
pub math_core: MathCoreConfig,
}
#[derive(Debug)]
pub enum ConfigError {
Io(io::Error),
Parse(toml::de::Error),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::Io(err) => write!(f, "I/O error: {err}"),
ConfigError::Parse(err) => write!(f, "TOML parsing error: {err}"),
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(err) => Some(err),
ConfigError::Parse(err) => Some(err),
}
}
}
impl From<io::Error> for ConfigError {
fn from(err: io::Error) -> Self {
ConfigError::Io(err)
}
}
impl From<toml::de::Error> for ConfigError {
fn from(err: toml::de::Error) -> Self {
ConfigError::Parse(err)
}
}
pub fn load_config_file(path: &Path) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)?;
let config = parse_config(&content)?;
Ok(config)
}
#[inline]
fn parse_config(s: &str) -> Result<Config, ConfigError> {
let config: Config = toml::from_str(s)?;
Ok(config)
}
#[cfg(test)]
mod tests {
use math_core::{MaxExpansions, PrettyPrint};
use super::*;
#[test]
fn test_full_config() {
let toml_content = r#"
pretty-print = "always"
xml-namespace = true
indentation = 4
max-expansions = 500
[macros]
R = '\mathbb{R}'
"é" = '\acute{e}'
[css-classes]
unknown-command = "unknown-cmd"
"#;
let config = parse_config(toml_content).unwrap();
std::assert_matches!(config.math_core.pretty_print, PrettyPrint::Always);
assert!(config.math_core.xml_namespace);
let r_macro = config.math_core.macros.iter().find(|(k, _)| k == "R");
assert_eq!(r_macro.unwrap().1, "\\mathbb{R}");
let e_macro = config.math_core.macros.iter().find(|(k, _)| k == "é");
assert_eq!(e_macro.unwrap().1, "\\acute{e}");
assert_eq!(config.math_core.css_classes.unknown_command, "unknown-cmd");
std::assert_matches!(
config.math_core.indentation,
math_core::Indentation::Spaces(4)
);
assert_eq!(config.math_core.max_expansions, MaxExpansions(500));
}
#[test]
fn test_invalid_config() {
let invalid_toml = "invalid_toml";
let result = parse_config(invalid_toml);
std::assert_matches!(result, Err(ConfigError::Parse(_)));
}
#[test]
fn test_partial_config() {
let toml_content = r#"
[macros]
R = '\mathbb{R}'
"#;
let config = parse_config(toml_content).unwrap();
std::assert_matches!(config.math_core.pretty_print, PrettyPrint::Never);
assert_eq!(config.math_core.max_expansions, MaxExpansions::default());
let r_macro = config.math_core.macros.iter().find(|(k, _)| k == "R");
assert_eq!(r_macro.unwrap().1, "\\mathbb{R}");
}
}