use std::collections::HashMap;
use serde::Deserialize;
use serde_aux::prelude::*;
use crate::{ReqError, make_agent};
const BASE_URL: &str = "https://www.ebi.ac.uk/chebi";
const API_URL: &str = "https://www.ebi.ac.uk/chebi/backend/api/public";
#[derive(Clone, Debug, Deserialize)]
pub struct Structure {
pub id: u32,
pub smiles: Option<String>,
pub standard_inchi: Option<String>,
pub standard_inchi_key: Option<String>,
pub wurcs: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub is_r_group: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ChemicalData {
pub formula: Option<String>,
pub charge: Option<i16>,
#[serde(default, deserialize_with = "deserialize_option_number_from_string")]
pub mass: Option<f32>,
#[serde(default, deserialize_with = "deserialize_option_number_from_string")]
pub monoisotopic_mass: Option<f32>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Name {
pub name: String,
pub ascii_name: Option<String>,
#[serde(rename = "type")]
pub type_: Option<String>,
pub status: Option<String>,
pub source: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub adapted: bool,
pub language_code: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct DatabaseAccession {
pub id: u32,
pub accession_number: String,
#[serde(rename = "type")]
pub type_: Option<String>,
pub source_name: Option<String>,
pub url: Option<String>,
pub prefix: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct OntologyRelation {
pub init_id: u32,
pub init_name: String,
pub relation_type: String,
pub final_id: u32,
pub final_name: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct OntologyRelations {
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub incoming_relations: Vec<OntologyRelation>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub outgoing_relations: Vec<OntologyRelation>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct RoleClassification {
pub id: u32,
pub chebi_accession: String,
pub name: String,
pub definition: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub biological_role: bool,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub chemical_role: bool,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub application: bool,
}
#[allow(unused)]
#[derive(Clone, Debug, Deserialize)]
pub struct CompoundOrigin {
pub species_text: Option<String>,
pub species_accession: Option<String>,
pub component_text: Option<String>,
pub component_accession: Option<String>,
pub strain_text: Option<String>,
pub strain_accession: Option<String>,
pub source: Option<String>,
pub source_accession: Option<String>,
pub comments: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Compound {
pub id: u32,
pub chebi_accession: String,
pub name: Option<String>,
pub ascii_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub stars: u8,
pub definition: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub names: HashMap<String, Vec<Name>>,
pub chemical_data: Option<ChemicalData>,
pub default_structure: Option<Structure>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub secondary_ids: Vec<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub compound_origins: Vec<CompoundOrigin>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub database_accessions: HashMap<String, Vec<DatabaseAccession>>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub ontology_relations: OntologyRelations,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub roles_classification: Vec<RoleClassification>,
pub modified_on: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub is_released: bool,
}
impl Compound {
pub fn name_of_type(&self, type_: &str) -> Option<String> {
let n = self.names.get(type_)?.first()?;
Some(n.ascii_name.clone().unwrap_or_else(|| n.name.clone()))
}
pub fn xrefs_from_source(&self, source: &str) -> Vec<String> {
self.database_accessions
.values()
.flatten()
.filter(|a| a.source_name.as_deref() == Some(source))
.map(|a| a.accession_number.clone())
.collect()
}
}
#[derive(Debug, Deserialize)]
struct CompoundEntry {
data: Option<Compound>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "encode", derive(bincode::Encode, bincode::Decode))]
pub struct Properties {
pub id: u32,
pub name: String,
pub iupac_name: Option<String>,
pub definition: Option<String>,
pub formula: Option<String>,
pub mass: Option<f32>,
pub monoisotopic_mass: Option<f32>,
pub charge: Option<i16>,
pub smiles: Option<String>,
pub inchi: Option<String>,
pub inchi_key: Option<String>,
}
impl From<&Compound> for Properties {
fn from(c: &Compound) -> Self {
let (formula, mass, monoisotopic_mass, charge) = match &c.chemical_data {
Some(d) => (d.formula.clone(), d.mass, d.monoisotopic_mass, d.charge),
None => (None, None, None, None),
};
let (smiles, inchi, inchi_key) = match &c.default_structure {
Some(s) => (
s.smiles.clone(),
s.standard_inchi.clone(),
s.standard_inchi_key.clone(),
),
None => (None, None, None),
};
Self {
id: c.id,
name: c
.ascii_name
.clone()
.or_else(|| c.name.clone())
.unwrap_or_default(),
iupac_name: c.name_of_type("IUPAC NAME"),
definition: c.definition.clone(),
formula,
mass,
monoisotopic_mass,
charge,
smiles,
inchi,
inchi_key,
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct SearchHit {
#[serde(rename = "_id", deserialize_with = "deserialize_number_from_string")]
pub id: u32,
#[serde(rename = "_score")]
pub score: Option<f32>,
#[serde(rename = "_source")]
pub data: SearchHitData,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SearchHitData {
pub chebi_accession: String,
pub name: Option<String>,
pub ascii_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub stars: u8,
pub definition: Option<String>,
pub formula: Option<String>,
pub mass: Option<f32>,
#[serde(rename = "monoisotopicmass")]
pub monoisotopic_mass: Option<f32>,
pub charge: Option<i16>,
pub smiles: Option<String>,
pub inchi: Option<String>,
#[serde(rename = "inchikey")]
pub inchi_key: Option<String>,
#[serde(default, deserialize_with = "deserialize_default_from_null")]
pub structures: Vec<u32>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct SearchResults {
pub results: Vec<SearchHit>,
pub total: u32,
pub number_pages: u32,
}
#[derive(Clone, Copy, PartialEq)]
pub enum StructureSearchType {
Connectivity,
Similarity,
Substructure,
}
impl std::fmt::Display for StructureSearchType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Connectivity => "connectivity",
Self::Similarity => "similarity",
Self::Substructure => "substructure",
};
write!(f, "{v}")
}
}
pub fn parse_id(ident: &str) -> Result<u32, ReqError> {
let v = ident.trim();
let digits = match v.split_once(':') {
Some((prefix, num)) if prefix.eq_ignore_ascii_case("chebi") => num,
Some(_) => return Err(ReqError::Deserialize),
None => v,
};
digits.trim().parse().map_err(|_| ReqError::Deserialize)
}
fn get(url: &str) -> Result<String, ReqError> {
let agent = make_agent();
let mut resp = agent.get(url).call()?;
if resp.status() != 200 {
return Err(ReqError::Http);
}
Ok(resp.body_mut().read_to_string()?)
}
pub fn open_overview(id: u32) {
if let Err(e) = webbrowser::open(&format!("{BASE_URL}/CHEBI:{id}")) {
eprintln!("Failed to open the web browser: {:?}", e);
}
}
pub fn load_compound(id: u32) -> Result<Compound, ReqError> {
let url = format!("{API_URL}/compound/{id}/");
Ok(serde_json::from_str(&get(&url)?)?)
}
pub fn load_compounds(idents: &[u32]) -> Result<Vec<Compound>, ReqError> {
if idents.is_empty() {
return Ok(Vec::new());
}
let ids: Vec<String> = idents.iter().map(|i| i.to_string()).collect();
let url = format!("{API_URL}/compounds/?chebi_ids={}", ids.join(","));
let parsed: HashMap<String, CompoundEntry> = serde_json::from_str(&get(&url)?)?;
Ok(parsed.into_values().filter_map(|e| e.data).collect())
}
pub fn properties(ident: u32) -> Result<Properties, ReqError> {
Ok((&load_compound(ident)?).into())
}
pub fn get_smiles(ident: u32) -> Result<String, ReqError> {
load_compound(ident)?
.default_structure
.and_then(|s| s.smiles)
.ok_or(ReqError::Deserialize)
}
fn mol_url(id: u32) -> String {
format!("{API_URL}/molfile/{id}/")
}
pub fn load_mol(id: u32) -> Result<String, ReqError> {
let result = get(&mol_url(id))?;
if result.trim().is_empty() {
return Err(ReqError::Deserialize);
}
Ok(result)
}
pub fn load_sdf(ident: u32) -> Result<String, ReqError> {
let mol = load_mol(ident)?;
Ok(format!("{}\n$$$$\n", mol.trim_end()))
}
pub fn search(term: &str, page: u32, size: u32) -> Result<SearchResults, ReqError> {
let term_sanitized = url::form_urlencoded::byte_serialize(term.as_bytes()).collect::<String>();
let url = format!("{API_URL}/es_search/?term={term_sanitized}&page={page}&size={size}");
Ok(serde_json::from_str(&get(&url)?)?)
}
pub fn find_ids_from_search(term: &str) -> Result<Vec<u32>, ReqError> {
Ok(search(term, 1, 15)?
.results
.into_iter()
.map(|r| r.id)
.collect())
}
pub fn structure_search(
smiles: &str,
search_type: StructureSearchType,
similarity: Option<f32>,
three_star_only: bool,
page: u32,
size: u32,
) -> Result<SearchResults, ReqError> {
let smiles_sanitized =
url::form_urlencoded::byte_serialize(smiles.as_bytes()).collect::<String>();
let mut url = format!(
"{API_URL}/structure_search/?smiles={smiles_sanitized}&search_type={search_type}\
&three_star_only={three_star_only}&page={page}&size={size}"
);
if let Some(s) = similarity {
url += &format!("&similarity={s}");
}
Ok(serde_json::from_str(&get(&url)?)?)
}
pub fn find_similar_mols(smiles: &str, similarity: f32) -> Result<Vec<u32>, ReqError> {
Ok(structure_search(
smiles,
StructureSearchType::Similarity,
Some(similarity),
true,
1,
15,
)?
.results
.into_iter()
.map(|r| r.id)
.collect())
}
#[derive(Debug, Deserialize)]
struct OntologyResp {
ontology_relations: OntologyRelations,
}
pub fn load_parents(id: u32) -> Result<Vec<OntologyRelation>, ReqError> {
let url = format!("{API_URL}/ontology/parents/CHEBI:{id}/");
let parsed: OntologyResp = serde_json::from_str(&get(&url)?)?;
Ok(parsed.ontology_relations.outgoing_relations)
}
pub fn load_children(id: u32) -> Result<Vec<OntologyRelation>, ReqError> {
let url = format!("{API_URL}/ontology/children/CHEBI:{id}/");
let parsed: OntologyResp = serde_json::from_str(&get(&url)?)?;
Ok(parsed.ontology_relations.incoming_relations)
}