bio_forge/io/
error.rs

1//! Canonical error type for all structure and template IO operations.
2//!
3//! This module wraps parser, serializer, and filesystem failures into a single
4//! `Error` enum that higher-level operations can bubble up or convert into
5//! user-facing diagnostics with uniform wording.
6
7use std::fmt;
8use std::path::PathBuf;
9use thiserror::Error;
10
11/// Errors that can occur while reading or writing biomolecular data.
12///
13/// The enum captures I/O failures, structured parser issues, unknown residues, and
14/// integrity mismatches so callers can inspect the variant and react accordingly.
15#[derive(Debug, Error)]
16pub enum Error {
17    /// Wrapper around operating-system level I/O failures.
18    ///
19    /// Includes both filesystem and stream sources, optionally carrying the file path for
20    /// richer error messages.
21    #[error(
22        "I/O error for {path_desc}: {source}",
23        path_desc = PathDisplay(path)
24    )]
25    Io {
26        /// Path to the file involved in the failed operation, if any.
27        path: Option<PathBuf>,
28        /// Underlying error emitted by the standard library.
29        #[source]
30        source: std::io::Error,
31    },
32
33    /// Indicates that an input line could not be parsed into the expected record.
34    ///
35    /// Exposes the textual format, source path, failing line number, and an explanatory
36    /// detail string to assist with debugging malformed files.
37    #[error(
38        "failed to parse {format} {path_desc}: {details} (line {line_number})",
39        path_desc = PathDisplay(path)
40    )]
41    Parse {
42        /// Name of the textual format (e.g., `"PDB"`, `"mmCIF"`).
43        format: &'static str,
44        /// Path to the offending file, if known.
45        path: Option<PathBuf>,
46        /// One-based line number where parsing failed.
47        line_number: usize,
48        /// Human-readable description of what went wrong.
49        details: String,
50    },
51
52    /// Raised when an atom record references a residue not present in template libraries.
53    #[error(
54        "unknown standard residue name '{name}' in {path_desc} could not be resolved",
55        path_desc = PathDisplay(path)
56    )]
57    UnknownStandardResidue { name: String, path: Option<PathBuf> },
58
59    /// Reports logical inconsistencies such as mismatched atom counts or invalid records.
60    #[error(
61        "inconsistent data in {format} {path_desc}: {details}",
62        path_desc = PathDisplay(path)
63    )]
64    InconsistentData {
65        /// Name of the textual format being processed.
66        format: &'static str,
67        /// Related file path when available.
68        path: Option<PathBuf>,
69        /// Summary of the detected inconsistency.
70        details: String,
71    },
72}
73
74impl Error {
75    /// Constructs an [`Error::Io`] variant from a standard I/O error.
76    ///
77    /// # Arguments
78    ///
79    /// * `source` - The original `std::io::Error` emitted by the OS or runtime.
80    /// * `path` - Optional file path associated with the operation.
81    ///
82    /// # Returns
83    ///
84    /// A ready-to-use `Error` that preserves the source error for chaining.
85    pub fn from_io(source: std::io::Error, path: Option<PathBuf>) -> Self {
86        Self::Io { path, source }
87    }
88
89    /// Builds a [`Error::Parse`] variant with consistent messaging.
90    ///
91    /// # Arguments
92    ///
93    /// * `format` - Name of the textual format being parsed.
94    /// * `path` - Optional path pointing to the input file.
95    /// * `line_number` - Line where the failure occurred (1-indexed).
96    /// * `details` - Additional context about the parsing problem.
97    ///
98    /// # Returns
99    ///
100    /// An `Error::Parse` carrying the supplied metadata.
101    pub fn parse(
102        format: &'static str,
103        path: Option<PathBuf>,
104        line_number: usize,
105        details: impl Into<String>,
106    ) -> Self {
107        Self::Parse {
108            format,
109            path,
110            line_number,
111            details: details.into(),
112        }
113    }
114
115    /// Generates an [`Error::UnknownStandardResidue`] for unresolved template names.
116    ///
117    /// # Arguments
118    ///
119    /// * `name` - Residue identifier that failed to look up.
120    /// * `path` - Optional path to the source file.
121    ///
122    /// # Returns
123    ///
124    /// An error variant signalling the missing residue definition.
125    pub fn unknown_standard_residue(name: impl Into<String>, path: Option<PathBuf>) -> Self {
126        Self::UnknownStandardResidue {
127            name: name.into(),
128            path,
129        }
130    }
131
132    /// Creates an [`Error::InconsistentData`] describing logical mismatches.
133    ///
134    /// # Arguments
135    ///
136    /// * `format` - Name of the textual format being processed.
137    /// * `path` - Optional file path, if applicable.
138    /// * `details` - Explanation of the inconsistency.
139    ///
140    /// # Returns
141    ///
142    /// An error variant pointing to structural or semantic discrepancies.
143    pub fn inconsistent_data(
144        format: &'static str,
145        path: Option<PathBuf>,
146        details: impl Into<String>,
147    ) -> Self {
148        Self::InconsistentData {
149            format,
150            path,
151            details: details.into(),
152        }
153    }
154}
155
156/// Lightweight formatter for optional paths used in error messages.
157///
158/// When a path is present it prints `file '<path>'`; otherwise it emits `stream source` so
159/// error messages remain grammatically consistent.
160struct PathDisplay<'a>(&'a Option<PathBuf>);
161
162impl<'a> fmt::Display for PathDisplay<'a> {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self.0 {
165            Some(p) => write!(f, "file '{}'", p.display()),
166            None => write!(f, "stream source"),
167        }
168    }
169}