katana-render-runtime-cli 0.3.1

CLI front-end for katana-render-runtime: render, reference-update, compare, bench.
use crate::commands::{DiagramAction, ThemeModeArg};
use crate::diagram_source::DiagramSourceOps;
#[cfg(test)]
pub(crate) use crate::diagram_source::MermaidMarkdownOps;
use crate::file_ops::FileOps;
use crate::reference_cmd::ReferenceCommand;
use katana_render_runtime::{
    DiagramKind, DrawioRenderer, MathJaxRenderer, MermaidRenderer, PlantUmlRenderer, RenderConfig,
    RenderContext, RenderInput, RenderOutput, RenderPolicy, Renderer, RuntimePathResolver,
};
use std::path::PathBuf;

pub(crate) struct DiagramCommand {
    kind: DiagramKind,
}

impl DiagramCommand {
    pub(crate) fn new(kind: DiagramKind) -> Self {
        Self { kind }
    }

    pub(crate) fn run(self, action: DiagramAction) -> anyhow::Result<()> {
        match action {
            DiagramAction::Render {
                input,
                output,
                runtime,
                theme,
                theme_from,
                theme_mode,
                cache_dir,
            } => self.render(DiagramRenderRequest {
                input_path: input,
                output_path: output,
                runtime,
                theme,
                theme_from,
                theme_mode,
                cache_dir,
            }),
            DiagramAction::ReferenceUpdate { fixtures } => {
                ReferenceCommand::update(self.kind, fixtures)
            }
            DiagramAction::Compare {
                fixtures,
                min_score,
            } => ReferenceCommand::compare(self.kind, fixtures, min_score),
            DiagramAction::Bench { fixtures } => ReferenceCommand::bench(self.kind, fixtures),
        }
    }

    fn render(self, request: DiagramRenderRequest) -> anyhow::Result<()> {
        Self::validate_runtime_options(
            self.kind,
            request.runtime.as_ref(),
            request.cache_dir.as_ref(),
        )?;
        let runtime_path = RuntimePathResolver::resolve_with_plantuml_cache_dir(
            self.kind,
            request.runtime,
            request.cache_dir.clone(),
        )?;
        let source = FileOps::read_to_string(&request.input_path)?;
        let input = RenderInputFactory::create(
            self.kind,
            DiagramSourceOps::prepare(self.kind, source),
            RenderInputFactory::vendor_config(
                self.kind,
                request.theme,
                request.theme_from,
                request.theme_mode,
                request.cache_dir,
            )?,
        );
        let output = self.renderer(runtime_path).render(&input)?;
        Self::write_render_output(request.output_path, &output)
    }

    fn validate_runtime_options(
        kind: DiagramKind,
        runtime: Option<&PathBuf>,
        cache_dir: Option<&PathBuf>,
    ) -> anyhow::Result<()> {
        if kind == DiagramKind::PlantUml && runtime.is_some() && cache_dir.is_some() {
            anyhow::bail!("--runtime and --cache-dir cannot be used together for plantuml");
        }
        Ok(())
    }

    fn renderer(&self, runtime_path: PathBuf) -> Box<dyn Renderer> {
        match self.kind {
            DiagramKind::Mermaid => Box::new(MermaidRenderer::with_runtime_path(runtime_path)),
            DiagramKind::Drawio => Box::new(DrawioRenderer::with_runtime_path(runtime_path)),
            DiagramKind::PlantUml => Box::new(PlantUmlRenderer::with_runtime_path(runtime_path)),
            DiagramKind::MathJax => Box::new(MathJaxRenderer::with_runtime_path(runtime_path)),
        }
    }

    fn write_render_output(
        output_path: Option<PathBuf>,
        output: &RenderOutput,
    ) -> anyhow::Result<()> {
        for warning in &output.diagnostics.warnings {
            eprintln!("{warning}");
        }
        match output_path {
            Some(path) => FileOps::write(&path, output.svg.as_bytes()),
            None => {
                print!("{}", output.svg);
                Ok(())
            }
        }
    }
}

struct DiagramRenderRequest {
    input_path: PathBuf,
    output_path: Option<PathBuf>,
    runtime: Option<PathBuf>,
    theme: Option<String>,
    theme_from: Option<String>,
    theme_mode: Option<ThemeModeArg>,
    cache_dir: Option<PathBuf>,
}

struct RenderInputFactory;

impl RenderInputFactory {
    fn create(kind: DiagramKind, source: String, vendor_config: serde_json::Value) -> RenderInput {
        RenderInput {
            kind,
            source,
            config: RenderConfig { vendor_config },
            policy: RenderPolicy::default(),
            context: RenderContext::default(),
        }
    }

    fn vendor_config(
        kind: DiagramKind,
        theme: Option<String>,
        theme_from: Option<String>,
        theme_mode: Option<ThemeModeArg>,
        cache_dir: Option<PathBuf>,
    ) -> anyhow::Result<serde_json::Value> {
        if theme.is_none() && theme_from.is_none() && theme_mode.is_none() && cache_dir.is_none() {
            return Ok(serde_json::Value::Null);
        }
        if kind != DiagramKind::PlantUml {
            anyhow::bail!(
                "--theme, --theme-from, --theme-mode, and --cache-dir are currently supported only for plantuml"
            );
        }
        Ok(serde_json::json!({
            "plantuml_theme": theme.unwrap_or_default(),
            "plantuml_theme_from": theme_from.unwrap_or_default(),
            "plantuml_theme_mode": theme_mode.map_or("", ThemeModeArg::as_str),
            "plantuml_cache_dir": cache_dir.map_or_else(String::new, |it| it.display().to_string()),
        }))
    }
}

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