query-forge 0.1.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::path::PathBuf;

use anyhow::{Result, bail};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InputSpec {
    pub(crate) path: PathBuf,
    pub(crate) sheet_name: Option<String>,
}

pub(crate) fn parse_input_specs(
    entries: &[String],
    default_sheet: Option<&str>,
) -> Result<Vec<InputSpec>> {
    entries
        .iter()
        .map(|entry| parse_input_entry(entry, default_sheet))
        .collect()
}

fn parse_input_entry(entry: &str, default_sheet: Option<&str>) -> Result<InputSpec> {
    let (path, sheet_name) = split_input_entry(entry)?;
    if path.trim().is_empty() {
        bail!("invalid --input '{entry}', file path cannot be empty");
    }

    Ok(InputSpec {
        path: PathBuf::from(path),
        sheet_name: sheet_name
            .map(str::to_owned)
            .or_else(|| default_sheet.map(str::to_owned)),
    })
}

pub(crate) fn split_input_entry(entry: &str) -> Result<(&str, Option<&str>)> {
    if let Some((path, sheet_name)) = entry.rsplit_once('#') {
        let sheet_name = sheet_name.trim();
        if sheet_name.is_empty() {
            bail!("invalid --input '{entry}', sheet name cannot be empty");
        }

        return Ok((path, Some(sheet_name)));
    }

    let Some((path, sheet_name)) = entry.rsplit_once(':') else {
        return Ok((entry, None));
    };

    // Preserve Windows drive-letter paths like C:\data.xlsx.
    if path.len() == 1
        && path.as_bytes()[0].is_ascii_alphabetic()
        && (sheet_name.starts_with('\\') || sheet_name.starts_with('/'))
    {
        return Ok((entry, None));
    }

    let sheet_name = sheet_name.trim();
    if sheet_name.is_empty() {
        bail!("invalid --input '{entry}', sheet name cannot be empty");
    }

    Ok((path, Some(sheet_name)))
}

#[cfg(test)]
mod tests {
    use super::{parse_input_specs, split_input_entry};

    #[test]
    fn splits_input_path_and_sheet() {
        let (path, sheet) = split_input_entry("./data.xlsx:Consuntivo").expect("valid input");

        assert_eq!(path, "./data.xlsx");
        assert_eq!(sheet, Some("Consuntivo"));
    }

    #[test]
    fn splits_input_path_and_sheet_with_hash_separator() {
        let (path, sheet) = split_input_entry("./data.xlsx#Consuntivo").expect("valid input");

        assert_eq!(path, "./data.xlsx");
        assert_eq!(sheet, Some("Consuntivo"));
    }

    #[test]
    fn keeps_windows_drive_path_without_sheet() {
        let (path, sheet) =
            split_input_entry(r"C:\data\report.xlsx").expect("valid windows path input");

        assert_eq!(path, r"C:\data\report.xlsx");
        assert_eq!(sheet, None);
    }

    #[test]
    fn parses_windows_drive_path_with_hash_sheet() {
        let (path, sheet) =
            split_input_entry(r"C:\data\report.xlsx#WKL").expect("valid windows path input");

        assert_eq!(path, r"C:\data\report.xlsx");
        assert_eq!(sheet, Some("WKL"));
    }

    #[test]
    fn applies_default_sheet_when_not_specified_in_input() {
        let entries = vec!["./consuntivo.xlsx".to_owned(), "./wkl.xlsx:WKL".to_owned()];
        let parsed = parse_input_specs(&entries, Some("Sheet1")).expect("parsed inputs");

        assert_eq!(parsed[0].sheet_name.as_deref(), Some("Sheet1"));
        assert_eq!(parsed[1].sheet_name.as_deref(), Some("WKL"));
    }
}