use std::collections::HashMap;
use std::fmt;
use crate::query::{AtomPrimitive, AtomQuery, BondPrimitive, BondQuery, QueryMolecule};
#[derive(Debug, Clone, PartialEq)]
pub enum SmartsError {
UnexpectedEnd,
UnexpectedChar(char, usize),
UnclosedBracket(usize),
UnclosedBranch(usize),
InvalidRingClosure(u8),
RecursionDepthExceeded,
}
impl fmt::Display for SmartsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SmartsError::UnexpectedEnd => write!(f, "unexpected end of SMARTS string"),
SmartsError::UnexpectedChar(ch, pos) => {
write!(f, "unexpected character {:?} at position {}", ch, pos)
}
SmartsError::UnclosedBracket(pos) => {
write!(f, "unclosed '[' bracket at position {}", pos)
}
SmartsError::UnclosedBranch(pos) => {
write!(f, "unclosed '(' branch at position {}", pos)
}
SmartsError::InvalidRingClosure(num) => {
write!(f, "invalid ring closure number: {}", num)
}
SmartsError::RecursionDepthExceeded => {
write!(f, "recursive SMARTS nesting depth exceeded safety limit")
}
}
}
}
impl std::error::Error for SmartsError {}
const DEFAULT_MAX_RECURSIVE_SMARTS_DEPTH: usize = 8;
const ABSOLUTE_MAX_RECURSIVE_SMARTS_DEPTH: usize = 16;
#[derive(Debug, Clone)]
pub struct SmartsParserConfig {
pub max_recursion_depth: usize,
}
impl Default for SmartsParserConfig {
fn default() -> Self {
Self {
max_recursion_depth: DEFAULT_MAX_RECURSIVE_SMARTS_DEPTH,
}
}
}
pub fn parse_smarts(smarts: &str) -> Result<QueryMolecule, SmartsError> {
parse_smarts_with_config(smarts, &SmartsParserConfig::default())
}
pub fn parse_smarts_with_config(
smarts: &str,
config: &SmartsParserConfig,
) -> Result<QueryMolecule, SmartsError> {
let max_depth = config
.max_recursion_depth
.clamp(1, ABSOLUTE_MAX_RECURSIVE_SMARTS_DEPTH);
let mut parser = Parser {
src: smarts.as_bytes(),
pos: 0,
recursion_depth: 0,
max_recursion_depth: max_depth,
};
parser.parse()
}
struct Parser<'a> {
src: &'a [u8],
pos: usize,
recursion_depth: usize,
max_recursion_depth: usize,
}
impl<'a> Parser<'a> {
#[inline]
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
#[inline]
fn advance(&mut self) -> Option<u8> {
let b = self.src.get(self.pos).copied();
if b.is_some() {
self.pos += 1;
}
b
}
fn parse(&mut self) -> Result<QueryMolecule, SmartsError> {
let mut mol = QueryMolecule::new();
let mut open_rings: HashMap<u8, (usize, Option<BondQuery>)> = HashMap::new();
self.parse_chain(&mut mol, None, None, &mut open_rings)?;
if let Some((&num, _)) = open_rings.iter().next() {
return Err(SmartsError::InvalidRingClosure(num));
}
if let Some(c) = self.peek()
&& c != b' '
&& c != b'\t'
&& c != b'\n'
&& c != b'\r'
{
return Err(SmartsError::UnexpectedChar(c as char, self.pos));
}
Ok(mol)
}
fn parse_chain(
&mut self,
mol: &mut QueryMolecule,
attach_to: Option<usize>,
attach_bond: Option<BondQuery>,
open_rings: &mut HashMap<u8, (usize, Option<BondQuery>)>,
) -> Result<Option<usize>, SmartsError> {
let first_atom = match self.try_parse_atom()? {
Some(a) => a,
None => return Ok(attach_to),
};
let first_idx = mol.add_atom(first_atom);
if let Some(prev) = attach_to {
let bond = attach_bond.unwrap_or(BondQuery::Any);
mol.add_bond(prev, first_idx, bond);
}
let mut current = first_idx;
loop {
match self.peek() {
Some(b'(') => {
let branch_start = self.pos;
self.advance(); let branch_bond = self.try_parse_bond();
self.parse_chain(mol, Some(current), branch_bond, open_rings)?;
match self.peek() {
Some(b')') => {
self.advance(); }
_ => return Err(SmartsError::UnclosedBranch(branch_start)),
}
}
Some(b'0'..=b'9') | Some(b'%') => {
let (ring_num, ring_bond) = self.parse_ring_closure_num(None)?;
self.handle_ring_closure(mol, current, ring_num, ring_bond, open_rings)?;
}
None | Some(b')') => break,
_ => {
let pending_bond = self.try_parse_bond();
match self.peek() {
Some(b'0'..=b'9') | Some(b'%') => {
let (ring_num, ring_bond) =
self.parse_ring_closure_num(pending_bond)?;
self.handle_ring_closure(
mol, current, ring_num, ring_bond, open_rings,
)?;
}
Some(b'(') => {
let branch_start = self.pos;
self.advance(); self.parse_chain(mol, Some(current), pending_bond, open_rings)?;
match self.peek() {
Some(b')') => {
self.advance();
}
_ => return Err(SmartsError::UnclosedBranch(branch_start)),
}
}
None | Some(b')') => {
if pending_bond.is_some() {
return Err(SmartsError::UnexpectedEnd);
}
break;
}
_ => match self.try_parse_atom()? {
Some(next_atom) => {
let next_idx = mol.add_atom(next_atom);
let bond = pending_bond.unwrap_or(BondQuery::Any);
mol.add_bond(current, next_idx, bond);
current = next_idx;
}
None => {
if pending_bond.is_some() {
return Err(SmartsError::UnexpectedEnd);
}
break;
}
},
}
}
}
}
Ok(Some(current))
}
fn handle_ring_closure(
&mut self,
mol: &mut QueryMolecule,
current: usize,
ring_num: u8,
ring_bond: Option<BondQuery>,
open_rings: &mut HashMap<u8, (usize, Option<BondQuery>)>,
) -> Result<(), SmartsError> {
if let Some((open_atom, open_bond)) = open_rings.remove(&ring_num) {
let bond = match (open_bond, ring_bond) {
(Some(a), Some(b)) if a == b => a,
(Some(_), Some(_)) => {
BondQuery::Any
}
(Some(b), None) | (None, Some(b)) => b,
(None, None) => BondQuery::Any,
};
mol.add_bond(open_atom, current, bond);
} else {
open_rings.insert(ring_num, (current, ring_bond));
}
Ok(())
}
fn parse_ring_closure_num(
&mut self,
prefix_bond: Option<BondQuery>,
) -> Result<(u8, Option<BondQuery>), SmartsError> {
let ring_num = if self.peek() == Some(b'%') {
self.advance(); let tens = self
.advance()
.filter(|c| c.is_ascii_digit())
.ok_or(SmartsError::UnexpectedEnd)?
- b'0';
let units = self
.advance()
.filter(|c| c.is_ascii_digit())
.ok_or(SmartsError::UnexpectedEnd)?
- b'0';
tens * 10 + units
} else {
self.advance()
.expect("ring closure digit guaranteed by caller peek")
- b'0'
};
Ok((ring_num, prefix_bond))
}
fn try_parse_bond(&mut self) -> Option<BondQuery> {
let first = self.try_parse_bond_factor()?;
Some(self.parse_bond_or_tail(first))
}
fn try_parse_bond_factor(&mut self) -> Option<BondQuery> {
match self.peek()? {
b'!' => {
if self
.src
.get(self.pos + 1)
.copied()
.map(Self::is_bond_token)
.unwrap_or(false)
{
self.advance(); let prim = self.consume_bond_prim().unwrap();
Some(BondQuery::Not(Box::new(BondQuery::Primitive(prim))))
} else {
None
}
}
c if Self::is_bond_token(c) => {
let prim = self.consume_bond_prim().unwrap();
Some(BondQuery::Primitive(prim))
}
_ => None,
}
}
fn consume_bond_prim(&mut self) -> Option<BondPrimitive> {
let prim = match self.peek()? {
b'-' => BondPrimitive::Single,
b'=' => BondPrimitive::Double,
b'#' => BondPrimitive::Triple,
b':' => BondPrimitive::Aromatic,
b'~' => BondPrimitive::Any,
b'@' => BondPrimitive::Ring,
b'/' => BondPrimitive::Up,
b'\\' => BondPrimitive::Down,
_ => return None,
};
self.advance();
Some(prim)
}
#[inline]
fn is_bond_token(c: u8) -> bool {
matches!(c, b'-' | b'=' | b'#' | b':' | b'~' | b'@' | b'/' | b'\\')
}
fn parse_bond_or_tail(&mut self, left: BondQuery) -> BondQuery {
if self.peek() == Some(b',') {
let next = self.src.get(self.pos + 1).copied();
if next.map(Self::is_bond_token).unwrap_or(false) || next == Some(b'!') {
self.advance(); if let Some(right) = self.try_parse_bond_factor() {
let right = self.parse_bond_and_tail(right);
let or_expr = BondQuery::Or(Box::new(left), Box::new(right));
return self.parse_bond_or_tail(or_expr);
}
}
}
self.parse_bond_and_tail(left)
}
fn parse_bond_and_tail(&mut self, left: BondQuery) -> BondQuery {
if self.peek() == Some(b'&') {
self.advance(); }
if self.peek() == Some(b'!') {
let next = self.src.get(self.pos + 1).copied();
if next.map(Self::is_bond_token).unwrap_or(false)
&& let Some(right) = self.try_parse_bond_factor()
{
let and_expr = BondQuery::And(Box::new(left), Box::new(right));
return self.parse_bond_and_tail(and_expr);
}
}
left
}
fn try_parse_atom(&mut self) -> Result<Option<AtomQuery>, SmartsError> {
match self.peek() {
Some(b'[') => Ok(Some(self.parse_bracket_atom()?)),
Some(b'*') => {
self.advance();
Ok(Some(AtomQuery::Primitive(AtomPrimitive::Wildcard)))
}
Some(b'B') | Some(b'C') | Some(b'N') | Some(b'O') | Some(b'P') | Some(b'S')
| Some(b'F') | Some(b'I') => Ok(Some(self.parse_organic_atom(false)?)),
Some(b'b') | Some(b'c') | Some(b'n') | Some(b'o') | Some(b'p') | Some(b's') => {
Ok(Some(self.parse_organic_atom(true)?))
}
_ => Ok(None),
}
}
fn parse_organic_atom(&mut self, aromatic: bool) -> Result<AtomQuery, SmartsError> {
let pos = self.pos;
let first = self.advance().unwrap() as char;
let symbol = if !aromatic {
if first == 'C' && self.peek() == Some(b'l') {
self.advance();
"Cl".to_string()
} else if first == 'B' && self.peek() == Some(b'r') {
self.advance();
"Br".to_string()
} else {
first.to_string()
}
} else {
first.to_ascii_uppercase().to_string()
};
if chematic_core::Element::from_symbol(&symbol).is_none() {
return Err(SmartsError::UnexpectedChar(first, pos));
}
let sym_query = AtomQuery::Primitive(AtomPrimitive::Symbol(symbol));
let arom_query = AtomQuery::Primitive(AtomPrimitive::Aromatic(aromatic));
Ok(AtomQuery::And(Box::new(sym_query), Box::new(arom_query)))
}
fn parse_bracket_atom(&mut self) -> Result<AtomQuery, SmartsError> {
let bracket_pos = self.pos;
self.advance();
let expr = self.parse_expr()?;
match self.peek() {
Some(b']') => {
self.advance(); }
_ => return Err(SmartsError::UnclosedBracket(bracket_pos)),
}
Ok(expr)
}
fn parse_expr(&mut self) -> Result<AtomQuery, SmartsError> {
self.parse_low_and()
}
fn parse_low_and(&mut self) -> Result<AtomQuery, SmartsError> {
let mut left = self.parse_or()?;
while self.peek() == Some(b';') {
self.advance(); let right = self.parse_or()?;
left = AtomQuery::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_or(&mut self) -> Result<AtomQuery, SmartsError> {
let mut left = self.parse_high_and()?;
while self.peek() == Some(b',') {
self.advance(); let right = self.parse_high_and()?;
left = AtomQuery::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_high_and(&mut self) -> Result<AtomQuery, SmartsError> {
let mut left = self.parse_unary()?;
loop {
let explicit_and = self.peek() == Some(b'&');
if explicit_and {
self.advance(); }
if self.can_start_primitive() {
let right = self.parse_unary()?;
left = AtomQuery::And(Box::new(left), Box::new(right));
} else if explicit_and {
return Err(SmartsError::UnexpectedEnd);
} else {
break;
}
}
Ok(left)
}
fn can_start_primitive(&self) -> bool {
match self.peek() {
Some(b'!') => true,
Some(b'#') => true,
Some(b'a') | Some(b'A') => true,
Some(b'+') | Some(b'-') => true,
Some(b'H') => true,
Some(b'D') => true,
Some(b'r') => true,
Some(b'R') => true,
Some(b'*') => true,
Some(b'$') => true,
Some(b'v') | Some(b'x') | Some(b'^') => true,
Some(c) if c.is_ascii_digit() => true,
Some(b'@') => true,
Some(c) if c.is_ascii_uppercase() => true,
Some(c) if c.is_ascii_lowercase() => true,
_ => false,
}
}
fn parse_unary(&mut self) -> Result<AtomQuery, SmartsError> {
if self.peek() == Some(b'!') {
self.advance(); let inner = self.parse_unary()?;
return Ok(AtomQuery::Not(Box::new(inner)));
}
self.parse_primitive()
}
fn parse_primitive(&mut self) -> Result<AtomQuery, SmartsError> {
let pos = self.pos;
match self.peek() {
Some(b'*') => {
self.advance();
Ok(AtomQuery::Primitive(AtomPrimitive::Wildcard))
}
Some(b'$') => {
self.advance(); if self.peek() != Some(b'(') {
return Err(SmartsError::UnexpectedChar('$', pos));
}
self.advance(); let start = self.pos;
let mut depth = 1usize;
let mut end = start;
while end < self.src.len() {
match self.src[end] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
end += 1;
}
if depth != 0 {
return Err(SmartsError::UnexpectedEnd);
}
let inner_str = std::str::from_utf8(&self.src[start..end])
.map_err(|_| SmartsError::UnexpectedEnd)?;
if self.recursion_depth >= self.max_recursion_depth {
return Err(SmartsError::RecursionDepthExceeded);
}
let mut inner_parser = Parser {
src: inner_str.as_bytes(),
pos: 0,
recursion_depth: self.recursion_depth + 1,
max_recursion_depth: self.max_recursion_depth,
};
let inner_mol = inner_parser.parse()?;
self.pos = end + 1; Ok(AtomQuery::Primitive(AtomPrimitive::Recursive(Box::new(
inner_mol,
))))
}
Some(b'a') => {
self.advance();
Ok(AtomQuery::Primitive(AtomPrimitive::Aromatic(true)))
}
Some(b'A') => {
self.advance();
Ok(AtomQuery::Primitive(AtomPrimitive::Aromatic(false)))
}
Some(b'#') => {
self.advance(); let n = self.parse_digits_u8().ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::AtomicNum(n)))
}
Some(b'+') => {
self.advance(); let n = if self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
self.parse_single_digit().unwrap_or(1)
} else {
1
};
Ok(AtomQuery::Primitive(AtomPrimitive::Charge(n as i8)))
}
Some(b'-') => {
self.advance(); let n = if self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
self.parse_single_digit().unwrap_or(1)
} else {
1
};
Ok(AtomQuery::Primitive(AtomPrimitive::Charge(-(n as i8))))
}
Some(b'H') => {
self.advance(); let n = self.parse_single_digit().unwrap_or(1);
Ok(AtomQuery::Primitive(AtomPrimitive::HCount(n)))
}
Some(b'h') => {
self.advance(); let n = self.parse_single_digit().unwrap_or(1);
Ok(AtomQuery::Primitive(AtomPrimitive::ImplicitHCount(n)))
}
Some(b'D') => {
self.advance(); let n = self
.parse_single_digit()
.ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::Degree(n)))
}
Some(b'r') => {
self.advance(); let n = self
.parse_single_digit()
.ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::RingSize(n)))
}
Some(b'R') => {
self.advance(); if let Some(n) = self.parse_single_digit() {
Ok(AtomQuery::Primitive(AtomPrimitive::RingCount(n)))
} else {
Ok(AtomQuery::Primitive(AtomPrimitive::RingMembership(true)))
}
}
Some(b'v') => {
self.advance(); let n = self
.parse_single_digit()
.ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::Valence(n)))
}
Some(b'x') => {
self.advance(); let n = self
.parse_single_digit()
.ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::RingBondCount(n)))
}
Some(b'^') => {
self.advance(); let n = self
.parse_single_digit()
.ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::Hybridization(n)))
}
Some(b'X') => {
self.advance(); let n = self
.parse_single_digit()
.ok_or(SmartsError::UnexpectedEnd)?;
Ok(AtomQuery::Primitive(AtomPrimitive::TotalConnectivity(n)))
}
Some(c) if c.is_ascii_digit() => {
let mut mass: u16 = 0;
while let Some(d) = self.peek().filter(|b| b.is_ascii_digit()) {
self.advance();
mass = mass * 10 + (d - b'0') as u16;
}
Ok(AtomQuery::Primitive(AtomPrimitive::Isotope(mass)))
}
Some(b'@') => {
self.advance(); let kind = if self.peek() == Some(b'@') {
self.advance(); 2u8 } else {
1u8 };
Ok(AtomQuery::Primitive(AtomPrimitive::Chirality(kind)))
}
Some(c) if c.is_ascii_alphabetic() => self.parse_element_primitive(),
Some(c) => Err(SmartsError::UnexpectedChar(c as char, pos)),
None => Err(SmartsError::UnexpectedEnd),
}
}
fn parse_element_primitive(&mut self) -> Result<AtomQuery, SmartsError> {
let pos = self.pos;
let first = self.advance().unwrap() as char;
let _aromatic = first.is_ascii_lowercase();
let upper_first = first.to_ascii_uppercase();
if let Some(second) = self.peek()
&& second.is_ascii_lowercase()
{
let candidate = format!("{upper_first}{}", second as char);
if chematic_core::Element::from_symbol(&candidate).is_some() {
self.advance();
return Ok(AtomQuery::Primitive(AtomPrimitive::Symbol(candidate)));
}
}
let sym = upper_first.to_string();
if chematic_core::Element::from_symbol(&sym).is_some() {
Ok(AtomQuery::Primitive(AtomPrimitive::Symbol(sym)))
} else {
Err(SmartsError::UnexpectedChar(first, pos))
}
}
fn parse_digits_u8(&mut self) -> Option<u8> {
if !self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
return None;
}
let mut val: u16 = 0;
while let Some(d) = self.peek().filter(|c| c.is_ascii_digit()) {
self.advance();
val = val * 10 + (d - b'0') as u16;
}
if val > 255 { None } else { Some(val as u8) }
}
fn parse_single_digit(&mut self) -> Option<u8> {
match self.peek() {
Some(d) if d.is_ascii_digit() => {
self.advance();
Some(d - b'0')
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::{AtomPrimitive, AtomQuery, BondPrimitive, BondQuery};
#[test]
fn test_parse_aliphatic_c() {
let mol = parse_smarts("C").unwrap();
assert_eq!(mol.atoms.len(), 1);
assert_eq!(mol.bonds.len(), 0);
let expected = AtomQuery::And(
Box::new(AtomQuery::Primitive(AtomPrimitive::Symbol("C".to_string()))),
Box::new(AtomQuery::Primitive(AtomPrimitive::Aromatic(false))),
);
assert_eq!(mol.atoms[0].query, expected);
}
#[test]
fn test_parse_aromatic_c() {
let mol = parse_smarts("c").unwrap();
assert_eq!(mol.atoms.len(), 1);
let expected = AtomQuery::And(
Box::new(AtomQuery::Primitive(AtomPrimitive::Symbol("C".to_string()))),
Box::new(AtomQuery::Primitive(AtomPrimitive::Aromatic(true))),
);
assert_eq!(mol.atoms[0].query, expected);
}
#[test]
fn test_parse_atomic_num() {
let mol = parse_smarts("[#6]").unwrap();
assert_eq!(mol.atoms.len(), 1);
assert_eq!(
mol.atoms[0].query,
AtomQuery::Primitive(AtomPrimitive::AtomicNum(6))
);
}
#[test]
fn test_parse_not() {
let mol = parse_smarts("[!C]").unwrap();
assert_eq!(
mol.atoms[0].query,
AtomQuery::Not(Box::new(AtomQuery::Primitive(AtomPrimitive::Symbol(
"C".to_string()
))))
);
}
#[test]
fn test_parse_aromatic_primitive() {
let mol = parse_smarts("[a]").unwrap();
assert_eq!(
mol.atoms[0].query,
AtomQuery::Primitive(AtomPrimitive::Aromatic(true))
);
}
#[test]
fn test_parse_degree() {
let mol = parse_smarts("[D3]").unwrap();
assert_eq!(
mol.atoms[0].query,
AtomQuery::Primitive(AtomPrimitive::Degree(3))
);
}
#[test]
fn test_parse_ring_size() {
let mol = parse_smarts("[r5]").unwrap();
assert_eq!(
mol.atoms[0].query,
AtomQuery::Primitive(AtomPrimitive::RingSize(5))
);
}
#[test]
fn test_parse_hcount() {
let mol = parse_smarts("[H2]").unwrap();
assert_eq!(
mol.atoms[0].query,
AtomQuery::Primitive(AtomPrimitive::HCount(2))
);
}
#[test]
fn test_parse_cc_bond() {
let mol = parse_smarts("CC").unwrap();
assert_eq!(mol.atoms.len(), 2);
assert_eq!(mol.bonds.len(), 1);
assert_eq!(mol.bonds[0].query, BondQuery::Any);
}
#[test]
fn test_parse_double_bond() {
let mol = parse_smarts("C=C").unwrap();
assert_eq!(mol.atoms.len(), 2);
assert_eq!(mol.bonds.len(), 1);
assert_eq!(
mol.bonds[0].query,
BondQuery::Primitive(BondPrimitive::Double)
);
}
#[test]
fn test_parse_branch() {
let mol = parse_smarts("C(=O)O").unwrap();
assert_eq!(mol.atoms.len(), 3, "should have 3 atoms");
assert_eq!(mol.bonds.len(), 2, "should have 2 bonds");
assert_eq!(
mol.bonds[0].query,
BondQuery::Primitive(BondPrimitive::Double)
);
assert_eq!(mol.bonds[1].query, BondQuery::Any);
}
#[test]
fn test_parse_benzene_ring() {
let mol = parse_smarts("c1ccccc1").unwrap();
assert_eq!(mol.atoms.len(), 6, "benzene has 6 atoms");
assert_eq!(mol.bonds.len(), 6, "benzene has 6 bonds");
for atom in &mol.atoms {
let expected = AtomQuery::And(
Box::new(AtomQuery::Primitive(AtomPrimitive::Symbol("C".to_string()))),
Box::new(AtomQuery::Primitive(AtomPrimitive::Aromatic(true))),
);
assert_eq!(atom.query, expected);
}
}
#[test]
fn test_parse_with_custom_config_default_depth() {
let config = SmartsParserConfig::default();
assert_eq!(
config.max_recursion_depth,
DEFAULT_MAX_RECURSIVE_SMARTS_DEPTH
);
let mol = parse_smarts_with_config("c1ccccc1", &config).unwrap();
assert_eq!(mol.atoms.len(), 6);
}
#[test]
fn test_parse_with_custom_config_increased_depth() {
let config = SmartsParserConfig {
max_recursion_depth: 12,
};
let mol = parse_smarts_with_config("[C,N,O]", &config).unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_parse_with_custom_config_depth_clamped_high() {
let config = SmartsParserConfig {
max_recursion_depth: 100,
};
let mol = parse_smarts_with_config("C", &config).unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_parse_with_custom_config_depth_clamped_low() {
let config = SmartsParserConfig {
max_recursion_depth: 0,
};
let mol = parse_smarts_with_config("C", &config).unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_parse_with_default_config_equivalent() {
let pattern = "c1ccccc1";
let mol1 = parse_smarts(pattern).unwrap();
let mol2 = parse_smarts_with_config(pattern, &SmartsParserConfig::default()).unwrap();
assert_eq!(mol1.atoms.len(), mol2.atoms.len());
}
#[test]
fn test_config_depth_parameter_clamped_correctly() {
assert_eq!(
SmartsParserConfig {
max_recursion_depth: 0
}
.max_recursion_depth,
0
);
assert_eq!(
SmartsParserConfig {
max_recursion_depth: 16
}
.max_recursion_depth,
16
);
assert_eq!(
SmartsParserConfig {
max_recursion_depth: 100
}
.max_recursion_depth,
100
);
}
#[test]
fn test_operator_precedence_and_over_or() {
let mol = parse_smarts("[C&N,O]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_operator_precedence_not_highest() {
let mol = parse_smarts("[!C&N]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_operator_precedence_semicolon_lowest() {
let mol = parse_smarts("[C;N,O]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_complex_bracket_atom_all_primitives() {
let mol = parse_smarts("[#6;a;R;H1]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_long_chain_many_atoms() {
let mol = parse_smarts("CCCCCCCCCC").unwrap();
assert_eq!(mol.atoms.len(), 10);
assert_eq!(mol.bonds.len(), 9);
}
#[test]
fn test_multiple_rings_fused() {
let mol = parse_smarts("c1ccc2ccccc2c1").unwrap();
assert_eq!(mol.atoms.len(), 10);
}
#[test]
fn test_branching_from_multiple_atoms() {
let mol = parse_smarts("C(C)(C)C").unwrap();
assert_eq!(mol.atoms.len(), 4);
assert_eq!(mol.bonds.len(), 3);
}
#[test]
fn test_recursive_smarts_simple() {
let mol = parse_smarts("[$(C)]").unwrap();
assert_eq!(mol.atoms.len(), 1);
assert!(matches!(
mol.atoms[0].query,
AtomQuery::Primitive(AtomPrimitive::Recursive(_))
));
}
#[test]
fn test_recursive_smarts_with_operators() {
let mol = parse_smarts("[$([C&N])]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
fn nested_recursive_smarts(depth: usize) -> String {
let mut smarts = String::from("C");
for _ in 0..depth {
smarts = format!("[$({smarts})]");
}
smarts
}
#[test]
fn test_recursive_smarts_default_depth_boundary() {
let smarts = nested_recursive_smarts(DEFAULT_MAX_RECURSIVE_SMARTS_DEPTH);
let mol = parse_smarts(&smarts).expect("default recursive SMARTS depth should parse");
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_recursive_smarts_default_depth_rejects_too_deep_pattern() {
let smarts = nested_recursive_smarts(DEFAULT_MAX_RECURSIVE_SMARTS_DEPTH + 1);
assert!(matches!(
parse_smarts(&smarts),
Err(SmartsError::RecursionDepthExceeded)
));
}
#[test]
fn test_recursive_smarts_config_depth_is_clamped_for_safety() {
let low_config = SmartsParserConfig {
max_recursion_depth: 0,
};
let too_deep_for_low_config = nested_recursive_smarts(2);
assert!(matches!(
parse_smarts_with_config(&too_deep_for_low_config, &low_config),
Err(SmartsError::RecursionDepthExceeded)
));
let high_config = SmartsParserConfig {
max_recursion_depth: 100,
};
let absolute_boundary = nested_recursive_smarts(ABSOLUTE_MAX_RECURSIVE_SMARTS_DEPTH);
parse_smarts_with_config(&absolute_boundary, &high_config)
.expect("absolute recursive SMARTS depth boundary should parse");
let too_deep_for_absolute_limit =
nested_recursive_smarts(ABSOLUTE_MAX_RECURSIVE_SMARTS_DEPTH + 1);
assert!(matches!(
parse_smarts_with_config(&too_deep_for_absolute_limit, &high_config),
Err(SmartsError::RecursionDepthExceeded)
));
}
#[test]
fn test_malformed_recursive_smarts_return_errors() {
assert!(matches!(
parse_smarts("[$(C]"),
Err(SmartsError::UnexpectedEnd)
));
assert!(matches!(
parse_smarts("[$(C"),
Err(SmartsError::UnexpectedEnd)
));
assert!(matches!(
parse_smarts("[$(C))]"),
Err(SmartsError::UnclosedBracket(_) | SmartsError::UnexpectedChar(_, _))
));
}
#[test]
fn test_isotope_with_bracket_atom() {
let mol = parse_smarts("[13C]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_charge_positive_and_negative() {
let mol_pos = parse_smarts("[C+2]").unwrap();
let mol_neg = parse_smarts("[O-1]").unwrap();
assert_eq!(mol_pos.atoms.len(), 1);
assert_eq!(mol_neg.atoms.len(), 1);
}
#[test]
fn test_degree_and_connectivity() {
let mol = parse_smarts("[D4]").unwrap();
assert_eq!(mol.atoms.len(), 1);
let mol2 = parse_smarts("[X3]").unwrap();
assert_eq!(mol2.atoms.len(), 1);
}
#[test]
fn test_ring_size_constraint() {
let mol = parse_smarts("[r6]").unwrap();
assert_eq!(mol.atoms.len(), 1);
}
#[test]
fn test_nested_branch_depth() {
let mol = parse_smarts("C(C(C(C(C))))").unwrap();
assert_eq!(mol.atoms.len(), 5);
}
#[test]
fn test_ring_and_branch_combined() {
let mol = parse_smarts("c1cc(C)ccc1").unwrap();
assert_eq!(mol.atoms.len(), 7);
}
#[test]
fn test_all_bond_types() {
let bonds = vec![
("C-C", BondPrimitive::Single),
("C=C", BondPrimitive::Double),
("C#C", BondPrimitive::Triple),
("c:c", BondPrimitive::Aromatic),
];
for (smarts, expected_prim) in bonds {
let mol = parse_smarts(smarts).unwrap();
assert_eq!(mol.atoms.len(), 2);
assert_eq!(
mol.bonds[0].query,
BondQuery::Primitive(expected_prim),
"bond type mismatch for {smarts}"
);
}
}
#[test]
fn test_empty_recursive_pattern_rejected() {
let result = parse_smarts("[$$()]");
assert!(result.is_err() || result.as_ref().unwrap().atoms.len() == 1);
}
}