ayun-view 0.24.0

The RUST Framework for Web Rustceans.
Documentation
use crate::{config, error::Error, View, ViewResult};
use ayun_core::traits::ErrorTrait;
use std::{collections::HashMap, ops::Deref};
use tera::Tera;

impl View {
    pub fn try_from_config(config: config::View) -> ViewResult<Self> {
        let mut tera = Tera::new(&config.path).map_err(Error::wrap)?;

        tera.register_function("asset", asset(config.asset.url.to_owned()));
        tera.register_function("vite", vite(config.asset.url.to_owned()));

        Ok(Self {
            inner: tera,
            config,
        })
    }

    pub fn config(self) -> config::View {
        self.config
    }
}

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={} but `resource` can only be a \
                             string",
                            val
                        );

                        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={} but `entry` can only be a string",
                            val
                        );

                        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.inner
    }
}