ayun-view 0.22.0

The RUST Framework for Web Rustceans.
Documentation
pub mod config;
mod instance;

use ayun_core::{traits::ErrorTrait, Error, Result};
use std::{collections::HashMap, ops::Deref};
use tera::Tera;

pub struct View {
    inner: Tera,
    config: config::View,
}

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

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

        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(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));
        }
    };

    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));
    }

    manifest(entry)
}

fn manifest(entry: String) -> tera::Result<serde_json::Value> {
    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="{}">"#,
                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="{}"></script>"#,
            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
    }
}