catapulte-engine 0.1.0

The core engine of catapulte with template loading, interpolation and rendering.
Documentation
use super::prelude::Template;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use std::fs::{read_to_string, File};
use std::io::BufReader;
use std::path::PathBuf;

#[derive(Clone, Debug, serde::Deserialize)]
pub struct Config {
    pub path: PathBuf,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            path: PathBuf::new().join("template"),
        }
    }
}

impl From<Config> for LocalLoader {
    fn from(value: Config) -> Self {
        Self::new(value.path)
    }
}

fn default_template_path() -> String {
    "template.mjml".into()
}

#[derive(Debug, Deserialize)]
pub struct LocalMetadata {
    name: String,
    description: String,
    #[serde(default = "default_template_path")]
    template: String,
    attributes: JsonValue,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Unable to open metadata file: {0:?}")]
    MetadataOpenFailed(std::io::Error),
    #[error("Unable to deserialize metadata file: {0:?}")]
    MetadataFormatInvalid(serde_json::Error),
    #[error("Unable to open template file: {0:?}")]
    TemplateOpenFailed(std::io::Error),
}

#[derive(Debug)]
pub struct LocalLoader {
    root: PathBuf,
}

impl LocalLoader {
    pub fn new(root: PathBuf) -> Self {
        Self { root }
    }
}

impl LocalLoader {
    pub(super) async fn find_by_name(&self, name: &str) -> Result<Template, Error> {
        tracing::debug!("loading template {}", name);
        let path = self.root.join(name).join("metadata.json");
        let metadata_file = File::open(path).map_err(|err| {
            metrics::counter!("template_provider_error", "reason" => "metadata_not_found")
                .increment(1);
            tracing::debug!("template provider error: metadata not found ({:?})", err);
            Error::MetadataOpenFailed(err)
        })?;
        let metadata_reader = BufReader::new(metadata_file);
        let metadata: LocalMetadata = serde_json::from_reader(metadata_reader).map_err(|err| {
            metrics::counter!("template_provider_error", "reason" => "metadata_invalid")
                .increment(1);
            tracing::debug!("template provider error: metadata invalid ({:?})", err);
            Error::MetadataFormatInvalid(err)
        })?;
        let template_path = self.root.join(name).join(metadata.template);
        let content = read_to_string(template_path).map_err(|err| {
            metrics::counter!("template_provider_error", "reason" => "template_not_found")
                .increment(1);
            tracing::debug!("template provider error: template not found ({:?})", err);
            Error::TemplateOpenFailed(err)
        })?;
        Ok(Template {
            name: metadata.name,
            description: metadata.description,
            content,
            attributes: metadata.attributes,
        })
    }
}