katana-canvas-forge 0.1.7

Versioned diagram rendering and document export runtime for KatanA (Mermaid, Draw.io, HTML/PDF/PNG/JPEG).
Documentation
use crate::markdown::runtime_assets::RuntimeAsset;
use std::path::PathBuf;

pub struct MermaidBinaryOps;

impl MermaidBinaryOps {
    pub fn default_install_path() -> Option<PathBuf> {
        Some(RuntimeAsset::mermaid().materialized_path())
    }

    pub fn resolve_mermaid_js() -> Result<PathBuf, String> {
        Self::resolve_mermaid_js_with_env(
            std::env::var_os("MERMAID_JS"),
            Self::default_install_path(),
        )
    }

    fn resolve_mermaid_js_with_env(
        env_value: Option<std::ffi::OsString>,
        home_path: Option<PathBuf>,
    ) -> Result<PathBuf, String> {
        if let Some(path) = Self::env_mermaid_js_from(env_value)? {
            return Ok(path);
        }

        Self::resolve_mermaid_js_with_home(home_path)
    }

    fn env_mermaid_js_from(value: Option<std::ffi::OsString>) -> Result<Option<PathBuf>, String> {
        let Some(path) = value else {
            return Ok(None);
        };
        if path.is_empty() {
            return Err("MERMAID_JS is empty".to_string());
        }
        Ok(Some(PathBuf::from(path)))
    }

    fn resolve_mermaid_js_with_home(home_path: Option<PathBuf>) -> Result<PathBuf, String> {
        let Some(path) = home_path else {
            return Err("bundled Mermaid.js path is unavailable".to_string());
        };
        RuntimeAsset::mermaid().materialize_at(path)
    }

    pub fn find_mermaid_js() -> Result<Option<PathBuf>, String> {
        Self::find_mermaid_js_from(Self::resolve_mermaid_js())
    }

    fn find_mermaid_js_from(path: Result<PathBuf, String>) -> Result<Option<PathBuf>, String> {
        let path = path?;
        Ok(path.exists().then_some(path))
    }
}

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

    #[test]
    fn resolve_mermaid_js_reports_missing_default_without_fallback() {
        let result = MermaidBinaryOps::resolve_mermaid_js_with_home(None);

        assert!(matches!(result, Err(error) if error.contains("bundled Mermaid.js")));
    }

    #[test]
    fn resolve_mermaid_js_uses_versioned_repository_asset_without_env() {
        let result = MermaidBinaryOps::resolve_mermaid_js_with_env(
            None,
            MermaidBinaryOps::default_install_path(),
        );

        assert!(matches!(
            result,
            Ok(path) if path.ends_with("vendor/mermaid/3.3.1/mermaid.min.js")
        ));
    }

    #[test]
    fn resolve_mermaid_js_accepts_explicit_env_override() {
        let result = MermaidBinaryOps::resolve_mermaid_js_with_env(
            Some(std::ffi::OsString::from("runtime.js")),
            None,
        );

        assert!(matches!(result, Ok(path) if path == std::path::Path::new("runtime.js")));
    }

    #[test]
    fn resolve_mermaid_js_rejects_empty_env_override() {
        let result = MermaidBinaryOps::resolve_mermaid_js_with_env(
            Some(std::ffi::OsString::new()),
            Some(std::path::PathBuf::from("fallback.js")),
        );

        assert!(matches!(result, Err(error) if error.contains("MERMAID_JS")));
    }

    #[test]
    fn find_mermaid_js_propagates_resolution_errors() {
        let result = MermaidBinaryOps::find_mermaid_js_from(Err("boom".to_string()));

        assert!(matches!(result, Err(error) if error == "boom"));
    }

    #[test]
    fn env_mermaid_js_rejects_empty_override() {
        let result = MermaidBinaryOps::env_mermaid_js_from(Some(std::ffi::OsString::new()));

        assert!(matches!(result, Err(error) if error.contains("MERMAID_JS")));
    }

    #[test]
    fn env_mermaid_js_treats_missing_override_as_optional() {
        let result = MermaidBinaryOps::env_mermaid_js_from(None);

        assert!(matches!(result, Ok(None)));
    }
}