use alloc::{format, string::String};
use core::{fmt, num::TryFromIntError, ops::Range};
use elements_rs::Element;
use thiserror::Error;
use crate::{
atom::atom_symbol::AtomSymbol,
bond::{Bond, BondDescriptor},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
pub enum SmilesError {
#[error("Bond in bracket: {0}")]
BondInBracket(Bond),
#[error("Charge overflow: {0}")]
ChargeOverflow(i8),
#[error("Charge underflow: {0}")]
ChargeUnderflow(i8),
#[error("Node A: {0} has multiple edges with Node B: {1}")]
DuplicateEdge(usize, usize),
#[error("Element requires brackets")]
ElementRequiresBrackets,
#[error("Error Parsing Element: {0}")]
ElementsRs(#[from] elements_rs::errors::Error),
#[error("A branch without any nodes has been found")]
EmptyBranch,
#[error("Bond: {0} missing atom index(es)")]
IncompleteBond(BondDescriptor),
#[error("Invalid aromatic element: {0}")]
InvalidAromaticElement(Element),
#[error("A bond has been found in a non valid location")]
InvalidBond,
#[error("An invalid branch has been found")]
InvalidBranch,
#[error("Invalid chirality")]
InvalidChirality,
#[error("Invalid class")]
InvalidClass,
#[error("Invalid element name: {0}")]
InvalidElementName(char),
#[error("Hydrogen count overflow: {0}")]
HydrogenCountOverflow(u8),
#[error("Hydrogen found as bracketed atom with an unsupported explicit hydrogen count")]
InvalidHydrogenWithExplicitHydrogensFound,
#[error("Invalid isotope")]
InvalidIsotope,
#[error("Invalid Non-bond '.' found")]
InvalidNonBondToken,
#[error("Invalid number")]
InvalidNumber,
#[error("Integer overflow")]
IntegerOverflow,
#[error("Invalid unbracketed atom: {0}")]
InvalidUnbracketedAtom(AtomSymbol),
#[error("Invalid ring number")]
InvalidRingNumber,
#[error("Missing element inside brackets")]
MissingBracketElement,
#[error("Missing element")]
MissingElement,
#[error("Invalid atom index: {0}")]
NodeIdInvalid(usize),
#[error("Non-bond '.' in bracket")]
NonBondInBracket,
#[error("Ring number overflow: {0}")]
RingNumberOverflow(u32),
#[error("Node: {0} has an edge that goes from itself and to itself")]
SelfLoopEdge(usize),
#[error("Unexpected bracketed state")]
UnexpectedBracketedState,
#[error("Unexpected end of string")]
UnexpectedEndOfString,
#[error("Unexpected character: {0}")]
UnexpectedCharacter(char),
#[error("Unexpected unicode character")]
UnexpectedUnicodeCharacter,
#[error("Unexpected ':'")]
UnexpectedColon,
#[error("Unexpected '-'")]
UnexpectedDash,
#[error("Unexpected '%'")]
UnexpectedPercent,
#[error("Unexpected '['")]
UnexpectedLeftBracket,
#[error("Unexpected '('")]
UnexpectedLeftParentheses,
#[error("Unexpected ']'")]
UnexpectedRightBracket,
#[error("Unexpected `)`")]
UnexpectedRightParentheses,
#[error("Wildcard atom not allowed")]
WildcardAtomNotAllowed,
#[error("Unclosed '['")]
UnclosedBracket,
#[error("Branch not closed")]
UnclosedBranch,
#[error("Ring not closed")]
UnclosedRing,
}
impl From<TryFromIntError> for SmilesError {
fn from(_: TryFromIntError) -> Self {
SmilesError::InvalidNumber
}
}
#[derive(Debug)]
pub struct SmilesErrorWithSpan {
smiles_error: SmilesError,
span: Range<usize>,
}
impl SmilesErrorWithSpan {
#[must_use]
pub fn new(smiles_error: SmilesError, start: usize, end: usize) -> Self {
Self { smiles_error, span: Range { start, end } }
}
#[must_use]
pub fn smiles_error(&self) -> SmilesError {
self.smiles_error
}
#[must_use]
pub fn start(&self) -> usize {
self.span.start
}
#[must_use]
pub fn end(&self) -> usize {
self.span.end
}
#[must_use]
pub fn span(&self) -> Range<usize> {
self.span.start..self.span.end
}
#[must_use]
pub fn render(&self, input: &str) -> String {
let start = self.start().min(input.len());
let end = self.end().min(input.len()).max(start + 1).min(input.len());
let mut underline = String::new();
underline.push_str(&" ".repeat(start));
underline.push_str(&"^".repeat(end - start));
format!("{input}\n{underline}\n{}", self.smiles_error)
}
}
impl fmt::Display for SmilesErrorWithSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} at {}..{}", self.smiles_error, self.start(), self.end())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum SubgraphError {
#[error("Atom id {0} is out of range for the parent graph")]
AtomOutOfRange(usize),
#[error("Bond references atom {0}, which is not in the fragment")]
BondReferencesUnknownAtom(usize),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum RootError {
#[error("Atom {0} is not in the fragment")]
AtomNotInFragment(usize),
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use std::num::TryFromIntError;
use elements_rs::Element;
use crate::{
atom::atom_symbol::AtomSymbol,
bond::{Bond, BondDescriptor},
errors::{SmilesError, SmilesErrorWithSpan},
};
#[test]
fn test_smiles_error_display_monolithic() {
let elements_rs_error = elements_rs::errors::Error::AtomicNumber(4);
let cases = [
(
SmilesError::BondInBracket(Bond::Double),
format!("Bond in bracket: {}", Bond::Double),
),
(SmilesError::ChargeOverflow(50), "Charge overflow: 50".to_string()),
(SmilesError::ChargeUnderflow(-50), "Charge underflow: -50".to_string()),
(SmilesError::ElementRequiresBrackets, "Element requires brackets".to_string()),
(
SmilesError::ElementsRs(elements_rs_error),
format!("Error Parsing Element: {elements_rs_error}"),
),
(
SmilesError::IncompleteBond(BondDescriptor::aromatic(Bond::Single)),
"Bond: : missing atom index(es)".to_string(),
),
(SmilesError::HydrogenCountOverflow(16), "Hydrogen count overflow: 16".to_string()),
(
SmilesError::InvalidAromaticElement(Element::Ac),
format!("Invalid aromatic element: {}", Element::Ac),
),
(SmilesError::InvalidChirality, "Invalid chirality".to_string()),
(SmilesError::InvalidClass, "Invalid class".to_string()),
(SmilesError::InvalidElementName('w'), "Invalid element name: w".to_string()),
(SmilesError::InvalidIsotope, "Invalid isotope".to_string()),
(SmilesError::InvalidNonBondToken, "Invalid Non-bond '.' found".to_string()),
(SmilesError::InvalidNumber, "Invalid number".to_string()),
(SmilesError::IntegerOverflow, "Integer overflow".to_string()),
(
SmilesError::InvalidUnbracketedAtom(AtomSymbol::WildCard),
format!("Invalid unbracketed atom: {}", AtomSymbol::WildCard),
),
(SmilesError::InvalidRingNumber, "Invalid ring number".to_string()),
(SmilesError::MissingBracketElement, "Missing element inside brackets".to_string()),
(SmilesError::MissingElement, "Missing element".to_string()),
(SmilesError::NodeIdInvalid(2), "Invalid atom index: 2".to_string()),
(SmilesError::NonBondInBracket, "Non-bond '.' in bracket".to_string()),
(SmilesError::RingNumberOverflow(100), "Ring number overflow: 100".to_string()),
(SmilesError::UnexpectedBracketedState, "Unexpected bracketed state".to_string()),
(SmilesError::UnexpectedEndOfString, "Unexpected end of string".to_string()),
(SmilesError::UnexpectedCharacter('$'), "Unexpected character: $".to_string()),
(SmilesError::UnexpectedUnicodeCharacter, "Unexpected unicode character".to_string()),
(SmilesError::UnexpectedColon, "Unexpected ':'".to_string()),
(SmilesError::UnexpectedDash, "Unexpected '-'".to_string()),
(SmilesError::UnexpectedPercent, "Unexpected '%'".to_string()),
(SmilesError::UnexpectedLeftBracket, "Unexpected '['".to_string()),
(SmilesError::UnexpectedLeftParentheses, "Unexpected '('".to_string()),
(SmilesError::UnexpectedRightBracket, "Unexpected ']'".to_string()),
(SmilesError::UnexpectedRightParentheses, "Unexpected `)`".to_string()),
(SmilesError::WildcardAtomNotAllowed, "Wildcard atom not allowed".to_string()),
(SmilesError::UnclosedBracket, "Unclosed '['".to_string()),
(SmilesError::UnclosedBranch, "Branch not closed".to_string()),
(SmilesError::UnclosedRing, "Ring not closed".to_string()),
(
SmilesError::SelfLoopEdge(1),
"Node: 1 has an edge that goes from itself and to itself".to_string(),
),
(
SmilesError::DuplicateEdge(0, 1),
"Node A: 0 has multiple edges with Node B: 1".to_string(),
),
];
for (error, expected) in cases {
assert_eq!(error.to_string(), expected, "wrong Display for {error:?}");
}
}
#[test]
fn test_smiles_error_from_conversions() {
let int_error: Result<u8, TryFromIntError> = u8::try_from(300_i16);
let int_error = int_error.unwrap_err();
let smiles_error: SmilesError = int_error.into();
assert_eq!(smiles_error, SmilesError::InvalidNumber);
let elements_error = elements_rs::errors::Error::AtomicNumber(4);
let smiles_error: SmilesError = elements_error.into();
assert_eq!(
smiles_error,
SmilesError::ElementsRs(elements_rs::errors::Error::AtomicNumber(4))
);
}
#[test]
fn test_smiles_error_with_span() {
let error = SmilesErrorWithSpan::new(SmilesError::UnexpectedCharacter('$'), 2, 3);
assert_eq!(error.smiles_error(), SmilesError::UnexpectedCharacter('$'));
assert_eq!(error.start(), 2);
assert_eq!(error.end(), 3);
assert_eq!(error.span(), (2..3));
assert_eq!(error.to_string(), "Unexpected character: $ at 2..3");
assert_eq!(error.render("CC$O"), "CC$O\n ^\nUnexpected character: $");
}
#[test]
fn render_marks_zero_width_and_multi_character_spans() {
let zero_width = SmilesErrorWithSpan::new(SmilesError::UnexpectedEndOfString, 2, 2);
assert_eq!(zero_width.render("CCO"), "CCO\n ^\nUnexpected end of string");
let two_wide = SmilesErrorWithSpan::new(SmilesError::UnexpectedCharacter('x'), 1, 3);
assert_eq!(two_wide.render("CCCC"), "CCCC\n ^^\nUnexpected character: x");
}
#[test]
fn test_smiles_error_with_unicode_span() {
let error = SmilesErrorWithSpan::new(SmilesError::UnexpectedUnicodeCharacter, 2, 4);
assert_eq!(error.smiles_error(), SmilesError::UnexpectedUnicodeCharacter);
assert_eq!(error.start(), 2);
assert_eq!(error.end(), 4);
assert_eq!(error.span(), (2..4));
assert_eq!(error.to_string(), "Unexpected unicode character at 2..4");
}
}