datavzrd 2.75.2

A tool to create visual HTML reports from collections of CSV/TSV tables
use crate::render::portable::utils::{render_index_file, render_static_files};
use crate::render::portable::ItemRenderer;
use crate::render::Renderer;
use crate::spec::ItemsSpec;
use anyhow::{bail, Context, Result};
use std::fs;
use std::path::PathBuf;
use thiserror::Error;

pub mod render;
pub mod spec;
pub mod spells;
pub mod utils;

/// Renders a report based on the provided configuration file and outputs it to the specified directory.
///
/// # Arguments
///
/// * `config_file` - A `PathBuf` reference to the configuration file containing the report specifications.
/// * `output` - A `PathBuf` reference to the directory where the rendered report will be saved.
/// * `webview_url` - A string slice representing the base URL for webview links in the report.
/// * `debug` - A boolean flag indicating whether to enable debug mode for rendering.
/// * `overwrite_output` - A boolean flag indicating whether to overwrite the output directory if it is not empty.
/// * `dryrun` - A boolean flag indicating whether to only validate the configuration and report what would be rendered, without writing anything.
///
/// # Returns
///
/// * `Result<()>` - Returns `Ok(())` if the report is successfully rendered, or an error if any step fails.
///
/// # Errors
///
/// * Returns an error if the configuration file is invalid or cannot be read.
/// * Returns an error if the output directory is not empty and `overwrite_output` is not set to `true`.
/// * Returns an error if any of the rendering steps fail.
pub fn render_report(
    config_file: &PathBuf,
    output: &PathBuf,
    webview_url: &str,
    debug: bool,
    overwrite_output: bool,
    dryrun: bool,
) -> Result<()> {
    let config = ItemsSpec::from_file(config_file)?;
    config.validate()?;

    if dryrun {
        let count = config.views.len();
        let views = if count == 1 { "view" } else { "views" };
        println!(
            "Configuration is valid. {count} {views} would be rendered to {output:?}. Nothing written as called with --dryrun/-n flag."
        );
        return Ok(());
    }

    if !output.exists() {
        fs::create_dir_all(output)
            .with_context(|| format!("Failed to create output directory {output:?}"))?;
    } else if output
        .read_dir()
        .with_context(|| format!("Failed to read output directory {output:?}"))?
        .next()
        .is_some()
    {
        if overwrite_output {
            fs::remove_dir_all(output)?;
            fs::create_dir(output)?;
        } else {
            bail!(OutputError::OutputDirectoryNotEmpty {
                output_path: output.to_path_buf(),
            })
        }
    }

    render_index_file(output, &config)?;
    render_static_files(output)?;

    let renderer = ItemRenderer::builder().specs(config).build();
    renderer.render_tables(output, webview_url, debug)?;

    Ok(())
}

#[derive(Error, Debug)]
pub enum OutputError {
    #[error("Given output directory {output_path:?} was not empty. If you wish to overwrite it please use the --overwrite-output option.")]
    OutputDirectoryNotEmpty { output_path: PathBuf },
}

#[cfg(test)]
mod tests {
    use super::render_report;
    use std::io::Write;

    #[test]
    fn test_render_report_creates_missing_parent_directories() {
        let mut config_file = tempfile::NamedTempFile::new().unwrap();
        config_file
            .write_all(
                b"datasets:\n  d:\n    path: tests/data/uniform_datatypes.csv\nviews:\n  d:\n    dataset: d\n",
            )
            .unwrap();
        let output_dir = tempfile::tempdir().unwrap();
        let output = output_dir.path().join("reports").join("report");
        render_report(
            &config_file.path().to_path_buf(),
            &output,
            "",
            false,
            false,
            false,
        )
        .unwrap();
        assert!(output.join("index.html").exists());
    }
}