katana-render-runtime 0.3.0

Versioned render runtime for KatanA diagrams and math (Mermaid, Draw.io, ZenUML, PlantUML, MathJax).
Documentation
use super::asset::PlantUmlJarAssetOps;
use super::types::PlantUmlRuntimeWarning;
use std::path::{Path, PathBuf};

pub(crate) struct PlantUmlRuntimePaths {
    pub(crate) jvm_path: PathBuf,
    pub(crate) jar_path: PathBuf,
}

pub(crate) struct PlantUmlRuntimePathOps;

impl PlantUmlRuntimePathOps {
    pub(crate) fn surface_jar_path() -> PathBuf {
        Self::env_path("KDR_PLANTUML_JAR")
            .or_else(|| Self::env_path("PLANTUML_JAR"))
            .unwrap_or_else(|| PlantUmlJarAssetOps::cache_path(None))
    }

    pub(crate) fn surface_jar_path_for_cache_dir(cache_dir: &Path) -> PathBuf {
        Self::env_path("KDR_PLANTUML_JAR")
            .or_else(|| Self::env_path("PLANTUML_JAR"))
            .unwrap_or_else(|| PlantUmlJarAssetOps::cache_path(Some(cache_dir)))
    }

    pub(crate) fn effective_jar_path(jar_path: &Path, cache_dir: Option<&Path>) -> PathBuf {
        if cache_dir.is_some() && Self::can_override_cache_path(jar_path) {
            return PlantUmlJarAssetOps::cache_path(cache_dir);
        }
        jar_path.to_path_buf()
    }

    pub(crate) fn resolve_paths(
        jar_path: &Path,
        cache_dir: Option<&Path>,
    ) -> Result<PlantUmlRuntimePaths, PlantUmlRuntimeWarning> {
        let jar_path = Self::resolve_existing_jar(jar_path, cache_dir)?;
        let jvm_path = Self::resolve_existing_jvm()?;
        Ok(PlantUmlRuntimePaths { jvm_path, jar_path })
    }

    fn resolve_existing_jar(
        jar_path: &Path,
        cache_dir: Option<&Path>,
    ) -> Result<PathBuf, PlantUmlRuntimeWarning> {
        if Self::is_cache_jar_path(jar_path, cache_dir) {
            return PlantUmlJarAssetOps::prepare_cache_jar(cache_dir).map_err(Self::jar_warning);
        }
        if !jar_path.exists() {
            let candidates = vec![jar_path.to_path_buf()];
            return Err(PlantUmlRuntimeWarning::new(
                "plantuml.jar was not found",
                Self::jar_env_names(),
                Self::display_paths(&candidates),
            ));
        }
        PlantUmlJarAssetOps::verify_jar(jar_path).map_err(Self::jar_warning)?;
        Ok(jar_path.to_path_buf())
    }

    fn can_override_cache_path(jar_path: &Path) -> bool {
        Self::env_path("KDR_PLANTUML_JAR").is_none()
            && Self::env_path("PLANTUML_JAR").is_none()
            && Self::is_cache_jar_path(jar_path, None)
    }

    fn is_cache_jar_path(jar_path: &Path, cache_dir: Option<&Path>) -> bool {
        jar_path == PlantUmlJarAssetOps::cache_path(cache_dir)
    }

    fn jar_warning(reason: String) -> PlantUmlRuntimeWarning {
        PlantUmlRuntimeWarning::new(reason, Self::jar_env_names(), Vec::new())
    }

    fn jar_env_names() -> Vec<&'static str> {
        vec!["KDR_PLANTUML_JAR", "PLANTUML_JAR", "KDR_PLANTUML_CACHE_DIR"]
    }

    fn resolve_existing_jvm() -> Result<PathBuf, PlantUmlRuntimeWarning> {
        let candidates = Self::jvm_candidates();
        Self::resolve_jvm_from_candidates(candidates)
    }

    fn resolve_jvm_from_candidates(
        candidates: Vec<PathBuf>,
    ) -> Result<PathBuf, PlantUmlRuntimeWarning> {
        Self::first_existing(candidates.clone()).ok_or_else(|| {
            PlantUmlRuntimeWarning::new(
                "libjvm was not found",
                vec!["KDR_PLANTUML_JVM", "JAVA_HOME"],
                Self::display_paths(&candidates),
            )
        })
    }

    fn first_existing(candidates: Vec<PathBuf>) -> Option<PathBuf> {
        candidates.into_iter().find(|it| it.exists())
    }

    fn jvm_candidates() -> Vec<PathBuf> {
        let mut candidates = Vec::new();
        Self::push_env_path(&mut candidates, "KDR_PLANTUML_JVM");
        if let Some(java_home) = Self::env_path("JAVA_HOME") {
            candidates.extend(Self::java_home_jvm_candidates(&java_home));
        }
        candidates.extend(Self::platform_jvm_candidates());
        candidates
    }

    fn java_home_jvm_candidates(java_home: &Path) -> Vec<PathBuf> {
        vec![
            java_home
                .join("lib")
                .join("server")
                .join(Self::libjvm_name()),
            java_home
                .join("jre")
                .join("lib")
                .join("server")
                .join(Self::libjvm_name()),
            java_home
                .join("bin")
                .join("server")
                .join(Self::libjvm_name()),
        ]
    }

    fn platform_jvm_candidates() -> Vec<PathBuf> {
        if cfg!(target_os = "macos") {
            return vec![
                "/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home/lib/server/libjvm.dylib".into(),
                "/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home/lib/server/libjvm.dylib".into(),
                "/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/lib/server/libjvm.dylib".into(),
            ];
        }
        if cfg!(target_os = "linux") {
            return vec![
                "/usr/lib/jvm/default-java/lib/server/libjvm.so".into(),
                "/usr/lib/jvm/default/lib/server/libjvm.so".into(),
                "/usr/lib/jvm/java-21-openjdk-amd64/lib/server/libjvm.so".into(),
                "/usr/lib/jvm/java-17-openjdk-amd64/lib/server/libjvm.so".into(),
            ];
        }
        Vec::new()
    }

    fn libjvm_name() -> &'static str {
        if cfg!(target_os = "windows") {
            "jvm.dll"
        } else if cfg!(target_os = "macos") {
            "libjvm.dylib"
        } else {
            "libjvm.so"
        }
    }

    fn push_env_path(candidates: &mut Vec<PathBuf>, name: &'static str) {
        if let Some(path) = Self::env_path(name) {
            candidates.push(path);
        }
    }

    fn env_path(name: &'static str) -> Option<PathBuf> {
        let value = std::env::var_os(name)?;
        (!value.is_empty()).then(|| PathBuf::from(value))
    }

    fn display_paths(paths: &[PathBuf]) -> Vec<String> {
        paths.iter().map(|it| it.display().to_string()).collect()
    }
}

#[cfg(test)]
#[path = "resolve_tests.rs"]
mod tests;