genome-sh 0.1.0

The jq of genomics. Fast, local, human-readable variant analysis.
use anyhow::{Context, Result};
use clap::Args;

use crate::db::Database;
use crate::output::{Format, VariantPrinter};
use crate::variant::{AnnotatedVariant, VariantQuery};

const DEFAULT_API_URL: &str = "https://api.genome.sh";

#[derive(Args)]
pub struct QueryArgs {
    /// Variant identifier(s): rsID, coordinates (chr:pos:ref:alt), HGVS, or gene name.
    #[arg(required = true, num_args = 1..)]
    pub queries: Vec<String>,

    /// Output format.
    #[arg(short, long, default_value = "human")]
    pub format: Format,

    /// Use local database instead of the API.
    #[arg(long)]
    pub local: bool,

    /// API base URL (default: https://api.genome.sh).
    #[arg(long, env = "GENOME_API_URL", default_value = DEFAULT_API_URL)]
    pub api_url: String,
}

impl QueryArgs {
    pub async fn run(self) -> Result<()> {
        let printer = VariantPrinter::new(self.format.clone());

        for raw in &self.queries {
            let results = if self.local {
                self.query_local(raw)?
            } else {
                self.query_api(raw).await?
            };

            if results.is_empty() {
                eprintln!("No results for: {raw}");
                continue;
            }

            for variant in &results {
                printer.print(variant)?;
            }
        }

        Ok(())
    }

    fn query_local(&self, raw: &str) -> Result<Vec<AnnotatedVariant>> {
        let db = Database::open()
            .context("No local database. Run `genome db install` or remove --local.")?;
        let query = VariantQuery::parse(raw)?;
        db.query(&query)
    }

    async fn query_api(&self, raw: &str) -> Result<Vec<AnnotatedVariant>> {
        let url = format!("{}/v1/query/{}", self.api_url, urlencoding::encode(raw));

        let response = reqwest::get(&url).await.with_context(|| {
            format!(
                "Failed to reach API at {}. Use --local for offline mode.",
                self.api_url
            )
        })?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            anyhow::bail!("API returned {status}: {body}");
        }

        let variants: Vec<AnnotatedVariant> = response
            .json()
            .await
            .context("Failed to parse API response")?;

        Ok(variants)
    }
}