use std::fmt;
use anyhow::Error;
use de_core::fs;
use thiserror::Error as ErrorDerive;
#[derive(Debug, ErrorDerive)]
pub enum ConfigLoadError {
DirectoryError(#[from] fs::DirError),
CheckErrors(Vec<(String, String)>),
Other(#[from] Error),
}
impl fmt::Display for ConfigLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Other(err) => write!(f, "Configuration error: {err}")?,
Self::DirectoryError(err) => write!(f, "Configuration directory error: {err}")?,
Self::CheckErrors(errors) => {
write!(f, "Configuration validation error(s):")?;
for err in errors {
write!(f, "\n - {}", err.1)?;
}
}
}
Ok(())
}
}
#[macro_export]
macro_rules! bundle_config {
($($name:ident : $type_into:ty : $type_from:ty),*) => {
use $crate::io::load_conf_text;
use $crate::macros::ConfigLoadError;
use tracing::{trace, debug};
use paste::paste;
use bevy::prelude::Resource;
use serde::{Deserialize as MacroDeserialize, Serialize as MacroSerialize};
#[derive(Resource, Debug, Clone)]
pub struct Configuration {
$(
$name: $type_into,
)*
}
#[derive(Debug, Clone, MacroSerialize)]
pub struct RawConfiguration {
$(
$name: $type_from,
)*
}
impl TryInto<Configuration> for RawConfiguration {
type Error = Error;
fn try_into(self) -> Result<Configuration, Self::Error> {
Ok(Configuration {
$(
$name: self.$name.try_into()?,
)*
})
}
}
#[derive(MacroDeserialize, MacroSerialize, Debug, Clone, Default)]
struct PartialConfiguration {
$(
$name: Option<paste! {[<Partial $type_from>]}>,
)*
}
impl Configuration {
$(
pub fn $name(&self) -> &$type_into {
&self.$name
}
)*
pub async fn load(path: &Path) -> Result<Self, ConfigLoadError> {
let from = RawConfiguration::load(path).await?;
let serialized = serde_yaml::to_string(&from)
.expect("Failed to serialize raw configuration");
debug!("Loaded raw configuration: \n{serialized}");
Ok(from.try_into().unwrap())
}
}
impl RawConfiguration {
pub async fn load(path: &Path) -> Result<Self, ConfigLoadError> {
match load_conf_text(path).await? {
Some(text) => {
let partial: PartialConfiguration =
serde_yaml::from_str(text.as_str()).context("Failed to parse DE configuration")?;
let config: Self = partial.try_into().context("Failed to convert partial configuration")?;
let mut errors = vec![];
$(
let value = &config.$name;
let check = value.check();
if let Err(err) = check {
for err in err {
trace!("Failed check: {:?}", err);
errors.push(err);
}
}
)*
if !errors.is_empty() {
trace!("Failed checks:");
for err in &errors {
trace!("{}", err.0);
}
return Err(ConfigLoadError::CheckErrors(errors));
}
Ok(config)
}
None => Ok(Self::default()),
}
}
}
impl TryFrom<PartialConfiguration> for RawConfiguration {
type Error = ConfigLoadError;
fn try_from(value: PartialConfiguration) -> Result<Self, ConfigLoadError> {
Ok(Self {
$(
$name: match value.$name{
Some(v) => v.try_into().context(concat!("`", stringify!($name), "` validation failed"))?,
None => Default::default(),
},
)*
})
}
}
impl Default for RawConfiguration {
fn default() -> Self {
PartialConfiguration::default().try_into().unwrap()
}
}
impl Default for Configuration {
fn default() -> Self {
RawConfiguration::default().try_into().unwrap()
}
}
};
}