coil-template 0.1.1

Server-side template parsing and rendering for the Coil framework.
Documentation
use super::*;
#[cfg(test)]
use crate::runtime::escape_html_text;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrustedHtml(String);

impl TrustedHtml {
    pub fn new(value: impl Into<String>) -> Result<Self, TemplateModelError> {
        Ok(Self(require_non_empty("trusted_html", value.into())?))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderValue {
    Text(String),
    TrustedHtml(TrustedHtml),
    Bool(bool),
    List(Vec<RenderModel>),
    Object(RenderModel),
}

impl RenderValue {
    pub fn text(value: impl Into<String>) -> Self {
        Self::Text(value.into())
    }

    pub fn trusted_html(value: TrustedHtml) -> Self {
        Self::TrustedHtml(value)
    }

    pub fn bool(value: bool) -> Self {
        Self::Bool(value)
    }

    pub fn list(value: Vec<RenderModel>) -> Self {
        Self::List(value)
    }

    pub fn object(value: RenderModel) -> Self {
        Self::Object(value)
    }

    pub(crate) fn as_text(&self, key: &str) -> Result<&str, TemplateModelError> {
        match self {
            Self::Text(value) => Ok(value),
            Self::TrustedHtml(_) => Err(TemplateModelError::ValueTypeMismatch {
                key: key.to_string(),
                expected: "text",
            }),
            Self::Bool(_) => Err(TemplateModelError::ValueTypeMismatch {
                key: key.to_string(),
                expected: "text",
            }),
            Self::List(_) => Err(TemplateModelError::ValueTypeMismatch {
                key: key.to_string(),
                expected: "text",
            }),
            Self::Object(_) => Err(TemplateModelError::ValueTypeMismatch {
                key: key.to_string(),
                expected: "text",
            }),
        }
    }

    pub(crate) fn as_bool(&self, key: &str) -> Result<bool, TemplateModelError> {
        match self {
            Self::Bool(value) => Ok(*value),
            Self::Text(_) | Self::TrustedHtml(_) | Self::List(_) | Self::Object(_) => {
                Err(TemplateModelError::ValueTypeMismatch {
                    key: key.to_string(),
                    expected: "bool",
                })
            }
        }
    }

    pub(crate) fn as_list(&self, key: &str) -> Result<&[RenderModel], TemplateModelError> {
        match self {
            Self::List(value) => Ok(value.as_slice()),
            Self::Text(_) | Self::TrustedHtml(_) | Self::Bool(_) | Self::Object(_) => {
                Err(TemplateModelError::ValueTypeMismatch {
                    key: key.to_string(),
                    expected: "list",
                })
            }
        }
    }

    pub(crate) fn as_object(&self, key: &str) -> Result<&RenderModel, TemplateModelError> {
        match self {
            Self::Object(value) => Ok(value),
            Self::Text(_) | Self::TrustedHtml(_) | Self::Bool(_) | Self::List(_) => {
                Err(TemplateModelError::ValueTypeMismatch {
                    key: key.to_string(),
                    expected: "object",
                })
            }
        }
    }

    #[cfg(test)]
    pub(crate) fn render_html(&self) -> String {
        match self {
            Self::Text(value) => escape_html_text(value),
            Self::TrustedHtml(value) => value.as_str().to_string(),
            Self::Bool(value) => value.to_string(),
            Self::List(_) => String::new(),
            Self::Object(_) => String::new(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RenderModel {
    values: BTreeMap<String, RenderValue>,
    asset_paths: BTreeMap<String, String>,
    translations: BTreeMap<String, String>,
}

impl RenderModel {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_value(
        mut self,
        key: impl Into<String>,
        value: RenderValue,
    ) -> Result<Self, TemplateModelError> {
        let key = validate_token("render_key", key.into())?;
        self.values.insert(key, value);
        Ok(self)
    }

    pub fn with_bool(
        self,
        key: impl Into<String>,
        value: bool,
    ) -> Result<Self, TemplateModelError> {
        self.with_value(key, RenderValue::bool(value))
    }

    pub fn with_list(
        self,
        key: impl Into<String>,
        value: Vec<RenderModel>,
    ) -> Result<Self, TemplateModelError> {
        self.with_value(key, RenderValue::list(value))
    }

    pub fn with_object(
        self,
        key: impl Into<String>,
        value: RenderModel,
    ) -> Result<Self, TemplateModelError> {
        self.with_value(key, RenderValue::object(value))
    }

    pub fn with_asset_path(
        mut self,
        logical_path: impl Into<String>,
        public_url: impl Into<String>,
    ) -> Result<Self, TemplateModelError> {
        let logical_path = validate_token("asset_logical_path", logical_path.into())?;
        let public_url = require_non_empty("asset_public_url", public_url.into())?;
        self.asset_paths.insert(logical_path, public_url);
        Ok(self)
    }

    pub fn with_translation(
        mut self,
        key: impl Into<String>,
        value: impl Into<String>,
    ) -> Result<Self, TemplateModelError> {
        let key = validate_token("translation_key", key.into())?;
        let value = require_non_empty("translation_value", value.into())?;
        self.translations.insert(key, value);
        Ok(self)
    }

    pub(crate) fn get(&self, key: &str) -> Option<&RenderValue> {
        if let Some(value) = self.values.get(key) {
            return Some(value);
        }

        let (head, tail) = key.split_once('.')?;
        let value = self.values.get(head)?;
        value.as_object(head).ok()?.get(tail)
    }

    pub(crate) fn get_path(&self, path: &str) -> Option<&RenderValue> {
        self.get(path)
    }

    pub(crate) fn get_asset_path(&self, logical_path: &str) -> Option<&str> {
        self.asset_paths.get(logical_path).map(String::as_str)
    }

    pub(crate) fn get_translation(&self, key: &str) -> Option<&str> {
        self.translations.get(key).map(String::as_str)
    }

    pub(crate) fn merged_with(&self, overlay: &RenderModel) -> RenderModel {
        let mut values = self.values.clone();
        values.extend(overlay.values.clone());
        let mut asset_paths = self.asset_paths.clone();
        asset_paths.extend(overlay.asset_paths.clone());
        let mut translations = self.translations.clone();
        translations.extend(overlay.translations.clone());
        RenderModel {
            values,
            asset_paths,
            translations,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderOutput {
    pub html: String,
}