Skip to main content

ferro_hgvs/error_handling/
mod.rs

1//! Configurable error handling for HGVS parsing.
2//!
3//! This module provides three error handling modes (Strict, Lenient, Silent)
4//! plus per-error-type configuration overrides. This allows users to control
5//! how the parser handles common input errors like wrong dash characters,
6//! lowercase amino acids, and extra whitespace.
7//!
8//! # Example
9//!
10//! ```
11//! use ferro_hgvs::error_handling::{ErrorConfig, ErrorMode, ErrorType, ErrorOverride};
12//!
13//! // Strict mode (default): reject all non-standard input
14//! let config = ErrorConfig::strict();
15//!
16//! // Lenient mode: auto-correct with warnings
17//! let config = ErrorConfig::lenient();
18//!
19//! // Silent mode: auto-correct without warnings
20//! let config = ErrorConfig::silent();
21//!
22//! // Custom: lenient mode but reject lowercase amino acids
23//! let config = ErrorConfig::lenient()
24//!     .with_override(ErrorType::LowercaseAminoAcid, ErrorOverride::Reject);
25//! ```
26//!
27//! # Error Types
28//!
29//! The following error types can be individually configured:
30//!
31//! | Error Type | Example | Description |
32//! |------------|---------|-------------|
33//! | `LowercaseAminoAcid` | `val` → `Val` | Lowercase 3-letter amino acid codes |
34//! | `MissingVersion` | `NM_000088` → `NM_000088.3` | Missing accession version |
35//! | `WrongDashCharacter` | `–` → `-` | En-dash/em-dash instead of hyphen |
36//! | `ExtraWhitespace` | `c.100 A>G` → `c.100A>G` | Extra spaces in description |
37//! | `ProteinSubstitutionArrow` | `Val600>Glu` → `Val600Glu` | Arrow in protein substitution |
38//! | `PositionZero` | `c.0A>G` | Invalid position zero (always rejected) |
39//!
40//! # Override Behaviors
41//!
42//! Each error type can have one of these override behaviors:
43//!
44//! - `Default`: Use the base mode's behavior
45//! - `Reject`: Always return an error
46//! - `WarnCorrect`: Auto-correct and emit a warning
47//! - `SilentCorrect`: Auto-correct without warning
48//! - `Accept`: Accept the input as-is without correction
49
50pub mod codes;
51pub mod corrections;
52mod preprocessor;
53pub mod registry;
54mod types;
55
56pub use codes::{CodeCategory, CodeInfo, ModeAction, ModeBehavior};
57pub use corrections::{
58    detect_accession_typo, detect_amino_acid_typo, detect_edit_type_typo, detect_missing_version,
59    detect_swapped_positions, detect_typos, find_closest_match, levenshtein_distance,
60    DetectedCorrection, FuzzyMatch, TypoSuggestion, TypoTokenType,
61};
62pub use preprocessor::{CorrectionWarning, InputPreprocessor, PreprocessResult};
63pub use registry::{get_code_info, list_all_codes, list_error_codes, list_warning_codes};
64pub use types::{ErrorMode, ErrorOverride, ErrorType, ResolvedAction};
65
66use std::collections::HashMap;
67
68/// Error handling configuration.
69///
70/// Controls how the parser handles common input errors. The configuration
71/// consists of a base mode plus optional per-error-type overrides.
72#[derive(Debug, Clone)]
73pub struct ErrorConfig {
74    /// Base error handling mode.
75    pub mode: ErrorMode,
76    /// Per-error-type overrides.
77    pub overrides: HashMap<ErrorType, ErrorOverride>,
78}
79
80impl ErrorConfig {
81    /// Create a new configuration with the given mode.
82    pub fn new(mode: ErrorMode) -> Self {
83        Self {
84            mode,
85            overrides: HashMap::new(),
86        }
87    }
88
89    /// Create a strict configuration.
90    ///
91    /// In strict mode, all non-standard input is rejected.
92    pub fn strict() -> Self {
93        Self::new(ErrorMode::Strict)
94    }
95
96    /// Create a lenient configuration.
97    ///
98    /// In lenient mode, common errors are auto-corrected with warnings.
99    pub fn lenient() -> Self {
100        Self::new(ErrorMode::Lenient)
101    }
102
103    /// Create a silent configuration.
104    ///
105    /// In silent mode, common errors are auto-corrected without warnings.
106    pub fn silent() -> Self {
107        Self::new(ErrorMode::Silent)
108    }
109
110    /// Add an override for a specific error type.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// use ferro_hgvs::error_handling::{ErrorConfig, ErrorType, ErrorOverride};
116    ///
117    /// let config = ErrorConfig::lenient()
118    ///     .with_override(ErrorType::LowercaseAminoAcid, ErrorOverride::Reject)
119    ///     .with_override(ErrorType::ExtraWhitespace, ErrorOverride::SilentCorrect);
120    /// ```
121    pub fn with_override(mut self, error_type: ErrorType, override_: ErrorOverride) -> Self {
122        self.overrides.insert(error_type, override_);
123        self
124    }
125
126    /// Set an override for a specific error type.
127    ///
128    /// This is the mutable version of `with_override`.
129    pub fn set_override(&mut self, error_type: ErrorType, override_: ErrorOverride) {
130        self.overrides.insert(error_type, override_);
131    }
132
133    /// Remove an override for a specific error type.
134    pub fn remove_override(&mut self, error_type: ErrorType) {
135        self.overrides.remove(&error_type);
136    }
137
138    /// Get the resolved action for an error type.
139    ///
140    /// This applies the override if one exists, otherwise uses the base mode.
141    pub fn action_for(&self, error_type: ErrorType) -> ResolvedAction {
142        let override_ = self.overrides.get(&error_type).copied().unwrap_or_default();
143        override_.resolve(self.mode)
144    }
145
146    /// Returns true if the given error type should be rejected.
147    pub fn should_reject(&self, error_type: ErrorType) -> bool {
148        self.action_for(error_type).should_reject()
149    }
150
151    /// Returns true if the given error type should be corrected.
152    pub fn should_correct(&self, error_type: ErrorType) -> bool {
153        self.action_for(error_type).should_correct()
154    }
155
156    /// Returns true if the given error type should emit a warning.
157    pub fn should_warn(&self, error_type: ErrorType) -> bool {
158        self.action_for(error_type).should_warn()
159    }
160
161    /// Create a preprocessor with this configuration.
162    pub fn preprocessor(&self) -> InputPreprocessor {
163        InputPreprocessor::new(self.clone())
164    }
165}
166
167impl Default for ErrorConfig {
168    fn default() -> Self {
169        Self::strict()
170    }
171}
172
173/// Parse result with warnings.
174///
175/// This struct wraps a parsed variant along with any warnings
176/// generated during preprocessing or parsing.
177#[derive(Debug, Clone)]
178pub struct ParseResultWithWarnings<T> {
179    /// The parsed result.
180    pub result: T,
181    /// Warnings generated during parsing.
182    pub warnings: Vec<CorrectionWarning>,
183    /// The original input.
184    pub original_input: String,
185    /// The preprocessed input (may be same as original).
186    pub preprocessed_input: String,
187}
188
189impl<T> ParseResultWithWarnings<T> {
190    /// Create a new parse result with warnings.
191    pub fn new(
192        result: T,
193        warnings: Vec<CorrectionWarning>,
194        original_input: String,
195        preprocessed_input: String,
196    ) -> Self {
197        Self {
198            result,
199            warnings,
200            original_input,
201            preprocessed_input,
202        }
203    }
204
205    /// Create a parse result without warnings.
206    pub fn without_warnings(result: T, input: String) -> Self {
207        Self {
208            result,
209            warnings: Vec::new(),
210            original_input: input.clone(),
211            preprocessed_input: input,
212        }
213    }
214
215    /// Returns true if there were any corrections made.
216    pub fn had_corrections(&self) -> bool {
217        self.original_input != self.preprocessed_input
218    }
219
220    /// Returns true if there are any warnings.
221    pub fn has_warnings(&self) -> bool {
222        !self.warnings.is_empty()
223    }
224
225    /// Map the result to a new type.
226    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> ParseResultWithWarnings<U> {
227        ParseResultWithWarnings {
228            result: f(self.result),
229            warnings: self.warnings,
230            original_input: self.original_input,
231            preprocessed_input: self.preprocessed_input,
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    // ErrorConfig tests
241    #[test]
242    fn test_error_config_default() {
243        let config = ErrorConfig::default();
244        assert_eq!(config.mode, ErrorMode::Strict);
245        assert!(config.overrides.is_empty());
246    }
247
248    #[test]
249    fn test_error_config_strict() {
250        let config = ErrorConfig::strict();
251        assert_eq!(config.mode, ErrorMode::Strict);
252        assert!(config.should_reject(ErrorType::WrongDashCharacter));
253    }
254
255    #[test]
256    fn test_error_config_lenient() {
257        let config = ErrorConfig::lenient();
258        assert_eq!(config.mode, ErrorMode::Lenient);
259        assert!(config.should_correct(ErrorType::WrongDashCharacter));
260        assert!(config.should_warn(ErrorType::WrongDashCharacter));
261    }
262
263    #[test]
264    fn test_error_config_silent() {
265        let config = ErrorConfig::silent();
266        assert_eq!(config.mode, ErrorMode::Silent);
267        assert!(config.should_correct(ErrorType::WrongDashCharacter));
268        assert!(!config.should_warn(ErrorType::WrongDashCharacter));
269    }
270
271    #[test]
272    fn test_error_config_with_override() {
273        let config = ErrorConfig::lenient()
274            .with_override(ErrorType::LowercaseAminoAcid, ErrorOverride::Reject);
275
276        // Overridden error type should reject
277        assert!(config.should_reject(ErrorType::LowercaseAminoAcid));
278
279        // Non-overridden error types should use base mode
280        assert!(config.should_correct(ErrorType::WrongDashCharacter));
281    }
282
283    #[test]
284    fn test_error_config_set_override() {
285        let mut config = ErrorConfig::strict();
286        config.set_override(ErrorType::WrongDashCharacter, ErrorOverride::SilentCorrect);
287        assert!(config.should_correct(ErrorType::WrongDashCharacter));
288        assert!(!config.should_warn(ErrorType::WrongDashCharacter));
289    }
290
291    #[test]
292    fn test_error_config_remove_override() {
293        let mut config = ErrorConfig::lenient()
294            .with_override(ErrorType::WrongDashCharacter, ErrorOverride::Reject);
295
296        // With override
297        assert!(config.should_reject(ErrorType::WrongDashCharacter));
298
299        // After removing override
300        config.remove_override(ErrorType::WrongDashCharacter);
301        assert!(config.should_correct(ErrorType::WrongDashCharacter));
302    }
303
304    #[test]
305    fn test_error_config_action_for() {
306        let config =
307            ErrorConfig::lenient().with_override(ErrorType::PositionZero, ErrorOverride::Reject);
308
309        assert_eq!(
310            config.action_for(ErrorType::WrongDashCharacter),
311            ResolvedAction::WarnCorrect
312        );
313        assert_eq!(
314            config.action_for(ErrorType::PositionZero),
315            ResolvedAction::Reject
316        );
317    }
318
319    #[test]
320    fn test_error_config_preprocessor() {
321        let config = ErrorConfig::lenient();
322        let preprocessor = config.preprocessor();
323
324        let result = preprocessor.preprocess("c.100\u{2013}200del");
325        assert!(result.success);
326        assert_eq!(result.preprocessed, "c.100-200del");
327    }
328
329    // ParseResultWithWarnings tests
330    #[test]
331    fn test_parse_result_with_warnings_new() {
332        let result = ParseResultWithWarnings::new(
333            42,
334            vec![CorrectionWarning::new(
335                ErrorType::WrongDashCharacter,
336                "test",
337                None,
338                "",
339                "",
340            )],
341            "original".to_string(),
342            "preprocessed".to_string(),
343        );
344
345        assert_eq!(result.result, 42);
346        assert!(result.has_warnings());
347        assert!(result.had_corrections());
348    }
349
350    #[test]
351    fn test_parse_result_with_warnings_without_warnings() {
352        let result = ParseResultWithWarnings::without_warnings(42, "input".to_string());
353
354        assert_eq!(result.result, 42);
355        assert!(!result.has_warnings());
356        assert!(!result.had_corrections());
357    }
358
359    #[test]
360    fn test_parse_result_with_warnings_map() {
361        let result = ParseResultWithWarnings::without_warnings(42, "input".to_string());
362        let mapped = result.map(|x| x.to_string());
363
364        assert_eq!(mapped.result, "42");
365    }
366}