use core::str::FromStr;
use super::{Smiles, SmilesAtomPolicy, WildcardSmiles};
use crate::{
errors::SmilesErrorWithSpan,
parser::smiles_parser::{parse_smiles, parse_smiles_with_policy, parse_wildcard_smiles},
};
impl Smiles {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, SmilesErrorWithSpan> {
parse_smiles(s)
}
}
impl<AtomPolicy: SmilesAtomPolicy> FromStr for Smiles<AtomPolicy> {
type Err = SmilesErrorWithSpan;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_smiles_with_policy(s)
}
}
impl WildcardSmiles {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, SmilesErrorWithSpan> {
parse_wildcard_smiles(s).map(Self::from_inner)
}
}
impl FromStr for WildcardSmiles {
type Err = SmilesErrorWithSpan;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str(s)
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use elements_rs::Element;
use crate::{
atom::{Atom, atom_symbol::AtomSymbol},
bond::{
Bond,
bond_edge::{bond_edge, bond_edge_ring_num_val},
ring_num::RingNum,
},
smiles::{Smiles, WildcardSmiles},
};
#[test]
fn parse_benzene_with_ring_nums() {
let smiles = Smiles::from_str("C1=CC=CC=C1")
.unwrap_or_else(|e| panic!("Failed to tokenize:\n{}", e.render("C1=CC=CC=C1")));
let expected_nodes = [
Atom::new_organic_subset(AtomSymbol::Element(Element::C), false),
Atom::new_organic_subset(AtomSymbol::Element(Element::C), false),
Atom::new_organic_subset(AtomSymbol::Element(Element::C), false),
Atom::new_organic_subset(AtomSymbol::Element(Element::C), false),
Atom::new_organic_subset(AtomSymbol::Element(Element::C), false),
Atom::new_organic_subset(AtomSymbol::Element(Element::C), false),
];
let expected_ring_edge = bond_edge(0, 5, Bond::Single, Some(RingNum::try_new(1).unwrap()));
assert_eq!(smiles.nodes()[0], expected_nodes[0]);
assert_eq!(smiles.nodes()[5], expected_nodes[5]);
assert_eq!(smiles.edge_for_node_pair((0, 5)), Some(expected_ring_edge));
assert_eq!(smiles.edge_for_node_pair((0, 5)).and_then(bond_edge_ring_num_val), Some(1));
assert_eq!(smiles.node_by_id(0), Some(&expected_nodes[0]));
assert_eq!(smiles.node_by_id(5), Some(&expected_nodes[5]));
}
#[test]
fn from_str_propagates_token_iter_error() {
let err = Smiles::from_str("Ac").expect_err("expected tokenization to fail");
assert_eq!(
err.smiles_error(),
crate::errors::SmilesError::InvalidUnbracketedAtom(AtomSymbol::Element(Element::Ac))
);
assert_eq!(err.start(), 0);
assert_eq!(err.end(), 2);
assert_eq!(err.to_string(), "Invalid unbracketed atom: Ac at 0..2");
}
#[test]
fn from_str_propagates_parser_error() {
let err = Smiles::from_str("C(").expect_err("expected parsing to fail");
assert_eq!(err.smiles_error(), crate::errors::SmilesError::UnclosedBranch);
assert_eq!(err.start(), 1);
assert_eq!(err.end(), 2);
assert_eq!(err.to_string(), "Branch not closed at 1..2");
}
#[test]
fn empty_input_is_not_a_valid_smiles() {
let err = Smiles::from_str("").expect_err("empty input should not parse");
assert_eq!(err.smiles_error(), crate::errors::SmilesError::MissingElement);
assert_eq!((err.start(), err.end()), (0, 0));
let err = WildcardSmiles::from_str("").expect_err("empty input should not parse");
assert_eq!(err.smiles_error(), crate::errors::SmilesError::MissingElement);
assert_eq!((err.start(), err.end()), (0, 0));
}
#[test]
fn strict_smiles_rejects_wildcards() {
for (source, span) in [
("*", (0, 1)),
("C(*)C", (2, 3)),
("[*]", (0, 3)),
("[13*]", (0, 5)),
("[*:1]", (0, 5)),
] {
let err = Smiles::from_str(source).expect_err("strict SMILES should reject wildcard");
assert_eq!(err.smiles_error(), crate::errors::SmilesError::WildcardAtomNotAllowed);
assert_eq!((err.start(), err.end()), span);
}
}
#[test]
fn wildcard_smiles_accepts_wildcards() {
for source in ["*", "C(*)C", "[*]", "[13*]", "[*:1]"] {
WildcardSmiles::from_str(source)
.unwrap_or_else(|error| panic!("failed to parse {source}: {error}"));
}
}
#[test]
fn concrete_isotopes_are_validated_while_parsing() {
let err = Smiles::from_str("[999C]").expect_err("unknown carbon isotope should be invalid");
assert_eq!(err.smiles_error(), crate::errors::SmilesError::InvalidIsotope);
assert_eq!((err.start(), err.end()), (0, 6));
let err = WildcardSmiles::from_str("[999C]")
.expect_err("wildcard-capable parsing should still validate concrete isotopes");
assert_eq!(err.smiles_error(), crate::errors::SmilesError::InvalidIsotope);
assert_eq!((err.start(), err.end()), (0, 6));
}
}