query-forge 0.2.0

Run SQL queries on XLSX/XML/CSV/JSON/JSONL/Markdown inputs and export results as text, CSV, JSONL, Markdown, XML, or XLSX
Documentation
use std::{fs, path::Path};

use anyhow::{Result, anyhow, bail};

use crate::cli::OutputFormat;

pub(crate) fn write_query_result(
    result: &query_forge::QueryResult,
    output_path: Option<&Path>,
    explicit_format: Option<OutputFormat>,
) -> Result<()> {
    match resolve_output_format(output_path, explicit_format)? {
        OutputFormat::Text => {
            let rendered = query_forge::render_text(result);
            write_or_print(&rendered, output_path)?;
        }
        OutputFormat::Csv => {
            let rendered = query_forge::render_csv(result);
            write_or_print(&rendered, output_path)?;
        }
        OutputFormat::Jsonl => {
            let rendered = query_forge::render_jsonl(result);
            write_or_print(&rendered, output_path)?;
        }
        OutputFormat::Markdown => {
            let rendered = query_forge::render_markdown(result);
            write_or_print(&rendered, output_path)?;
        }
        OutputFormat::Xml => {
            let rendered = query_forge::render_xml(result);
            write_or_print(&rendered, output_path)?;
        }
        OutputFormat::Xlsx => {
            let output_path = output_path
                .ok_or_else(|| anyhow!("--output is required when --format xlsx is selected"))?;
            query_forge::write_xlsx(result, output_path)?;
        }
    }

    Ok(())
}

fn write_or_print(rendered: &str, output_path: Option<&Path>) -> Result<()> {
    if let Some(output_path) = output_path {
        fs::write(output_path, rendered)?;
    } else {
        println!("{rendered}");
    }

    Ok(())
}

pub(crate) fn resolve_output_format(
    output: Option<&Path>,
    format: Option<OutputFormat>,
) -> Result<OutputFormat> {
    if let Some(format) = format {
        if matches!(format, OutputFormat::Xlsx) && output.is_none() {
            bail!("--output is required when --format xlsx is selected");
        }
        return Ok(format);
    }

    if output
        .and_then(|path| path.extension())
        .map(|extension| extension.to_string_lossy())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("xlsx"))
    {
        return Ok(OutputFormat::Xlsx);
    }

    if output
        .and_then(|path| path.extension())
        .map(|extension| extension.to_string_lossy())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("csv"))
    {
        return Ok(OutputFormat::Csv);
    }

    if output
        .and_then(|path| path.extension())
        .map(|extension| extension.to_string_lossy())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("jsonl"))
    {
        return Ok(OutputFormat::Jsonl);
    }

    if output
        .and_then(|path| path.extension())
        .map(|extension| extension.to_string_lossy())
        .is_some_and(|extension| {
            extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown")
        })
    {
        return Ok(OutputFormat::Markdown);
    }

    if output
        .and_then(|path| path.extension())
        .map(|extension| extension.to_string_lossy())
        .is_some_and(|extension| extension.eq_ignore_ascii_case("xml"))
    {
        return Ok(OutputFormat::Xml);
    }

    Ok(OutputFormat::Text)
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use crate::cli::OutputFormat;

    use super::resolve_output_format;

    #[test]
    fn infers_output_format_from_extension() {
        assert!(matches!(
            resolve_output_format(Some(Path::new("result.xlsx")), None).expect("xlsx format"),
            OutputFormat::Xlsx
        ));
        assert!(matches!(
            resolve_output_format(Some(Path::new("result.csv")), None).expect("csv format"),
            OutputFormat::Csv
        ));
        assert!(matches!(
            resolve_output_format(Some(Path::new("result.jsonl")), None).expect("jsonl format"),
            OutputFormat::Jsonl
        ));
        assert!(matches!(
            resolve_output_format(Some(Path::new("result.md")), None).expect("md format"),
            OutputFormat::Markdown
        ));
        assert!(matches!(
            resolve_output_format(Some(Path::new("result.txt")), None).expect("text format"),
            OutputFormat::Text
        ));
    }
}