soph-view 0.31.0

The RUST Framework for Web Rustceans.
Documentation
use crate::{config, View, ViewResult};
use soph_config::support::config;
use std::{collections::HashMap, ops::Deref};
use tera::Tera;

impl View {
    pub fn new() -> ViewResult<Self> {
        let config = config().parse::<config::View>()?;

        let mut tera = Tera::new(&config.path)?;
        tera.register_function("asset", asset(config.asset.url.to_owned()));
        tera.register_function("vite", vite(config.asset.url.to_owned()));

        Ok(Self { engine: tera })
    }
}

fn asset(url: String) -> impl tera::Function {
    Box::new(
        move |args: &HashMap<String, serde_json::Value>| -> tera::Result<serde_json::Value> {
            let resource = match args.get("resource") {
                Some(val) => match serde_json::from_value::<String>(val.clone()) {
                    Ok(v) => v,
                    Err(_) => {
                        let msg =
                            format!("Function `asset` received resource={val} but `resource` can only be a string",);

                        tracing::error!(err.msg = msg, "error");
                        return Err(tera::Error::msg(msg));
                    }
                },
                None => {
                    let msg = "Function `asset` didn't receive a `resource` argument";

                    tracing::error!(err.msg = msg, "error");
                    return Err(tera::Error::msg(msg));
                }
            };

            Ok(serde_json::Value::String(format!("{url}/{resource}")))
        },
    )
}

fn vite(url: String) -> impl tera::Function {
    Box::new(
        move |args: &HashMap<String, serde_json::Value>| -> tera::Result<serde_json::Value> {
            let entry = match args.get("entry") {
                Some(val) => match serde_json::from_value::<String>(val.clone()) {
                    Ok(v) => v,
                    Err(_) => {
                        let msg = format!("Function `vite` received entry={val} but `entry` can only be a string",);

                        tracing::error!(err.msg = msg, "error");
                        return Err(tera::Error::msg(msg));
                    }
                },
                None => {
                    let msg = "Function `vite` didn't receive a `entry` argument";

                    tracing::error!(err.msg = msg, "error");
                    return Err(tera::Error::msg(msg));
                }
            };

            // dev
            if std::path::Path::new("public/hot").exists() {
                let dev = format!(
                    r#"<script type="module" src="http://localhost:5173/@vite/client"></script>
                   <script type="module" src="http://localhost:5173/{}"></script>"#,
                    &entry
                );

                return Ok(serde_json::Value::String(dev));
            }

            // build
            let manifest = match std::fs::read_to_string("public/build/manifest.json").ok() {
                None => {
                    let msg = format!("Vite manifest not found at `{}`", "public/build/manifest.json");

                    tracing::error!(err.msg = msg, "error");
                    return Err(tera::Error::msg(msg));
                }
                Some(content) => match serde_json::from_str::<serde_json::Value>(&content)?.get(&entry) {
                    None => {
                        let msg = format!("Vite manifest entry not found at `{}`", &entry);

                        tracing::error!(err.msg = msg, "error");
                        return Err(tera::Error::msg(msg));
                    }
                    Some(val) => {
                        if let Some(is_entry) = val.get("isEntry") {
                            if !is_entry
                                .as_bool()
                                .ok_or_else(|| tera::Error::msg("Failed to parse `isEntry` as bool"))?
                            {
                                let msg = format!("Vite manifest entry `{}` is not an entry", &entry);

                                tracing::error!(err.msg = msg, "error");
                                return Err(tera::Error::msg(msg));
                            }
                        }

                        val.clone()
                    }
                },
            };

            let mut resources = String::new();
            if let Some(css) = manifest.get("css") {
                for css in css
                    .as_array()
                    .ok_or_else(|| tera::Error::msg("Failed to parse `css` as array"))?
                {
                    resources.push_str(&format!(
                        r#"<link rel="stylesheet" href="{}/build/{}">"#,
                        url,
                        css.as_str()
                            .ok_or_else(|| tera::Error::msg("Failed to parse `css` as string"))?
                    ));
                }
            }

            if let Some(js) = manifest.get("file") {
                resources.push_str(&format!(
                    r#"<script type="module" src="{}/build/{}"></script>"#,
                    url,
                    js.as_str()
                        .ok_or_else(|| tera::Error::msg("Failed to parse `file` as string"))?
                ));
            }

            Ok(serde_json::Value::String(resources))
        },
    )
}

impl Deref for View {
    type Target = Tera;

    fn deref(&self) -> &Self::Target {
        &self.engine
    }
}