lib-humus 0.5.0

Helps creating configurable frontends for humans and computers using axum, Tera and toml.
use lib_humus_configuration::read_from_toml_file;
use lib_humus_configuration::ErrorCause;
use lib_humus_configuration::HumusConfigError;
use log::{error, info};
use serde::Deserialize;
use tera::Tera;

use std::error::Error;
use std::fmt;

use crate::HumusProtoEngine;

/// Helps loading Templates from disk along with a handy configuration file.
///
/// Templates should be placed in a flat file structure in one directory.
/// They may be accompanied by an `extra.toml` file.
/// (by default, the location is configurable)
///
/// It also helps out a bit with deriving template realted configuration
/// for the rest of whatever you are building.
///
/// Example:
/// ```rust,ignore
/// use lib_humus::TemplateEngineLoader;
///
/// let template_loader = TemplateEngineLoader::new(
/// 	config.template.template_location.clone(),
/// 	config.template.extra_config.clone()
/// )
/// 	.cli_template_location(cli_args.template_location)
/// 	.cli_extra_config_location(cli_args.extra_config);
///
///
/// let templating_engine = match template_loader.load_templates() {
/// 	Ok(t) => t.into(),
/// 	Err(e) => {
/// 		println!("{e}");
/// 		::std::process::exit(1);
/// 	}
/// };
/// ```
///
#[derive(Debug, Clone, Deserialize)]
pub struct TemplateEngineLoader {
    /// The path to the directory where the templates are.
    pub template_location: String,
    /// The path to the extra configuration
    /// (relative to the current pwd, not to the templates)
    pub extra_config_location: Option<String>,
}

impl TemplateEngineLoader {
    /// Creates a new `TemplateEngineLoader` with minimal typing.
    pub fn new(template_location: String, extra_config_location: Option<String>) -> Self {
        Self {
            template_location: template_location,
            extra_config_location: extra_config_location,
        }
    }

    /// Overrides the template location with a new location if it is set.
    ///
    /// Intended for processing cli-options.
    pub fn cli_template_location(mut self, location: Option<String>) -> Self {
        if let Some(location) = location {
            self.template_location = location;
        }
        self
    }

    /// Overrides the extra configuration location with a new location if it is set.
    ///
    /// Intended for processing cli-options.
    pub fn cli_extra_config_location(mut self, location: Option<String>) -> Self {
        if let Some(location) = location {
            self.extra_config_location = Some(location);
        }
        self
    }

    /// Returns the template base directory.
    ///
    /// Contructed by ensuring the template_location ends in a "/".
    pub fn base_dir(&self) -> String {
        if self.template_location.ends_with("/") {
            self.template_location.clone()
        } else {
            self.template_location.clone() + "/"
        }
    }

    /// Initialize a [HumusProtoEngine] with the given templates and extra configuration.
    ///
    /// Failure Modes:
    /// * The `extra.toml` was not found and the path was explicitly set.
    /// * The `extra.toml` was found and is not valid toml.
    /// * The `extra.toml` passes, but tera finds an error in the templates.
    ///
    /// If `extra_config_location` is `None` no error is returned if the `extra.toml`
    /// was not found as the template might not require one.
    ///
    /// [HumusProtoEngine]: ./struct.HumusProtoEngine.html
    pub fn load_templates(&self) -> Result<HumusProtoEngine, TemplateEngineLoaderError> {
        let template_base_dir = self.base_dir();
        let template_extra_config_res = match &self.extra_config_location {
            Some(path) => read_from_toml_file(path),
            None => read_from_toml_file(&(template_base_dir.clone() + "extra.toml")),
        };
        let template_extra_config = match template_extra_config_res {
            Ok(c) => Some(c),
            Err(e) => match &e.cause {
                ErrorCause::FileRead { .. } => {
                    // Only fatal if the file was explicitly requested.
                    // An implicit request could also mean that
                    // the template doesn't need a config file.
                    if self.extra_config_location.is_some() {
                        return Err(TemplateEngineLoaderError::ConfigurationError(e));
                    }
                    None
                }
                _ => {
                    return Err(TemplateEngineLoaderError::ConfigurationError(e));
                }
            },
        };
        let template_glob = template_base_dir.clone() + "*";
        info!("Parsing Templates from '{}' ...", &template_glob);
        let res = Tera::new((template_glob).as_str());
        let tera = match res {
            Ok(t) => t,
            Err(e) => {
                error!("Error Parsing Template: {e}");
                return Err(TemplateEngineLoaderError::TemplateParseError {
                    path: template_glob,
                    tera_error: e,
                });
            }
        };
        Ok(HumusProtoEngine {
            tera: tera,
            template_config: template_extra_config,
        })
    }
}

/// Returned when loading a template using the [TemplateEngineLoader] fails.
///
/// [TemplateEngineLoader]: ./struct.TemplateEngineLoader.html
#[derive(Debug)]
pub enum TemplateEngineLoaderError {
    /// An error occourred while loading the template configuration file.
    ConfigurationError(HumusConfigError),
    /// An error occourred while parsing the templates.
    TemplateParseError {
        /// Path to the template directory
        path: String,
        /// what went wrong
        tera_error: tera::Error,
    },
}

impl fmt::Display for TemplateEngineLoaderError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::ConfigurationError(e) => {
                write!(f, "Error with template extra configuration:\n{e}")
            }
            Self::TemplateParseError { path, tera_error } => {
                write!(f, "Error parsing template '{path}':\n{tera_error}")
            }
        }
    }
}

impl Error for TemplateEngineLoaderError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::ConfigurationError(error) => Some(error),
            Self::TemplateParseError { tera_error, .. } => Some(tera_error),
        }
    }
}