genome-sh 0.1.0

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

const GNOMAD_API_URL: &str = "https://gnomad.broadinstitute.org/api";

/// Client for querying gnomAD's public GraphQL API.
/// We proxy queries through our API and cache results.
pub struct GnomadClient {
    client: reqwest::Client,
}

#[derive(Debug, Serialize)]
pub struct GnomadVariant {
    pub rsid: Option<String>,
    pub chrom: String,
    pub pos: u64,
    pub r#ref: String,
    pub alt: String,
    pub af: Option<f64>,
    pub populations: Vec<PopulationFrequency>,
    pub homozygote_count: Option<u64>,
    pub allele_count: Option<u64>,
    pub allele_number: Option<u64>,
}

#[derive(Debug, Serialize)]
pub struct PopulationFrequency {
    pub population: String,
    pub af: f64,
    pub ac: u64,
    pub an: u64,
}

impl GnomadClient {
    pub fn new() -> Self {
        Self {
            client: reqwest::Client::new(),
        }
    }

    /// Query gnomAD by rsID or variant ID.
    pub async fn query(&self, query: &str) -> Result<serde_json::Value> {
        let graphql_query = if query.starts_with("rs") {
            // Search by rsID.
            format!(
                r#"{{
                    variant(rsid: "{query}", dataset: gnomad_r4) {{
                        rsids
                        chrom
                        pos
                        ref
                        alt
                        exome {{
                            ac
                            an
                            ac_hom
                            populations {{
                                id
                                ac
                                an
                            }}
                        }}
                        genome {{
                            ac
                            an
                            ac_hom
                            populations {{
                                id
                                ac
                                an
                            }}
                        }}
                    }}
                }}"#,
            )
        } else {
            // Try variant ID format: chrom-pos-ref-alt.
            let variant_id = query.replace(':', "-");
            format!(
                r#"{{
                    variant(variantId: "{variant_id}", dataset: gnomad_r4) {{
                        rsids
                        chrom
                        pos
                        ref
                        alt
                        exome {{
                            ac
                            an
                            ac_hom
                            populations {{
                                id
                                ac
                                an
                            }}
                        }}
                        genome {{
                            ac
                            an
                            ac_hom
                            populations {{
                                id
                                ac
                                an
                            }}
                        }}
                    }}
                }}"#,
            )
        };

        let response = self
            .client
            .post(GNOMAD_API_URL)
            .json(&serde_json::json!({ "query": graphql_query }))
            .send()
            .await
            .context("Failed to reach gnomAD API")?;

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

        let data: serde_json::Value = response
            .json()
            .await
            .context("Failed to parse gnomAD response")?;

        Ok(data)
    }
}