Skip to main content

ferro_hgvs/normalize/
config.rs

1//! Normalization configuration options
2
3use crate::error_handling::{ErrorConfig, ErrorMode, ErrorOverride, ErrorType, ResolvedAction};
4use serde::{Deserialize, Serialize};
5
6/// Direction for variant shuffling during normalization
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8pub enum ShuffleDirection {
9    /// Shuffle towards 3' end (default for HGVS)
10    #[default]
11    ThreePrime,
12    /// Shuffle towards 5' end (for VCF compatibility)
13    FivePrime,
14}
15
16impl std::fmt::Display for ShuffleDirection {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            ShuffleDirection::ThreePrime => write!(f, "3prime"),
20            ShuffleDirection::FivePrime => write!(f, "5prime"),
21        }
22    }
23}
24
25impl std::str::FromStr for ShuffleDirection {
26    type Err = String;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s.to_lowercase().as_str() {
30            "3prime" | "3'" | "three_prime" => Ok(ShuffleDirection::ThreePrime),
31            "5prime" | "5'" | "five_prime" => Ok(ShuffleDirection::FivePrime),
32            _ => Err(format!("Invalid shuffle direction: {}", s)),
33        }
34    }
35}
36
37/// Configuration for variant normalization
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct NormalizeConfig {
40    /// Direction to shuffle variants (default: 3')
41    pub shuffle_direction: ShuffleDirection,
42
43    /// Whether to allow crossing exon-intron boundaries
44    pub cross_boundaries: bool,
45
46    /// Error handling configuration (controls reference validation behavior)
47    #[serde(skip)]
48    pub error_config: ErrorConfig,
49
50    /// Window size for reference sequence fetching
51    pub window_size: u64,
52
53    /// Whether to prevent overlaps in compound variant normalization
54    ///
55    /// When true, normalization will check if variants in an allele would overlap
56    /// after shifting and constrain the normalization to prevent collisions.
57    pub prevent_overlap: bool,
58}
59
60impl Default for NormalizeConfig {
61    fn default() -> Self {
62        Self {
63            shuffle_direction: ShuffleDirection::ThreePrime,
64            cross_boundaries: false,
65            // Default to Lenient mode for backwards compatibility
66            // (previous behavior was to not validate at all)
67            error_config: ErrorConfig::lenient(),
68            window_size: 100,
69            prevent_overlap: true,
70        }
71    }
72}
73
74impl PartialEq for NormalizeConfig {
75    fn eq(&self, other: &Self) -> bool {
76        self.shuffle_direction == other.shuffle_direction
77            && self.cross_boundaries == other.cross_boundaries
78            && self.window_size == other.window_size
79            && self.prevent_overlap == other.prevent_overlap
80    }
81}
82
83impl Eq for NormalizeConfig {}
84
85impl NormalizeConfig {
86    /// Create a new config with default values
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Create a config with strict error handling (reject reference mismatches)
92    pub fn strict() -> Self {
93        Self {
94            error_config: ErrorConfig::strict(),
95            ..Default::default()
96        }
97    }
98
99    /// Create a config with lenient error handling (warn on reference mismatches)
100    pub fn lenient() -> Self {
101        Self {
102            error_config: ErrorConfig::lenient(),
103            ..Default::default()
104        }
105    }
106
107    /// Create a config with silent error handling (ignore reference mismatches)
108    pub fn silent() -> Self {
109        Self {
110            error_config: ErrorConfig::silent(),
111            ..Default::default()
112        }
113    }
114
115    /// Set shuffle direction
116    pub fn with_direction(mut self, direction: ShuffleDirection) -> Self {
117        self.shuffle_direction = direction;
118        self
119    }
120
121    /// Allow crossing boundaries
122    pub fn allow_crossing_boundaries(mut self) -> Self {
123        self.cross_boundaries = true;
124        self
125    }
126
127    /// Set error handling mode
128    pub fn with_error_mode(mut self, mode: ErrorMode) -> Self {
129        self.error_config = ErrorConfig::new(mode);
130        self
131    }
132
133    /// Set a specific error type override
134    pub fn with_error_override(mut self, error_type: ErrorType, action: ErrorOverride) -> Self {
135        self.error_config = self.error_config.with_override(error_type, action);
136        self
137    }
138
139    /// Disable reference validation (sets RefSeqMismatch to SilentCorrect)
140    #[deprecated(
141        since = "0.2.0",
142        note = "Use with_error_mode(ErrorMode::Silent) instead"
143    )]
144    pub fn skip_validation(mut self) -> Self {
145        self.error_config = self
146            .error_config
147            .with_override(ErrorType::RefSeqMismatch, ErrorOverride::SilentCorrect);
148        self
149    }
150
151    /// Enable overlap prevention in compound variants
152    pub fn with_overlap_prevention(mut self, prevent: bool) -> Self {
153        self.prevent_overlap = prevent;
154        self
155    }
156
157    /// Get the resolved action for reference sequence mismatch
158    pub fn ref_mismatch_action(&self) -> ResolvedAction {
159        self.error_config.action_for(ErrorType::RefSeqMismatch)
160    }
161
162    /// Returns true if reference mismatches should be rejected
163    pub fn should_reject_ref_mismatch(&self) -> bool {
164        self.ref_mismatch_action().should_reject()
165    }
166
167    /// Returns true if reference mismatches should emit warnings
168    pub fn should_warn_ref_mismatch(&self) -> bool {
169        self.ref_mismatch_action().should_warn()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_default_config() {
179        let config = NormalizeConfig::default();
180        assert_eq!(config.shuffle_direction, ShuffleDirection::ThreePrime);
181        assert!(!config.cross_boundaries);
182        // Default is lenient (warn but don't reject)
183        assert!(!config.should_reject_ref_mismatch());
184        assert!(config.should_warn_ref_mismatch());
185    }
186
187    #[test]
188    fn test_strict_config() {
189        let config = NormalizeConfig::strict();
190        assert!(config.should_reject_ref_mismatch());
191        assert!(!config.should_warn_ref_mismatch());
192    }
193
194    #[test]
195    fn test_lenient_config() {
196        let config = NormalizeConfig::lenient();
197        assert!(!config.should_reject_ref_mismatch());
198        assert!(config.should_warn_ref_mismatch());
199    }
200
201    #[test]
202    fn test_silent_config() {
203        let config = NormalizeConfig::silent();
204        assert!(!config.should_reject_ref_mismatch());
205        assert!(!config.should_warn_ref_mismatch());
206    }
207
208    #[test]
209    fn test_error_override() {
210        // Start with lenient, override RefSeqMismatch to reject
211        let config = NormalizeConfig::lenient()
212            .with_error_override(ErrorType::RefSeqMismatch, ErrorOverride::Reject);
213        assert!(config.should_reject_ref_mismatch());
214    }
215
216    #[test]
217    fn test_direction_parsing() {
218        assert_eq!(
219            "3prime".parse::<ShuffleDirection>().unwrap(),
220            ShuffleDirection::ThreePrime
221        );
222        assert_eq!(
223            "5prime".parse::<ShuffleDirection>().unwrap(),
224            ShuffleDirection::FivePrime
225        );
226    }
227
228    #[test]
229    #[allow(deprecated)]
230    fn test_skip_validation_deprecated() {
231        let config = NormalizeConfig::default().skip_validation();
232        // skip_validation sets RefSeqMismatch to SilentCorrect
233        assert!(!config.should_reject_ref_mismatch());
234        assert!(!config.should_warn_ref_mismatch());
235    }
236}