use std::{
collections::HashMap,
fmt::{Display, Formatter},
};
use serde::Deserialize;
use crate::{ReqError, make_agent};
const BASE_COMPOUND_URL: &str = "https://pubchem.ncbi.nlm.nih.gov/compound";
const BASE_PUG_URL: &str = "https://pubchem.ncbi.nlm.nih.gov/rest/pug";
const PROTEIN_LOOKUP_URL: &str =
"https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/structure/compound";
#[allow(unused)]
#[derive(Clone, Debug, Deserialize)]
pub struct Taxonomy {
#[serde(rename = "ID")]
id: u32,
#[serde(rename = "Name")]
name: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ProteinStructure {
#[serde(rename = "MMDB_ID")]
pub mmdb_id: u32,
#[serde(rename = "PDB_ID")]
pub pdb_id: String,
#[serde(rename = "URL")]
pub url: String,
#[serde(rename = "ImageURL")]
pub image_url: String,
#[serde(rename = "Description")]
pub description: String,
#[serde(rename = "Taxonomy")]
pub taxonomy: Taxonomy,
}
#[derive(Deserialize)]
struct InnerStructure {
#[serde(rename = "Structures")]
structures: Vec<ProteinStructure>,
}
#[derive(Deserialize)]
struct ProteinStructureResponse {
#[serde(rename = "Structure")]
structure: InnerStructure,
}
#[derive(Clone, Copy, PartialEq)]
pub enum Domain {
Substance,
Compound,
Assay,
Gene,
Protein,
Pathway,
Taxonomy,
Cell,
}
impl Display for Domain {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Substance => "substance",
Self::Compound => "compound",
Self::Assay => "assay",
Self::Gene => "gene",
Self::Protein => "protein",
Self::Pathway => "pathway",
Self::Taxonomy => "taxonomy",
Self::Cell => "cell",
};
write!(f, "{v}")
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum StructureSearchCat {
Substructure,
Superstructure,
Similarity,
Identity,
}
impl Display for StructureSearchCat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Substructure => "substructure",
Self::Superstructure => "superstructure",
Self::Similarity => "similarity",
Self::Identity => "identity",
};
write!(f, "{v}")
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum FastSearchCat {
FastIdentity,
FastSimilarity2d,
FastSimilarity3d,
FastSubstructure,
FastSuperstructure,
}
impl Display for FastSearchCat {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::FastIdentity => "fastidentity",
Self::FastSimilarity2d => "fastsimilarity_2d",
Self::FastSimilarity3d => "fastsimilarity_3d",
Self::FastSubstructure => "fastsubstructure",
Self::FastSuperstructure => "fastsuperstructure",
};
write!(f, "{v}")
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum StructureSearchNamespace {
Smiles,
Inchi,
InchiKey,
Sdf,
Cid,
}
impl Display for StructureSearchNamespace {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Smiles => "smiles",
Self::Inchi => "inchi",
Self::InchiKey => "inchikey",
Self::Sdf => "sdf",
Self::Cid => "cid",
};
write!(f, "{v}")
}
}
#[derive(Clone, PartialEq)]
pub enum NamespaceCompound {
Cid,
Name,
Smiles,
Inchi,
Sdf,
Inchikey,
Formula,
StructureSearch((StructureSearchCat, StructureSearchNamespace)),
ListKey,
FastSearch((FastSearchCat, StructureSearchNamespace)),
}
impl Display for NamespaceCompound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Cid => "cid",
Self::Name => "name",
Self::Smiles => "smiles",
Self::Inchi => "inchi",
Self::Sdf => "sdf",
Self::Inchikey => "inchikey",
Self::Formula => "formula",
Self::StructureSearch((search_cat, search_namespace)) => {
&format!("{search_cat}/{search_namespace}")
}
Self::ListKey => "listkey",
Self::FastSearch((search_cat, search_namespace)) => {
&format!("{search_cat}/{search_namespace}")
}
};
write!(f, "{v}")
}
}
#[derive(Clone, PartialEq)]
pub enum NamespaceSubstance {
Sid,
SourceId(String),
SourceAll(String),
Name,
ListKey,
}
impl Display for NamespaceSubstance {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Sid => "sid",
Self::SourceId(v) => &format!("sourceid/{v}"),
Self::SourceAll(v) => &format!("sourceall/{v}"),
Self::Name => "name",
Self::ListKey => "listkey",
};
write!(f, "{v}")
}
}
#[derive(Clone, PartialEq)]
pub enum Namespace {
Compound(NamespaceCompound),
Substance(NamespaceSubstance),
}
impl Display for Namespace {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Compound(v) => v.to_string(),
Self::Substance(v) => v.to_string(),
};
write!(f, "{v}")
}
}
#[derive(Clone, PartialEq)]
pub enum OpSpecCompound {
Record,
Property(Vec<String>),
Synonyms,
Sids,
Cids,
Aids,
AssaySummary,
Classification,
Xrefs,
Description,
Conformers,
}
impl Display for OpSpecCompound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Record => "record",
Self::Property(v) => &format!("property/{}", v.join(",")),
Self::Synonyms => "synonyms",
Self::Sids => "sids",
Self::Cids => "cids",
Self::Aids => "aids",
Self::AssaySummary => "assaysummary",
Self::Classification => "classification",
Self::Xrefs => "xrefs",
Self::Description => "description",
Self::Conformers => "conformers",
};
write!(f, "{v}")
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum OpSpecSubstance {
Record,
Synonyms,
Sids,
Cids,
Aids,
AssaySummary,
Classification,
Xrefs,
Description,
}
impl Display for OpSpecSubstance {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Record => "record",
Self::Synonyms => "synonyms",
Self::Sids => "sids",
Self::Cids => "cids",
Self::Aids => "aids",
Self::AssaySummary => "assaysummary",
Self::Classification => "classification",
Self::Xrefs => "xrefs",
Self::Description => "description",
};
write!(f, "{v}")
}
}
#[derive(Clone, PartialEq)]
pub enum OperationSpecification {
Substance(OpSpecSubstance),
Compound(OpSpecCompound),
}
impl Display for OperationSpecification {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let v = match self {
Self::Substance(v) => v.to_string(),
Self::Compound(v) => v.to_string(),
};
write!(f, "{v}")
}
}
pub fn url_api_query(
domain: Domain,
namespace: Namespace,
identifiers: &[String],
op_spec: OperationSpecification,
) -> Result<String, ReqError> {
let idents = identifiers.join(","); let url = format!("{BASE_PUG_URL}/{domain}/{namespace}/{idents}/{op_spec}/JSON");
let agent = make_agent();
Ok(agent.get(url).call()?.body_mut().read_to_string()?)
}
#[derive(Clone, Debug, Deserialize)]
struct SimilarMolsCidResp {
#[serde(rename = "CID")]
pub cid: Vec<u32>,
}
#[derive(Clone, Debug, Deserialize)]
struct SimilarMolsResp {
#[serde(rename = "IdentifierList")]
pub identifier_list: SimilarMolsCidResp,
}
pub fn find_similar_mols(cid: u32) -> Result<Vec<u32>, ReqError> {
let resp = url_api_query(
Domain::Compound,
Namespace::Compound(NamespaceCompound::FastSearch((
FastSearchCat::FastSimilarity3d,
StructureSearchNamespace::Cid,
))),
&[cid.to_string()],
OperationSpecification::Compound(OpSpecCompound::Cids),
)?;
let parsed: SimilarMolsResp = serde_json::from_str(&resp)?;
Ok(parsed.identifier_list.cid)
}
pub fn open_overview(id: u32) {
if let Err(e) = webbrowser::open(&format!("{BASE_COMPOUND_URL}/{id}")) {
eprintln!("Failed to open the web browser: {:?}", e);
}
}
pub fn load_associated_structures(cid: u32) -> Result<Vec<ProteinStructure>, ReqError> {
let url = format!("{PROTEIN_LOOKUP_URL}/{cid}/JSON");
let agent = make_agent();
let resp = agent.get(url).call()?.body_mut().read_to_string()?;
let parsed: ProteinStructureResponse = serde_json::from_str(&resp)?;
Ok(parsed.structure.structures)
}
fn sdf_url(id_type: StructureSearchNamespace, id: &str) -> String {
format!("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/{id_type}/{id}/SDF?record_type=3d",)
}
pub fn load_sdf(id_type: StructureSearchNamespace, id: &str) -> Result<String, ReqError> {
let agent = make_agent();
Ok(agent
.get(sdf_url(id_type, id))
.call()?
.body_mut()
.read_to_string()?)
}
pub fn get_smiles_chem_name(name: &str) -> Result<String, ReqError> {
let agent = make_agent();
let url = format!("https://cactus.nci.nih.gov/chemical/structure/{name}/smiles");
let mut resp = agent.get(url).call()?;
if resp.status() != 200 {
return Err(ReqError::Http);
}
Ok(resp.body_mut().read_to_string()?)
}
fn pubchem_smiles_url(cid: u32) -> String {
format!("{BASE_PUG_URL}/compound/cid/{cid}/property/IsomericSMILES/TXT")
}
pub fn get_smiles(cid: u32) -> Result<String, ReqError> {
let agent = make_agent();
let url = pubchem_smiles_url(cid);
let mut resp = agent.get(url).call()?;
let s = resp.body_mut().read_to_string()?;
Ok(s.trim().to_string())
}
fn properties_url(id_type: StructureSearchNamespace, id: &str) -> String {
let id_santizied = id.replace("#", "%23");
format!(
"{BASE_PUG_URL}/compound/{id_type}/{id_santizied}/property/TPSA,XLogP,Complexity,Volume3D,SMILES,InChI,\
InChIKey,IUPACName,Title/JSON"
)
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "encode", derive(bincode::Encode, bincode::Decode))]
pub struct Properties {
pub log_p: f32,
pub total_polar_surface_area: f32,
pub complexity: f32,
pub volume: f32,
pub cid: u32,
pub smiles: String,
pub inchi: String,
pub inchi_key: String,
pub iupac_name: String,
pub title: String,
}
#[derive(Debug, Deserialize)]
struct PropertyTableResp {
#[serde(rename = "PropertyTable")]
property_table: PropertyTableInner,
}
#[allow(unused)]
#[derive(Debug, Deserialize)]
struct CompoundProps {
#[serde(rename = "CID")]
cid: u32,
#[serde(rename = "TPSA")]
tpsa: f32,
#[serde(rename = "XLogP")]
xlogp: f32,
#[serde(rename = "Complexity")]
complexity: f32,
#[serde(rename = "Volume3D")]
volume: f32,
#[serde(rename = "SMILES")]
smiles: String,
#[serde(rename = "InChI")]
inchi: String,
#[serde(rename = "InChIKey")]
inchi_key: String,
#[serde(rename = "IUPACName")]
iupac_name: String,
#[serde(rename = "Title")]
title: String,
}
#[derive(Debug, Deserialize)]
struct PropertyTableInner {
#[serde(rename = "Properties")]
properties: Vec<CompoundProps>,
}
pub fn properties(id_type: StructureSearchNamespace, id: &str) -> Result<Properties, ReqError> {
let agent = make_agent();
let url = properties_url(id_type, id);
let mut resp = agent.get(url).call()?;
let body = resp.body_mut().read_to_string()?;
let parsed: PropertyTableResp = serde_json::from_str(&body)?;
let row = parsed
.property_table
.properties
.into_iter()
.next()
.ok_or(ReqError::Deserialize)?;
Ok(Properties {
log_p: row.xlogp,
total_polar_surface_area: row.tpsa,
complexity: row.complexity,
volume: row.volume,
cid: row.cid,
smiles: row.smiles,
inchi: row.inchi,
inchi_key: row.inchi_key,
iupac_name: row.iupac_name,
title: row.title,
})
}
#[derive(Debug, Deserialize)]
struct TitleRow {
#[serde(rename = "CID")]
cid: u32,
#[serde(rename = "Title")]
title: Option<String>,
}
#[derive(Debug, Deserialize)]
struct TitleTableInner {
#[serde(rename = "Properties")]
properties: Vec<TitleRow>,
}
#[derive(Debug, Deserialize)]
struct TitleTableResp {
#[serde(rename = "PropertyTable")]
property_table: TitleTableInner,
}
pub fn titles_for_cids(cids: &[u32]) -> Result<HashMap<u32, String>, ReqError> {
if cids.is_empty() {
return Ok(HashMap::new());
}
let idents: Vec<String> = cids.iter().map(|c| c.to_string()).collect();
let data = url_api_query(
Domain::Compound,
Namespace::Compound(NamespaceCompound::Cid),
&idents,
OperationSpecification::Compound(OpSpecCompound::Property(vec!["Title".to_string()])),
)?;
let parsed: TitleTableResp = serde_json::from_str(&data)?;
Ok(parsed
.property_table
.properties
.into_iter()
.filter_map(|row| row.title.map(|t| (row.cid, t)))
.collect())
}
pub fn properties_from_pdbe_id(pdb_id: &str) -> Result<Properties, ReqError> {
let smiles = get_smiles_chem_name(pdb_id)?;
properties(StructureSearchNamespace::Smiles, &smiles)
}
pub fn get_cid_from_pdbe_id(pdb_id: &str) -> Result<(u32, String), ReqError> {
let smiles = get_smiles_chem_name(pdb_id)?;
let cids = find_cids_from_search(&smiles, true)?;
Ok((cids[0], smiles))
}
#[allow(unused)]
#[derive(Clone, Debug, Deserialize)]
struct RecordIdB {
cid: u32,
}
#[allow(unused)]
#[derive(Clone, Debug, Deserialize)]
struct RecordIdA {
id: RecordIdB,
}
#[allow(unused)]
#[derive(Clone, Debug, Deserialize)]
struct PcCompound {
id: RecordIdA,
}
#[allow(unused)]
#[derive(Clone, Debug, Deserialize)]
struct RecordResp {
#[serde(rename = "PC_Compounds")]
pc_compounds: Vec<PcCompound>,
}
pub fn find_cids_from_search(name: &str, smiles: bool) -> Result<Vec<u32>, ReqError> {
let domain = Domain::Compound;
let nsc = if smiles {
NamespaceCompound::Smiles
} else {
NamespaceCompound::Name
};
let namespace = Namespace::Compound(nsc);
let op_spec = OperationSpecification::Compound(OpSpecCompound::Record);
let data = url_api_query(domain, namespace, &[name.to_string()], op_spec)?;
let result: RecordResp = serde_json::from_str(&data)?;
Ok(result.pc_compounds.iter().map(|p| p.id.id.cid).collect())
}