use std::{
fmt::{Display, Formatter},
thread,
time::Duration,
};
use na_seq::{AminoAcid, seq_aa_from_str};
use serde::Deserialize;
use crate::{ReqError, make_agent};
const BASE_URL: &str = "https://www.uniprot.org/uniprotkb";
const API_URL: &str = "https://rest.uniprot.org/uniprotkb";
const ID_MAPPING_URL: &str = "https://rest.uniprot.org/idmapping";
const ALPHAFOLD_URL: &str = "https://alphafold.ebi.ac.uk";
const PAGE_SIZE_MAX: u32 = 500;
const JOB_POLL_INTERVAL: Duration = Duration::from_millis(500);
const JOB_POLL_ATTEMPTS: u32 = 40;
const USER_AGENT: &str = concat!(
"bio_apis/",
env!("CARGO_PKG_VERSION"),
" (https://github.com/David-OConnor/bio_apis)"
);
#[derive(Clone, PartialEq)]
pub enum Field {
Accession,
Id,
ProteinName,
GeneNames,
OrganismName,
OrganismId,
Lineage,
Sequence,
Length,
Mass,
SequenceVersion,
CcAlternativeProducts,
Ec,
CcFunction,
CcCatalyticActivity,
CcCofactor,
CcActivityRegulation,
CcPathway,
Kinetics,
FtBinding,
FtActSite,
Rhea,
Reviewed,
ProteinExistence,
AnnotationScore,
Keyword,
CcSubunit,
CcInteraction,
CcSubcellularLocation,
FtTransmem,
CcDisease,
FtChain,
FtSignal,
FtDisulfid,
FtModRes,
FtCarbohyd,
Structure3d,
FtHelix,
FtStrand,
FtTurn,
FtDomain,
FtRegion,
FtMotif,
ProteinFamilies,
CcSimilarity,
Go,
XrefPdb,
XrefAlphaFoldDb,
XrefChebi,
XrefDrugBank,
XrefChembl,
XrefEnsembl,
XrefRefSeq,
XrefEmbl,
XrefKegg,
XrefReactome,
DateModified,
Version,
Custom(String),
}
impl Display for Field {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Accession => "accession",
Self::Id => "id",
Self::ProteinName => "protein_name",
Self::GeneNames => "gene_names",
Self::OrganismName => "organism_name",
Self::OrganismId => "organism_id",
Self::Lineage => "lineage",
Self::Sequence => "sequence",
Self::Length => "length",
Self::Mass => "mass",
Self::SequenceVersion => "sequence_version",
Self::CcAlternativeProducts => "cc_alternative_products",
Self::Ec => "ec",
Self::CcFunction => "cc_function",
Self::CcCatalyticActivity => "cc_catalytic_activity",
Self::CcCofactor => "cc_cofactor",
Self::CcActivityRegulation => "cc_activity_regulation",
Self::CcPathway => "cc_pathway",
Self::Kinetics => "kinetics",
Self::FtBinding => "ft_binding",
Self::FtActSite => "ft_act_site",
Self::Rhea => "rhea",
Self::Reviewed => "reviewed",
Self::ProteinExistence => "protein_existence",
Self::AnnotationScore => "annotation_score",
Self::Keyword => "keyword",
Self::CcSubunit => "cc_subunit",
Self::CcInteraction => "cc_interaction",
Self::CcSubcellularLocation => "cc_subcellular_location",
Self::FtTransmem => "ft_transmem",
Self::CcDisease => "cc_disease",
Self::FtChain => "ft_chain",
Self::FtSignal => "ft_signal",
Self::FtDisulfid => "ft_disulfid",
Self::FtModRes => "ft_mod_res",
Self::FtCarbohyd => "ft_carbohyd",
Self::Structure3d => "structure_3d",
Self::FtHelix => "ft_helix",
Self::FtStrand => "ft_strand",
Self::FtTurn => "ft_turn",
Self::FtDomain => "ft_domain",
Self::FtRegion => "ft_region",
Self::FtMotif => "ft_motif",
Self::ProteinFamilies => "protein_families",
Self::CcSimilarity => "cc_similarity",
Self::Go => "go",
Self::XrefPdb => "xref_pdb",
Self::XrefAlphaFoldDb => "xref_alphafolddb",
Self::XrefChebi => "xref_chebi",
Self::XrefDrugBank => "xref_drugbank",
Self::XrefChembl => "xref_chembl",
Self::XrefEnsembl => "xref_ensembl",
Self::XrefRefSeq => "xref_refseq",
Self::XrefEmbl => "xref_embl",
Self::XrefKegg => "xref_kegg",
Self::XrefReactome => "xref_reactome",
Self::DateModified => "date_modified",
Self::Version => "version",
Self::Custom(v) => v.as_str(),
};
write!(f, "{v}")
}
}
#[derive(Clone, Copy, Default, PartialEq)]
pub enum Format {
#[default]
Json,
Tsv,
Fasta,
Txt,
Xml,
List,
Gff,
}
impl Display for Format {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Json => "json",
Self::Tsv => "tsv",
Self::Fasta => "fasta",
Self::Txt => "txt",
Self::Xml => "xml",
Self::List => "list",
Self::Gff => "gff",
};
write!(f, "{v}")
}
}
#[derive(Clone, PartialEq)]
pub enum Database {
UniProtKbAcId,
UniProtKb,
UniProtKbSwissProt,
UniParc,
UniRef50,
UniRef90,
UniRef100,
GeneName,
Pdb,
RefSeqProtein,
RefSeqNucleotide,
EmblGenBankDdbj,
EmblGenBankDdbjCds,
Ensembl,
GeneId,
Kegg,
ChemBl,
DrugBank,
String_,
BioGrid,
Reactome,
GeneCards,
Hgnc,
Mim,
Custom(String),
}
impl Display for Database {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::UniProtKbAcId => "UniProtKB_AC-ID",
Self::UniProtKb => "UniProtKB",
Self::UniProtKbSwissProt => "UniProtKB-Swiss-Prot",
Self::UniParc => "UniParc",
Self::UniRef50 => "UniRef50",
Self::UniRef90 => "UniRef90",
Self::UniRef100 => "UniRef100",
Self::GeneName => "Gene_Name",
Self::Pdb => "PDB",
Self::RefSeqProtein => "RefSeq_Protein",
Self::RefSeqNucleotide => "RefSeq_Nucleotide",
Self::EmblGenBankDdbj => "EMBL-GenBank-DDBJ",
Self::EmblGenBankDdbjCds => "EMBL-GenBank-DDBJ_CDS",
Self::Ensembl => "Ensembl",
Self::GeneId => "GeneID",
Self::Kegg => "KEGG",
Self::ChemBl => "ChEMBL",
Self::DrugBank => "DrugBank",
Self::String_ => "STRING",
Self::BioGrid => "BioGRID",
Self::Reactome => "Reactome",
Self::GeneCards => "GeneCards",
Self::Hgnc => "HGNC",
Self::Mim => "MIM",
Self::Custom(v) => v.as_str(),
};
write!(f, "{v}")
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Evidence {
pub evidence_code: String,
pub source: Option<String>,
pub id: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct EvidencedString {
pub value: String,
pub evidences: Vec<Evidence>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct DbReference {
pub database: String,
pub id: String,
}
impl DbReference {
pub fn id_bare(&self) -> &str {
match self.id.split_once(':') {
Some((_, v)) => v,
None => &self.id,
}
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ProteinName {
pub full_name: EvidencedString,
pub short_names: Vec<EvidencedString>,
pub ec_numbers: Vec<EvidencedString>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ProteinDescription {
pub recommended_name: Option<ProteinName>,
pub submission_names: Vec<ProteinName>,
pub alternative_names: Vec<ProteinName>,
pub contains: Vec<ProteinDescription>,
pub includes: Vec<ProteinDescription>,
}
impl ProteinDescription {
pub fn name(&self) -> Option<&str> {
self.recommended_name
.as_ref()
.or_else(|| self.submission_names.first())
.or_else(|| self.alternative_names.first())
.map(|n| n.full_name.value.as_str())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Gene {
pub gene_name: Option<EvidencedString>,
pub synonyms: Vec<EvidencedString>,
pub ordered_locus_names: Vec<EvidencedString>,
pub orf_names: Vec<EvidencedString>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Organism {
pub scientific_name: String,
pub common_name: Option<String>,
pub taxon_id: u32,
pub lineage: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Sequence {
pub value: String,
pub length: u32,
pub mol_weight: u32,
pub crc64: Option<String>,
pub md5: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct EntryAudit {
pub first_public_date: Option<String>,
pub last_annotation_update_date: Option<String>,
pub last_sequence_update_date: Option<String>,
pub entry_version: Option<u32>,
pub sequence_version: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Keyword {
pub id: String,
pub category: Option<String>,
pub name: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Property {
pub key: String,
pub value: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct CrossReference {
pub database: String,
pub id: String,
pub properties: Vec<Property>,
pub isoform_id: Option<String>,
}
impl CrossReference {
pub fn property(&self, key: &str) -> Option<&str> {
self.properties
.iter()
.find(|p| p.key == key)
.map(|p| p.value.as_str())
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "encode", derive(bincode::Encode, bincode::Decode))]
pub struct PdbXref {
pub id: String,
pub method: Option<String>,
pub resolution: Option<f32>,
pub chains: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Position {
pub value: Option<u32>,
pub modifier: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct FeatureLocation {
pub start: Position,
pub end: Position,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Ligand {
pub name: Option<String>,
pub id: Option<String>,
pub label: Option<String>,
}
impl Ligand {
pub fn chebi_id(&self) -> Option<u32> {
parse_chebi_id(self.id.as_deref()?)
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Feature {
#[serde(rename = "type")]
pub type_: String,
pub location: FeatureLocation,
pub description: Option<String>,
pub feature_id: Option<String>,
pub ligand: Option<Ligand>,
pub feature_cross_references: Vec<DbReference>,
pub evidences: Vec<Evidence>,
}
impl Feature {
pub fn range(&self) -> Option<(u32, u32)> {
Some((self.location.start.value?, self.location.end.value?))
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Reaction {
pub name: Option<String>,
pub reaction_cross_references: Vec<DbReference>,
pub ec_number: Option<String>,
}
impl Reaction {
pub fn rhea_id(&self) -> Option<u32> {
self.reaction_cross_references
.iter()
.find(|x| x.database == "Rhea")
.and_then(|x| x.id_bare().parse().ok())
}
pub fn chebi_ids(&self) -> Vec<u32> {
self.reaction_cross_references
.iter()
.filter(|x| x.database == "ChEBI")
.filter_map(|x| x.id_bare().parse().ok())
.collect()
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Cofactor {
pub name: Option<String>,
pub cofactor_cross_reference: Option<DbReference>,
}
impl Cofactor {
pub fn chebi_id(&self) -> Option<u32> {
parse_chebi_id(&self.cofactor_cross_reference.as_ref()?.id)
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SubcellularLocation {
pub location: Option<EvidencedString>,
pub topology: Option<EvidencedString>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Disease {
pub disease_id: Option<String>,
pub disease_accession: Option<String>,
pub acronym: Option<String>,
pub description: Option<String>,
pub disease_cross_reference: Option<DbReference>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Isoform {
pub name: Option<EvidencedString>,
pub synonyms: Vec<EvidencedString>,
pub isoform_ids: Vec<String>,
pub isoform_sequence_status: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Interactant {
#[serde(rename = "uniProtKBAccession")]
pub accession: Option<String>,
pub gene_name: Option<String>,
pub int_act_id: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Interaction {
pub interactant_one: Interactant,
pub interactant_two: Interactant,
pub number_of_experiments: Option<u32>,
pub organism_differ: bool,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MichaelisConstant {
pub constant: f32,
pub unit: String,
pub substrate: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MaximumVelocity {
pub velocity: f32,
pub unit: String,
pub enzyme: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct KineticParameters {
pub michaelis_constants: Vec<MichaelisConstant>,
pub maximum_velocities: Vec<MaximumVelocity>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(from = "NoteRaw")]
pub struct Note {
pub texts: Vec<EvidencedString>,
}
impl Note {
pub fn text(&self) -> Option<&str> {
self.texts.first().map(|t| t.value.as_str())
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum NoteRaw {
Text(String),
Structured {
#[serde(default)]
texts: Vec<EvidencedString>,
},
}
impl From<NoteRaw> for Note {
fn from(v: NoteRaw) -> Self {
let texts = match v {
NoteRaw::Text(value) => vec![EvidencedString {
value,
evidences: Vec::new(),
}],
NoteRaw::Structured { texts } => texts,
};
Self { texts }
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Comment {
pub comment_type: String,
pub molecule: Option<String>,
pub texts: Vec<EvidencedString>,
pub note: Option<Note>,
pub reaction: Option<Reaction>,
pub cofactors: Vec<Cofactor>,
pub subcellular_locations: Vec<SubcellularLocation>,
pub disease: Option<Disease>,
pub isoforms: Vec<Isoform>,
pub events: Vec<String>,
pub interactions: Vec<Interaction>,
pub kinetic_parameters: Option<KineticParameters>,
pub resource_name: Option<String>,
pub resource_url: Option<String>,
}
impl Comment {
pub fn text(&self) -> Option<&str> {
self.texts.first().map(|t| t.value.as_str())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Citation {
pub id: Option<String>,
pub citation_type: Option<String>,
pub authors: Vec<String>,
pub title: Option<String>,
pub journal: Option<String>,
pub publication_date: Option<String>,
pub volume: Option<String>,
pub first_page: Option<String>,
pub last_page: Option<String>,
pub citation_cross_references: Vec<DbReference>,
}
impl Citation {
pub fn pubmed_id(&self) -> Option<u32> {
self.citation_cross_references
.iter()
.find(|x| x.database == "PubMed")
.and_then(|x| x.id.parse().ok())
}
pub fn doi(&self) -> Option<&str> {
self.citation_cross_references
.iter()
.find(|x| x.database == "DOI")
.map(|x| x.id.as_str())
}
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Reference {
pub reference_number: Option<u32>,
pub citation: Option<Citation>,
pub reference_positions: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ExtraAttributes {
pub uni_parc_id: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Protein {
pub entry_type: String,
pub primary_accession: String,
pub secondary_accessions: Vec<String>,
#[serde(rename = "uniProtkbId")]
pub entry_name: String,
pub entry_audit: Option<EntryAudit>,
pub annotation_score: f32,
pub organism: Option<Organism>,
pub protein_existence: Option<String>,
pub protein_description: ProteinDescription,
pub genes: Vec<Gene>,
pub comments: Vec<Comment>,
pub features: Vec<Feature>,
pub keywords: Vec<Keyword>,
pub references: Vec<Reference>,
#[serde(rename = "uniProtKBCrossReferences")]
pub cross_references: Vec<CrossReference>,
pub sequence: Option<Sequence>,
pub extra_attributes: Option<ExtraAttributes>,
}
impl Protein {
pub fn reviewed(&self) -> bool {
self.entry_type.contains("Swiss-Prot")
}
pub fn name(&self) -> String {
self.protein_description
.name()
.unwrap_or_default()
.to_owned()
}
pub fn gene_names(&self) -> Vec<String> {
self.genes
.iter()
.filter_map(|g| g.gene_name.as_ref())
.map(|n| n.value.clone())
.collect()
}
pub fn ec_numbers(&self) -> Vec<String> {
let mut result: Vec<String> = self
.protein_description
.recommended_name
.iter()
.chain(&self.protein_description.submission_names)
.flat_map(|n| n.ec_numbers.iter())
.map(|e| e.value.clone())
.collect();
for r in self.catalytic_activities() {
if let Some(ec) = &r.ec_number
&& !result.contains(ec)
{
result.push(ec.clone());
}
}
result
}
pub fn seq_aa(&self) -> Vec<AminoAcid> {
match &self.sequence {
Some(s) => seq_aa_from_str(&s.value),
None => Vec::new(),
}
}
pub fn comments_of_type(&self, type_: &str) -> Vec<&Comment> {
self.comments
.iter()
.filter(|c| c.comment_type == type_)
.collect()
}
pub fn text_of_type(&self, type_: &str) -> Option<String> {
self.comments_of_type(type_)
.first()
.and_then(|c| c.text())
.map(str::to_owned)
}
pub fn function(&self) -> Option<String> {
self.text_of_type("FUNCTION")
}
pub fn catalytic_activities(&self) -> Vec<&Reaction> {
self.comments
.iter()
.filter_map(|c| c.reaction.as_ref())
.collect()
}
pub fn rhea_ids(&self) -> Vec<u32> {
self.catalytic_activities()
.iter()
.filter_map(|r| r.rhea_id())
.collect()
}
pub fn chebi_ids(&self) -> Vec<u32> {
let ligands = self.features.iter().filter_map(|f| f.ligand.as_ref());
let cofactors = self.comments.iter().flat_map(|c| c.cofactors.iter());
let mut result: Vec<u32> = Vec::new();
for id in ligands
.filter_map(Ligand::chebi_id)
.chain(cofactors.filter_map(Cofactor::chebi_id))
.chain(
self.catalytic_activities()
.iter()
.flat_map(|r| r.chebi_ids()),
)
{
if !result.contains(&id) {
result.push(id);
}
}
result
}
pub fn subcellular_locations(&self) -> Vec<String> {
self.comments
.iter()
.flat_map(|c| c.subcellular_locations.iter())
.filter_map(|l| l.location.as_ref())
.map(|l| l.value.clone())
.collect()
}
pub fn diseases(&self) -> Vec<&Disease> {
self.comments
.iter()
.filter_map(|c| c.disease.as_ref())
.collect()
}
pub fn isoform_ids(&self) -> Vec<String> {
self.comments
.iter()
.flat_map(|c| c.isoforms.iter())
.flat_map(|i| i.isoform_ids.iter())
.cloned()
.collect()
}
pub fn features_of_type(&self, type_: &str) -> Vec<&Feature> {
self.features.iter().filter(|f| f.type_ == type_).collect()
}
pub fn xrefs(&self, database: &str) -> Vec<&CrossReference> {
self.cross_references
.iter()
.filter(|x| x.database == database)
.collect()
}
pub fn xref_ids(&self, database: &str) -> Vec<String> {
self.xrefs(database).iter().map(|x| x.id.clone()).collect()
}
pub fn pdb_ids(&self) -> Vec<String> {
self.xref_ids("PDB")
}
pub fn pdb_xrefs(&self) -> Vec<PdbXref> {
self.xrefs("PDB")
.into_iter()
.map(|x| PdbXref {
id: x.id.clone(),
method: x.property("Method").map(str::to_owned),
resolution: x
.property("Resolution")
.and_then(|v| v.trim_end_matches(" A").parse().ok()),
chains: x.property("Chains").map(str::to_owned),
})
.collect()
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "encode", derive(bincode::Encode, bincode::Decode))]
pub struct Properties {
pub accession: String,
pub entry_name: String,
pub name: String,
pub reviewed: bool,
pub gene_names: Vec<String>,
pub organism: String,
pub taxon_id: u32,
pub length: u32,
pub mol_weight: u32,
pub ec_numbers: Vec<String>,
pub function: Option<String>,
pub sequence: String,
pub pdb_ids: Vec<String>,
}
impl From<&Protein> for Properties {
fn from(p: &Protein) -> Self {
let (organism, taxon_id) = match &p.organism {
Some(o) => (o.scientific_name.clone(), o.taxon_id),
None => (String::new(), 0),
};
let (sequence, length, mol_weight) = match &p.sequence {
Some(s) => (s.value.clone(), s.length, s.mol_weight),
None => (String::new(), 0, 0),
};
Self {
accession: p.primary_accession.clone(),
entry_name: p.entry_name.clone(),
name: p.name(),
reviewed: p.reviewed(),
gene_names: p.gene_names(),
organism,
taxon_id,
length,
mol_weight,
ec_numbers: p.ec_numbers(),
function: p.function(),
sequence,
pdb_ids: p.pdb_ids(),
}
}
}
const PROPERTIES_FIELDS: [Field; 12] = [
Field::Accession,
Field::Id,
Field::ProteinName,
Field::GeneNames,
Field::OrganismName,
Field::OrganismId,
Field::Length,
Field::Mass,
Field::Ec,
Field::CcFunction,
Field::Sequence,
Field::XrefPdb,
];
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "encode", derive(bincode::Encode, bincode::Decode))]
pub struct IdMapping {
pub from: String,
pub to: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct AlphaFoldPrediction {
pub entry_id: String,
pub uniprot_accession: Option<String>,
pub global_metric_value: Option<f32>,
pub latest_version: Option<u32>,
pub model_created_date: Option<String>,
pub uniprot_start: Option<u32>,
pub uniprot_end: Option<u32>,
pub uniprot_sequence: Option<String>,
pub cif_url: Option<String>,
pub pdb_url: Option<String>,
pub bcif_url: Option<String>,
pub plddt_doc_url: Option<String>,
pub pae_doc_url: Option<String>,
}
pub fn parse_accession(ident: &str) -> String {
let v = ident.trim();
match v.split_once(':') {
Some((prefix, acc))
if prefix.eq_ignore_ascii_case("uniprot")
|| prefix.eq_ignore_ascii_case("uniprotkb") =>
{
acc.trim().to_uppercase()
}
_ => v.to_uppercase(),
}
}
fn parse_chebi_id(id: &str) -> Option<u32> {
id.rsplit(':').next()?.parse().ok()
}
fn request(url: &str) -> Result<ureq::http::Response<ureq::Body>, ReqError> {
let agent = make_agent();
Ok(agent
.get(url)
.header("User-Agent", USER_AGENT)
.header("Accept-Encoding", "identity")
.call()?)
}
fn get(url: &str) -> Result<String, ReqError> {
let mut resp = request(url)?;
if resp.status() != 200 {
return Err(ReqError::Http);
}
Ok(resp.body_mut().read_to_string()?)
}
fn parse_next_link(header: &str) -> Option<String> {
if !header.contains("rel=\"next\"") {
return None;
}
let start = header.find('<')? + 1;
let end = header.find('>')?;
Some(header[start..end].to_owned())
}
fn get_pages(url: &str, max_pages: Option<u32>) -> Result<Vec<String>, ReqError> {
let mut next = Some(url.to_owned());
let mut result = Vec::new();
while let Some(u) = next {
let mut resp = request(&u)?;
if resp.status() != 200 {
return Err(ReqError::Http);
}
next = resp
.headers()
.get("link")
.and_then(|v| v.to_str().ok())
.and_then(parse_next_link);
result.push(resp.body_mut().read_to_string()?);
if let Some(m) = max_pages
&& result.len() >= m as usize
{
break;
}
}
Ok(result)
}
fn fields_param(fields: &[Field]) -> Option<String> {
if fields.is_empty() {
return None;
}
let v: Vec<String> = fields.iter().map(|f| f.to_string()).collect();
Some(v.join(","))
}
fn max_pages(limit: Option<u32>, page_size: u32) -> Option<u32> {
limit.map(|l| l.div_ceil(page_size))
}
pub fn open_overview(accession: &str) {
let url = format!("{BASE_URL}/{}/entry", parse_accession(accession));
if let Err(e) = webbrowser::open(&url) {
eprintln!("Failed to open the web browser: {:?}", e);
}
}
pub fn open_alphafold_view(accession: &str) {
let url = format!("{ALPHAFOLD_URL}/entry/{}", parse_accession(accession));
if let Err(e) = webbrowser::open(&url) {
eprintln!("Failed to open the web browser: {:?}", e);
}
}
pub fn query_search(
query: &str,
fields: &[Field],
format: Format,
size: Option<u32>,
) -> Result<String, ReqError> {
let mut params = url::form_urlencoded::Serializer::new(String::new());
params.append_pair("query", query);
params.append_pair("format", &format.to_string());
if let Some(f) = fields_param(fields) {
params.append_pair("fields", &f);
}
if let Some(s) = size {
params.append_pair("size", &s.min(PAGE_SIZE_MAX).to_string());
}
get(&format!("{API_URL}/search?{}", params.finish()))
}
pub fn query_entry(accession: &str, fields: &[Field], format: Format) -> Result<String, ReqError> {
let mut params = url::form_urlencoded::Serializer::new(String::new());
params.append_pair("format", &format.to_string());
if let Some(f) = fields_param(fields) {
params.append_pair("fields", &f);
}
let url = format!(
"{API_URL}/{}?{}",
parse_accession(accession),
params.finish()
);
get(&url)
}
pub fn load_protein(accession: &str) -> Result<Protein, ReqError> {
Ok(serde_json::from_str(&query_entry(
accession,
&[],
Format::Json,
)?)?)
}
pub fn load_protein_fields(accession: &str, fields: &[Field]) -> Result<Protein, ReqError> {
Ok(serde_json::from_str(&query_entry(
accession,
fields,
Format::Json,
)?)?)
}
#[derive(Debug, Deserialize)]
struct SearchResp {
results: Vec<Protein>,
}
pub fn load_proteins(accessions: &[String], fields: &[Field]) -> Result<Vec<Protein>, ReqError> {
if accessions.is_empty() {
return Ok(Vec::new());
}
let accs: Vec<String> = accessions.iter().map(|a| parse_accession(a)).collect();
let mut params = url::form_urlencoded::Serializer::new(String::new());
params.append_pair("accessions", &accs.join(","));
if let Some(f) = fields_param(fields) {
params.append_pair("fields", &f);
}
let url = format!("{API_URL}/accessions?{}", params.finish());
let parsed: SearchResp = serde_json::from_str(&get(&url)?)?;
Ok(parsed.results)
}
pub fn properties(accession: &str) -> Result<Properties, ReqError> {
Ok((&load_protein_fields(accession, &PROPERTIES_FIELDS)?).into())
}
pub fn load_fasta(accession: &str) -> Result<String, ReqError> {
get(&format!("{API_URL}/{}.fasta", parse_accession(accession)))
}
pub fn load_sequence(accession: &str) -> Result<Vec<AminoAcid>, ReqError> {
let protein = load_protein_fields(accession, &[Field::Sequence])?;
Ok(protein.seq_aa())
}
pub fn search(query: &str, fields: &[Field], limit: Option<u32>) -> Result<Vec<Protein>, ReqError> {
let page_size = limit.unwrap_or(PAGE_SIZE_MAX).min(PAGE_SIZE_MAX);
let mut params = url::form_urlencoded::Serializer::new(String::new());
params.append_pair("query", query);
params.append_pair("format", "json");
params.append_pair("size", &page_size.to_string());
if let Some(f) = fields_param(fields) {
params.append_pair("fields", &f);
}
let url = format!("{API_URL}/search?{}", params.finish());
let mut result = Vec::new();
for page in get_pages(&url, max_pages(limit, page_size))? {
let parsed: SearchResp = serde_json::from_str(&page)?;
if parsed.results.is_empty() {
break;
}
result.extend(parsed.results);
}
if let Some(l) = limit {
result.truncate(l as usize);
}
Ok(result)
}
pub fn count(query: &str) -> Result<u32, ReqError> {
let mut params = url::form_urlencoded::Serializer::new(String::new());
params.append_pair("query", query);
params.append_pair("format", "list");
params.append_pair("size", "1");
let resp = request(&format!("{API_URL}/search?{}", params.finish()))?;
if resp.status() != 200 {
return Err(ReqError::Http);
}
resp.headers()
.get("x-total-results")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.ok_or(ReqError::Deserialize)
}
pub fn find_accessions_from_search(
query: &str,
limit: Option<u32>,
) -> Result<Vec<String>, ReqError> {
let page_size = limit.unwrap_or(PAGE_SIZE_MAX).min(PAGE_SIZE_MAX);
let mut params = url::form_urlencoded::Serializer::new(String::new());
params.append_pair("query", query);
params.append_pair("format", "list");
params.append_pair("size", &page_size.to_string());
let url = format!("{API_URL}/search?{}", params.finish());
let mut result = Vec::new();
for page in get_pages(&url, max_pages(limit, page_size))? {
let accessions: Vec<String> = page
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_owned)
.collect();
if accessions.is_empty() {
break;
}
result.extend(accessions);
}
if let Some(l) = limit {
result.truncate(l as usize);
}
Ok(result)
}
fn query_reviewed(query: &str, reviewed_only: bool) -> String {
if reviewed_only {
format!("({query}) AND (reviewed:true)")
} else {
query.to_owned()
}
}
pub fn proteins_from_gene(
gene: &str,
taxon_id: Option<u32>,
reviewed_only: bool,
fields: &[Field],
limit: Option<u32>,
) -> Result<Vec<Protein>, ReqError> {
let mut query = format!("gene:{gene}");
if let Some(t) = taxon_id {
query += &format!(" AND organism_id:{t}");
}
search(&query_reviewed(&query, reviewed_only), fields, limit)
}
pub fn proteins_from_pdb_id(pdb_id: &str, fields: &[Field]) -> Result<Vec<Protein>, ReqError> {
search(
&format!("xref:pdb-{}", pdb_id.to_uppercase()),
fields,
Some(PAGE_SIZE_MAX),
)
}
pub fn proteins_from_rhea(
master_id: u32,
reviewed_only: bool,
fields: &[Field],
limit: Option<u32>,
) -> Result<Vec<Protein>, ReqError> {
let query = format!("(cc_catalytic_activity:\"rhea:{master_id}\")");
search(&query_reviewed(&query, reviewed_only), fields, limit)
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct JobStatus {
job_status: Option<String>,
messages: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct IdMappingRow {
from: String,
to: serde_json::Value,
}
#[derive(Debug, Deserialize)]
struct IdMappingResp {
results: Vec<IdMappingRow>,
}
#[derive(Debug, Deserialize)]
struct JobResp {
#[serde(rename = "jobId")]
job_id: String,
}
pub fn map_ids(
from: Database,
to: Database,
ids: &[String],
limit: Option<u32>,
) -> Result<Vec<IdMapping>, ReqError> {
if ids.is_empty() {
return Ok(Vec::new());
}
let agent = make_agent();
let mut payload = url::form_urlencoded::Serializer::new(String::new());
payload.append_pair("from", &from.to_string());
payload.append_pair("to", &to.to_string());
payload.append_pair("ids", &ids.join(","));
let mut resp = agent
.post(&format!("{ID_MAPPING_URL}/run"))
.header("User-Agent", USER_AGENT)
.header("Content-Type", "application/x-www-form-urlencoded")
.send(&payload.finish())?;
if resp.status() != 200 {
return Err(ReqError::Http);
}
let job: JobResp = serde_json::from_str(&resp.body_mut().read_to_string()?)?;
let status_url = format!("{ID_MAPPING_URL}/status/{}", job.job_id);
let mut finished = false;
for _ in 0..JOB_POLL_ATTEMPTS {
let body = get(&status_url)?;
let status: JobStatus = serde_json::from_str(&body)?;
match status.job_status.as_deref() {
Some("NEW") | Some("RUNNING") => thread::sleep(JOB_POLL_INTERVAL),
Some("ERROR") => return Err(ReqError::Http),
_ => {
if !status.messages.is_empty() {
return Err(ReqError::Http);
}
finished = true;
break;
}
}
}
if !finished {
return Err(ReqError::Http);
}
let page_size = limit.unwrap_or(PAGE_SIZE_MAX).min(PAGE_SIZE_MAX);
let url = format!(
"{ID_MAPPING_URL}/results/{}?format=json&size={page_size}",
job.job_id
);
let mut result = Vec::new();
for page in get_pages(&url, max_pages(limit, page_size))? {
let parsed: IdMappingResp = serde_json::from_str(&page)?;
if parsed.results.is_empty() {
break;
}
for row in parsed.results {
let to = match row.to {
serde_json::Value::String(v) => Some(v),
serde_json::Value::Object(ref o) => o
.get("primaryAccession")
.and_then(|v| v.as_str())
.map(str::to_owned),
_ => None,
};
if let Some(to) = to {
result.push(IdMapping { from: row.from, to });
}
}
}
if let Some(l) = limit {
result.truncate(l as usize);
}
Ok(result)
}
pub fn load_alphafold_predictions(accession: &str) -> Result<Vec<AlphaFoldPrediction>, ReqError> {
let url = format!(
"{ALPHAFOLD_URL}/api/prediction/{}",
parse_accession(accession)
);
Ok(serde_json::from_str(&get(&url)?)?)
}
fn alphafold_file_url(accession: &str, pdb: bool) -> Result<String, ReqError> {
let prediction = load_alphafold_predictions(accession)?
.into_iter()
.next()
.ok_or(ReqError::Deserialize)?;
let url = if pdb {
prediction.pdb_url
} else {
prediction.cif_url
};
url.ok_or(ReqError::Deserialize)
}
pub fn load_alphafold_cif(accession: &str) -> Result<String, ReqError> {
get(&alphafold_file_url(accession, false)?)
}
pub fn load_alphafold_pdb(accession: &str) -> Result<String, ReqError> {
get(&alphafold_file_url(accession, true)?)
}