1use alloc::{format, string::String};
4use core::{fmt, num::TryFromIntError, ops::Range};
5
6use elements_rs::Element;
7use thiserror::Error;
8
9use crate::{
10 atom::atom_symbol::AtomSymbol,
11 bond::{Bond, BondDescriptor},
12};
13
14#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
16pub enum SmilesError {
17 #[error("Bond in bracket: {0}")]
19 BondInBracket(Bond),
20 #[error("Charge overflow: {0}")]
22 ChargeOverflow(i8),
23 #[error("Charge underflow: {0}")]
25 ChargeUnderflow(i8),
26 #[error("Node A: {0} has multiple edges with Node B: {1}")]
28 DuplicateEdge(usize, usize),
29 #[error("Element requires brackets")]
31 ElementRequiresBrackets,
32 #[error("Error Parsing Element: {0}")]
34 ElementsRs(#[from] elements_rs::errors::Error),
35 #[error("A branch without any nodes has been found")]
37 EmptyBranch,
38 #[error("Bond: {0} missing atom index(es)")]
40 IncompleteBond(BondDescriptor),
41 #[error("Invalid aromatic element: {0}")]
43 InvalidAromaticElement(Element),
44 #[error("A bond has been found in a non valid location")]
47 InvalidBond,
48 #[error("An invalid branch has been found")]
50 InvalidBranch,
51 #[error("Invalid chirality")]
53 InvalidChirality,
54 #[error("Invalid class")]
56 InvalidClass,
57 #[error("Invalid element name: {0}")]
59 InvalidElementName(char),
60 #[error("Hydrogen count overflow: {0}")]
68 HydrogenCountOverflow(u8),
69 #[error("Hydrogen found as bracketed atom with an unsupported explicit hydrogen count")]
71 InvalidHydrogenWithExplicitHydrogensFound,
72 #[error("Invalid isotope")]
74 InvalidIsotope,
75 #[error("Invalid Non-bond '.' found")]
77 InvalidNonBondToken,
78 #[error("Invalid number")]
80 InvalidNumber,
81 #[error("Integer overflow")]
83 IntegerOverflow,
84 #[error("Invalid unbracketed atom: {0}")]
86 InvalidUnbracketedAtom(AtomSymbol),
87 #[error("Invalid ring number")]
89 InvalidRingNumber,
90 #[error("Missing element inside brackets")]
92 MissingBracketElement,
93 #[error("Missing element")]
95 MissingElement,
96 #[error("Invalid atom index: {0}")]
98 NodeIdInvalid(usize),
99 #[error("Non-bond '.' in bracket")]
101 NonBondInBracket,
102 #[error("Ring number overflow: {0}")]
104 RingNumberOverflow(u32),
105 #[error("Node: {0} has an edge that goes from itself and to itself")]
107 SelfLoopEdge(usize),
108 #[error("Unexpected bracketed state")]
110 UnexpectedBracketedState,
111 #[error("Unexpected end of string")]
113 UnexpectedEndOfString,
114 #[error("Unexpected character: {0}")]
116 UnexpectedCharacter(char),
117 #[error("Unexpected unicode character")]
119 UnexpectedUnicodeCharacter,
120 #[error("Unexpected ':'")]
122 UnexpectedColon,
123 #[error("Unexpected '-'")]
125 UnexpectedDash,
126 #[error("Unexpected '%'")]
128 UnexpectedPercent,
129 #[error("Unexpected '['")]
131 UnexpectedLeftBracket,
132 #[error("Unexpected '('")]
134 UnexpectedLeftParentheses,
135 #[error("Unexpected ']'")]
137 UnexpectedRightBracket,
138 #[error("Unexpected `)`")]
140 UnexpectedRightParentheses,
141 #[error("Wildcard atom not allowed")]
143 WildcardAtomNotAllowed,
144 #[error("Unclosed '['")]
146 UnclosedBracket,
147 #[error("Branch not closed")]
149 UnclosedBranch,
150 #[error("Ring not closed")]
152 UnclosedRing,
153}
154
155impl From<TryFromIntError> for SmilesError {
156 fn from(_: TryFromIntError) -> Self {
157 SmilesError::InvalidNumber
158 }
159}
160
161#[derive(Debug)]
163pub struct SmilesErrorWithSpan {
164 smiles_error: SmilesError,
166 span: Range<usize>,
168}
169
170impl SmilesErrorWithSpan {
171 #[must_use]
183 pub fn new(smiles_error: SmilesError, start: usize, end: usize) -> Self {
184 Self { smiles_error, span: Range { start, end } }
185 }
186
187 #[must_use]
198 pub fn smiles_error(&self) -> SmilesError {
199 self.smiles_error
200 }
201
202 #[must_use]
213 pub fn start(&self) -> usize {
214 self.span.start
215 }
216
217 #[must_use]
228 pub fn end(&self) -> usize {
229 self.span.end
230 }
231
232 #[must_use]
243 pub fn span(&self) -> Range<usize> {
244 self.span.start..self.span.end
245 }
246
247 #[must_use]
261 pub fn render(&self, input: &str) -> String {
262 let start = self.start().min(input.len());
263 let end = self.end().min(input.len()).max(start + 1).min(input.len());
264
265 let mut underline = String::new();
266 underline.push_str(&" ".repeat(start));
267 underline.push_str(&"^".repeat(end - start));
268
269 format!("{input}\n{underline}\n{}", self.smiles_error)
270 }
271}
272
273impl fmt::Display for SmilesErrorWithSpan {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 write!(f, "{} at {}..{}", self.smiles_error, self.start(), self.end())
276 }
277}
278
279#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
282#[non_exhaustive]
283pub enum SubgraphError {
284 #[error("Atom id {0} is out of range for the parent graph")]
286 AtomOutOfRange(usize),
287 #[error("Bond references atom {0}, which is not in the fragment")]
289 BondReferencesUnknownAtom(usize),
290}
291
292#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
294#[non_exhaustive]
295pub enum RootError {
296 #[error("Atom {0} is not in the fragment")]
298 AtomNotInFragment(usize),
299}
300
301#[cfg(test)]
302mod tests {
303 use alloc::string::ToString;
304 use std::num::TryFromIntError;
305
306 use elements_rs::Element;
307
308 use crate::{
309 atom::atom_symbol::AtomSymbol,
310 bond::{Bond, BondDescriptor},
311 errors::{SmilesError, SmilesErrorWithSpan},
312 };
313
314 #[test]
315 fn test_smiles_error_display_monolithic() {
316 let elements_rs_error = elements_rs::errors::Error::AtomicNumber(4);
317
318 let cases = [
319 (
320 SmilesError::BondInBracket(Bond::Double),
321 format!("Bond in bracket: {}", Bond::Double),
322 ),
323 (SmilesError::ChargeOverflow(50), "Charge overflow: 50".to_string()),
324 (SmilesError::ChargeUnderflow(-50), "Charge underflow: -50".to_string()),
325 (SmilesError::ElementRequiresBrackets, "Element requires brackets".to_string()),
326 (
327 SmilesError::ElementsRs(elements_rs_error),
328 format!("Error Parsing Element: {elements_rs_error}"),
329 ),
330 (
331 SmilesError::IncompleteBond(BondDescriptor::aromatic(Bond::Single)),
332 "Bond: : missing atom index(es)".to_string(),
333 ),
334 (SmilesError::HydrogenCountOverflow(16), "Hydrogen count overflow: 16".to_string()),
335 (
336 SmilesError::InvalidAromaticElement(Element::Ac),
337 format!("Invalid aromatic element: {}", Element::Ac),
338 ),
339 (SmilesError::InvalidChirality, "Invalid chirality".to_string()),
340 (SmilesError::InvalidClass, "Invalid class".to_string()),
341 (SmilesError::InvalidElementName('w'), "Invalid element name: w".to_string()),
342 (SmilesError::InvalidIsotope, "Invalid isotope".to_string()),
343 (SmilesError::InvalidNonBondToken, "Invalid Non-bond '.' found".to_string()),
344 (SmilesError::InvalidNumber, "Invalid number".to_string()),
345 (SmilesError::IntegerOverflow, "Integer overflow".to_string()),
346 (
347 SmilesError::InvalidUnbracketedAtom(AtomSymbol::WildCard),
348 format!("Invalid unbracketed atom: {}", AtomSymbol::WildCard),
349 ),
350 (SmilesError::InvalidRingNumber, "Invalid ring number".to_string()),
351 (SmilesError::MissingBracketElement, "Missing element inside brackets".to_string()),
352 (SmilesError::MissingElement, "Missing element".to_string()),
353 (SmilesError::NodeIdInvalid(2), "Invalid atom index: 2".to_string()),
354 (SmilesError::NonBondInBracket, "Non-bond '.' in bracket".to_string()),
355 (SmilesError::RingNumberOverflow(100), "Ring number overflow: 100".to_string()),
356 (SmilesError::UnexpectedBracketedState, "Unexpected bracketed state".to_string()),
357 (SmilesError::UnexpectedEndOfString, "Unexpected end of string".to_string()),
358 (SmilesError::UnexpectedCharacter('$'), "Unexpected character: $".to_string()),
359 (SmilesError::UnexpectedUnicodeCharacter, "Unexpected unicode character".to_string()),
360 (SmilesError::UnexpectedColon, "Unexpected ':'".to_string()),
361 (SmilesError::UnexpectedDash, "Unexpected '-'".to_string()),
362 (SmilesError::UnexpectedPercent, "Unexpected '%'".to_string()),
363 (SmilesError::UnexpectedLeftBracket, "Unexpected '['".to_string()),
364 (SmilesError::UnexpectedLeftParentheses, "Unexpected '('".to_string()),
365 (SmilesError::UnexpectedRightBracket, "Unexpected ']'".to_string()),
366 (SmilesError::UnexpectedRightParentheses, "Unexpected `)`".to_string()),
367 (SmilesError::WildcardAtomNotAllowed, "Wildcard atom not allowed".to_string()),
368 (SmilesError::UnclosedBracket, "Unclosed '['".to_string()),
369 (SmilesError::UnclosedBranch, "Branch not closed".to_string()),
370 (SmilesError::UnclosedRing, "Ring not closed".to_string()),
371 (
372 SmilesError::SelfLoopEdge(1),
373 "Node: 1 has an edge that goes from itself and to itself".to_string(),
374 ),
375 (
376 SmilesError::DuplicateEdge(0, 1),
377 "Node A: 0 has multiple edges with Node B: 1".to_string(),
378 ),
379 ];
380
381 for (error, expected) in cases {
382 assert_eq!(error.to_string(), expected, "wrong Display for {error:?}");
383 }
384 }
385
386 #[test]
387 fn test_smiles_error_from_conversions() {
388 let int_error: Result<u8, TryFromIntError> = u8::try_from(300_i16);
389 let int_error = int_error.unwrap_err();
390
391 let smiles_error: SmilesError = int_error.into();
392 assert_eq!(smiles_error, SmilesError::InvalidNumber);
393
394 let elements_error = elements_rs::errors::Error::AtomicNumber(4);
395 let smiles_error: SmilesError = elements_error.into();
396 assert_eq!(
397 smiles_error,
398 SmilesError::ElementsRs(elements_rs::errors::Error::AtomicNumber(4))
399 );
400 }
401
402 #[test]
403 fn test_smiles_error_with_span() {
404 let error = SmilesErrorWithSpan::new(SmilesError::UnexpectedCharacter('$'), 2, 3);
405
406 assert_eq!(error.smiles_error(), SmilesError::UnexpectedCharacter('$'));
407 assert_eq!(error.start(), 2);
408 assert_eq!(error.end(), 3);
409 assert_eq!(error.span(), (2..3));
410
411 assert_eq!(error.to_string(), "Unexpected character: $ at 2..3");
412
413 assert_eq!(error.render("CC$O"), "CC$O\n ^\nUnexpected character: $");
414 }
415
416 #[test]
417 fn render_marks_zero_width_and_multi_character_spans() {
418 let zero_width = SmilesErrorWithSpan::new(SmilesError::UnexpectedEndOfString, 2, 2);
422 assert_eq!(zero_width.render("CCO"), "CCO\n ^\nUnexpected end of string");
423
424 let two_wide = SmilesErrorWithSpan::new(SmilesError::UnexpectedCharacter('x'), 1, 3);
427 assert_eq!(two_wide.render("CCCC"), "CCCC\n ^^\nUnexpected character: x");
428 }
429
430 #[test]
431 fn test_smiles_error_with_unicode_span() {
432 let error = SmilesErrorWithSpan::new(SmilesError::UnexpectedUnicodeCharacter, 2, 4);
433
434 assert_eq!(error.smiles_error(), SmilesError::UnexpectedUnicodeCharacter);
435 assert_eq!(error.start(), 2);
436 assert_eq!(error.end(), 4);
437 assert_eq!(error.span(), (2..4));
438
439 assert_eq!(error.to_string(), "Unexpected unicode character at 2..4");
440 }
441}