rustploy-gui 0.1.7

Rustploy — desktop client (glacier-ui) for the Rustploy PaaS daemon
//! Assets embutidos no binário — modo standalone (só em builds de release).

use std::borrow::Cow;
use std::io;
use std::time::SystemTime;

use glacier_ui::AssetSource;
use include_dir::{Dir, File, include_dir};

/// `views/`: templates `.gvb`, estilos `styles/*.gss`, `styles/theme.json` e os
/// scripts Luau em `scripts/**/*.luau` (resolvidos por `require`/`<script src>`).
static VIEWS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/views");
/// `assets/icons/`: ícones SVG referenciados por `<svg src="assets/icons/…">`.
static ICONS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/assets/icons");

/// `assets/fonts/`: as fontes que o `app(...)` declara com `font(src = …)`.
static FONTS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/assets/fonts");
/// Logos dos blueprints, referenciados via o `{logo}` data-driven do catálogo
/// do daemon (`assets/blueprint-logos/<id>/<arquivo>`).
static BLUEPRINTS: Dir<'static> = include_dir!("$OUT_DIR/blueprint_logos");

/// Roteia um caminho lógico para a árvore embutida + o caminho relativo a ela
/// (a chave que `include_dir` usa, relativa à raiz do `#[folder]`).
fn route(path: &str) -> Option<&'static File<'static>> {
    let norm = path.replace('\\', "/");
    let norm = norm.strip_prefix("./").unwrap_or(&norm);

    const ROUTES: &[(&str, &Dir<'static>)] = &[
        ("views/", &VIEWS),
        ("assets/icons/", &ICONS),
        ("assets/fonts/", &FONTS),
        ("assets/blueprint-logos/", &BLUEPRINTS),
    ];
    for (prefix, dir) in ROUTES {
        if let Some(rest) = norm.strip_prefix(prefix) {
            return dir.get_file(rest);
        }
    }
    None
}

fn not_found(path: &str) -> io::Error {
    io::Error::new(
        io::ErrorKind::NotFound,
        format!("asset embutido não encontrado: {path}"),
    )
}

/// [`AssetSource`] servindo a árvore de assets embutida no binário.
#[derive(Debug, Default, Clone, Copy)]
pub struct EmbeddedAssets;

impl AssetSource for EmbeddedAssets {
    fn read_bytes(&self, path: &str) -> io::Result<Cow<'static, [u8]>> {
        route(path)
            .map(|f| Cow::Borrowed(f.contents()))
            .ok_or_else(|| not_found(path))
    }

    fn read_to_string(&self, path: &str) -> io::Result<Cow<'static, str>> {
        let file = route(path).ok_or_else(|| not_found(path))?;
        file.contents_utf8().map(Cow::Borrowed).ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("asset embutido não é UTF-8 válido: {path}"),
            )
        })
    }

    fn exists(&self, path: &str) -> bool {
        route(path).is_some()
    }

    fn modified(&self, _path: &str) -> Option<SystemTime> {
        None
    }

    fn supports_reload(&self) -> bool {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Cada caminho realmente referenciado em runtime resolve na árvore
    /// embutida — as três convenções de prefixo (views/icons/blueprints), texto
    /// e binário.
    #[test]
    fn resolve_os_assets_referenciados() {
        let a = EmbeddedAssets;
        for p in [
            "views/app.gvb",
            "views/styles/app.gss",
            "views/styles/theme.json",
            "views/scripts/app.luau",
            "views/scripts/handlers/connection.luau",
        ] {
            assert!(a.exists(p), "faltou embutir (texto): {p}");
            assert!(a.read_to_string(p).is_ok(), "não leu (texto): {p}");
        }
        for p in [
            "assets/icons/terminal.svg",
            "assets/fonts/JetBrainsMono-Regular.ttf",
            "assets/blueprint-logos/ackee/logo.png",
        ] {
            assert!(a.exists(p), "faltou embutir (binário): {p}");
            assert!(!a.read_bytes(p).unwrap().is_empty(), "vazio: {p}");
        }
        assert!(!a.exists("views/nao_existe.gvb"));
        assert!(a.read_to_string("views/nao_existe.gvb").is_err());
    }

    /// Prova de ponta a ponta, headless (sem janela e **sem `chdir`**): o motor
    /// carrega o app inteiro só da árvore embutida — template + `<link>`
    /// (estilo/tema) + `<script src>` Luau + a cadeia de `require` dos handlers
    /// + render.
    #[test]
    fn motor_sobe_o_app_so_do_binario() {
        use std::sync::Arc;
        let mut motor = glacier_ui::GlacierUI::new().with_asset_source(Arc::new(EmbeddedAssets));
        motor
            .register_app("views/app.gvb")
            .expect("registrar o app a partir dos assets embutidos");
        assert!(
            motor.render_current().is_ok(),
            "render do app embutido falhou"
        );
    }
}