#[cfg(feature = "chematic-normalization")]
mod chematic_impl;
#[cfg(feature = "chematic-normalization")]
pub use chematic_impl::ChematicNormalizer;
use crate::enrichment::ChemicalIdentityCandidate;
pub(crate) fn preferred_smiles(candidate: &ChemicalIdentityCandidate) -> Option<&str> {
candidate
.smiles
.as_deref()
.or(candidate.connectivity_smiles.as_deref())
}
pub trait ChemicalNormalizer {
fn normalize(&self, candidate: &ChemicalIdentityCandidate) -> ChemicalNormalizationResult;
}
pub struct UnavailableNormalizer;
impl ChemicalNormalizer for UnavailableNormalizer {
fn normalize(&self, candidate: &ChemicalIdentityCandidate) -> ChemicalNormalizationResult {
match preferred_smiles(candidate) {
None => ChemicalNormalizationResult {
original_smiles: None,
canonical_smiles: None,
status: NormalizationStatus::MissingStructure,
issues: vec![],
calculated: CalculatedIdentityProperties::default(),
screening_alerts: vec![],
},
Some(smiles) => ChemicalNormalizationResult {
original_smiles: Some(smiles.to_string()),
canonical_smiles: None,
status: NormalizationStatus::ReviewRequired,
issues: vec![],
calculated: CalculatedIdentityProperties::default(),
screening_alerts: vec![],
},
}
}
}
#[derive(Debug, Clone)]
pub struct ChemicalNormalizationResult {
pub original_smiles: Option<String>,
pub canonical_smiles: Option<String>,
pub status: NormalizationStatus,
pub issues: Vec<NormalizationIssue>,
pub calculated: CalculatedIdentityProperties,
pub screening_alerts: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NormalizationStatus {
Normalized,
MissingStructure,
InvalidStructure,
Ambiguous,
ReviewRequired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NormalizationIssue {
InvalidSmiles,
MultipleResolverCandidates,
MultiFragmentStructure,
FormulaMismatch,
ChargeOrSaltPresent,
UnsupportedPolymerOrMixture,
StereochemistryNotFullyPreserved,
}
#[derive(Debug, Clone, Default)]
pub struct CalculatedIdentityProperties {
pub molecular_formula: Option<String>,
pub molecular_weight: Option<f64>,
pub formal_charge_sum: Option<i32>,
pub has_multiple_fragments: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
fn candidate(smiles: Option<&str>) -> ChemicalIdentityCandidate {
ChemicalIdentityCandidate {
cas: "7732-18-5".into(),
pubchem_cid: Some(962),
iupac_name: Some("oxidane".into()),
molecular_formula: Some("H2O".into()),
smiles: smiles.map(str::to_string),
connectivity_smiles: None,
inchi_key: None,
}
}
#[test]
fn unavailable_normalizer_reports_missing_structure_when_no_smiles() {
let result = UnavailableNormalizer.normalize(&candidate(None));
assert_eq!(result.status, NormalizationStatus::MissingStructure);
}
#[test]
fn unavailable_normalizer_reports_review_required_when_smiles_present() {
let result = UnavailableNormalizer.normalize(&candidate(Some("O")));
assert_eq!(result.status, NormalizationStatus::ReviewRequired);
assert!(result.canonical_smiles.is_none());
}
#[test]
fn full_smiles_is_preferred_over_connectivity_smiles() {
let mut c = candidate(Some("CCO"));
c.connectivity_smiles = Some("CO".into());
assert_eq!(preferred_smiles(&c), Some("CCO"));
}
#[test]
fn connectivity_smiles_is_used_only_as_fallback() {
let mut c = candidate(None);
c.connectivity_smiles = Some("CCO".into());
assert_eq!(preferred_smiles(&c), Some("CCO"));
let result = UnavailableNormalizer.normalize(&c);
assert_eq!(result.status, NormalizationStatus::ReviewRequired);
assert_eq!(result.original_smiles.as_deref(), Some("CCO"));
}
#[cfg(not(feature = "chematic-normalization"))]
#[test]
fn crate_compiles_without_chematic_normalization_feature() {
}
}