query-forge 0.9.0

Run SQL queries and dataset diffs on XLSX/XML/CSV/JSON/JSONL/Markdown/HTML/Feather/Parquet inputs and export results as text, CSV, JSONL, Markdown, XML, HTML, XLSX, Feather, or Parquet
Documentation
use std::path::Path;

use anyhow::{Result, bail};
#[cfg(not(test))]
use anyhow::Context;

const CLIPBOARD_STEM: &str = "@clipboard";

pub(crate) fn is_clipboard_path(path: &Path) -> bool {
    path.file_stem()
        .and_then(|stem| stem.to_str())
        .is_some_and(|stem| stem.eq_ignore_ascii_case(CLIPBOARD_STEM))
}

#[allow(dead_code)]
pub(crate) fn read_text(_path: &Path) -> Result<String> {
    #[cfg(test)]
    {
        Ok(test_store().lock().expect("clipboard lock").clone())
    }

    #[cfg(not(test))]
    {
        let mut clipboard =
            arboard::Clipboard::new().context("failed to access the system clipboard")?;
        clipboard
            .get_text()
            .with_context(|| format!("failed to read text from clipboard for {}", _path.display()))
    }
}

#[allow(dead_code)]
pub(crate) fn write_text(_path: &Path, text: &str) -> Result<()> {
    #[cfg(test)]
    {
        *test_store().lock().expect("clipboard lock") = text.to_owned();
        Ok(())
    }

    #[cfg(not(test))]
    {
        let mut clipboard =
            arboard::Clipboard::new().context("failed to access the system clipboard")?;
        clipboard
            .set_text(text.to_owned())
            .with_context(|| format!("failed to write text to clipboard for {}", _path.display()))
    }
}

#[allow(dead_code)]
pub(crate) fn ensure_supported_clipboard_input(path: &Path) -> Result<()> {
    if matches!(
        path.extension().and_then(|extension| extension.to_str()),
        Some(extension)
            if ["csv", "json", "jsonl", "md", "markdown", "html", "htm", "xml"]
                .iter()
                .any(|candidate| extension.eq_ignore_ascii_case(candidate))
    ) {
        return Ok(());
    }

    bail!(
        "clipboard input {} requires a format specifier; use the |type suffix such as \
         @clipboard|csv, @clipboard|json, @clipboard|jsonl, @clipboard|md, @clipboard|html, \
         or @clipboard|xml",
        path.display()
    )
}

#[allow(dead_code)]
pub(crate) fn ensure_supported_clipboard_output(path: &Path) -> Result<()> {
    if matches!(
        path.extension().and_then(|extension| extension.to_str()),
        Some(extension)
            if ["txt", "csv", "json", "jsonl", "md", "markdown", "html", "htm", "xml"]
                .iter()
                .any(|candidate| extension.eq_ignore_ascii_case(candidate))
    ) || path.extension().is_none()
    {
        return Ok(());
    }

    bail!(
        "clipboard output {} only supports text-based formats; use the |type suffix such as \
         @clipboard|csv, @clipboard|json, @clipboard|jsonl, @clipboard|md, @clipboard|html, \
         or @clipboard|xml",
        path.display()
    )
}

#[cfg(test)]
fn test_store() -> &'static std::sync::Mutex<String> {
    static STORE: std::sync::OnceLock<std::sync::Mutex<String>> = std::sync::OnceLock::new();
    STORE.get_or_init(|| std::sync::Mutex::new(String::new()))
}

#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn set_test_text(text: &str) {
    *test_store().lock().expect("clipboard lock") = text.to_owned();
}

#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn get_test_text() -> String {
    test_store().lock().expect("clipboard lock").clone()
}