use crate::error_handling::{ErrorConfig, ErrorMode, ErrorOverride, ErrorType, ResolvedAction};
use serde::{Deserialize, Serialize};
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum ShuffleDirection {
#[default]
ThreePrime,
FivePrime,
}
impl std::fmt::Display for ShuffleDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ShuffleDirection::ThreePrime => write!(f, "3prime"),
ShuffleDirection::FivePrime => write!(f, "5prime"),
}
}
}
impl std::str::FromStr for ShuffleDirection {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"3prime" | "3'" | "three_prime" => Ok(ShuffleDirection::ThreePrime),
"5prime" | "5'" | "five_prime" => Ok(ShuffleDirection::FivePrime),
_ => Err(format!("Invalid shuffle direction: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NormalizeConfig {
#[doc(hidden)]
pub shuffle_direction: ShuffleDirection,
pub cross_boundaries: bool,
#[serde(skip)]
pub error_config: ErrorConfig,
pub window_size: u64,
pub prevent_overlap: bool,
}
impl Default for NormalizeConfig {
fn default() -> Self {
Self {
shuffle_direction: ShuffleDirection::ThreePrime,
cross_boundaries: false,
error_config: ErrorConfig::lenient(),
window_size: 100,
prevent_overlap: true,
}
}
}
impl PartialEq for NormalizeConfig {
fn eq(&self, other: &Self) -> bool {
self.shuffle_direction == other.shuffle_direction
&& self.cross_boundaries == other.cross_boundaries
&& self.window_size == other.window_size
&& self.prevent_overlap == other.prevent_overlap
}
}
impl Eq for NormalizeConfig {}
impl NormalizeConfig {
pub fn new() -> Self {
Self::default()
}
pub fn for_entry_point(direction: ShuffleDirection, error_config: ErrorConfig) -> Self {
Self {
shuffle_direction: direction,
error_config,
..Default::default()
}
}
pub fn strict() -> Self {
Self {
error_config: ErrorConfig::strict(),
..Default::default()
}
}
pub fn lenient() -> Self {
Self {
error_config: ErrorConfig::lenient(),
..Default::default()
}
}
pub fn silent() -> Self {
Self {
error_config: ErrorConfig::silent(),
..Default::default()
}
}
#[doc(hidden)]
pub fn with_direction(mut self, direction: ShuffleDirection) -> Self {
self.shuffle_direction = direction;
self
}
pub fn allow_crossing_boundaries(mut self) -> Self {
self.cross_boundaries = true;
self
}
pub fn with_error_mode(mut self, mode: ErrorMode) -> Self {
self.error_config = ErrorConfig::new(mode);
self
}
pub fn with_error_config(mut self, error_config: ErrorConfig) -> Self {
self.error_config = error_config;
self
}
pub fn with_error_override(mut self, error_type: ErrorType, action: ErrorOverride) -> Self {
self.error_config = self.error_config.with_override(error_type, action);
self
}
#[deprecated(
since = "0.2.0",
note = "Use with_error_mode(ErrorMode::Silent) instead"
)]
pub fn skip_validation(mut self) -> Self {
self.error_config = self
.error_config
.with_override(ErrorType::RefSeqMismatch, ErrorOverride::SilentCorrect);
self
}
pub fn with_overlap_prevention(mut self, prevent: bool) -> Self {
self.prevent_overlap = prevent;
self
}
pub fn ref_mismatch_action(&self) -> ResolvedAction {
self.error_config.action_for(ErrorType::RefSeqMismatch)
}
pub fn should_reject_ref_mismatch(&self) -> bool {
self.ref_mismatch_action().should_reject()
}
pub fn should_warn_ref_mismatch(&self) -> bool {
self.ref_mismatch_action().should_warn()
}
pub fn variant_exceeds_reference_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::VariantExceedsReference)
}
pub fn should_reject_variant_exceeds_reference(&self) -> bool {
self.variant_exceeds_reference_action().should_reject()
}
pub fn should_warn_variant_exceeds_reference(&self) -> bool {
self.variant_exceeds_reference_action().should_warn()
}
pub fn position_past_end_action(&self) -> ResolvedAction {
self.error_config.action_for(ErrorType::PositionPastEnd)
}
pub fn should_reject_position_past_end(&self) -> bool {
self.position_past_end_action().should_reject()
}
pub fn should_reject_reduced_capability(&self) -> bool {
self.error_config.mode.is_strict()
}
pub fn should_warn_position_past_end(&self) -> bool {
self.position_past_end_action().should_warn()
}
pub fn intronic_bare_transcript_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::IntronicOnBareTranscript)
}
pub fn should_reject_intronic_bare_transcript(&self) -> bool {
self.intronic_bare_transcript_action().should_reject()
}
pub fn should_warn_intronic_bare_transcript(&self) -> bool {
self.intronic_bare_transcript_action().should_warn()
}
pub fn overlap_conflict_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::OverlapConflictingEdits)
}
pub fn should_reject_overlap_conflict(&self) -> bool {
self.overlap_conflict_action().should_reject()
}
pub fn unresolvable_centromere_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::UnresolvableCentromere)
}
pub fn should_reject_unresolvable_centromere(&self) -> bool {
self.unresolvable_centromere_action().should_reject()
}
pub fn transcript_flank_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::TranscriptFlankNotDescribable)
}
pub fn should_reject_transcript_flank(&self) -> bool {
self.transcript_flank_action().should_reject()
}
pub fn incomplete_cds_start_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::IncompleteCdsStartReference)
}
pub fn should_reject_incomplete_cds_start(&self) -> bool {
self.incomplete_cds_start_action().should_reject()
}
pub fn should_warn_incomplete_cds_start(&self) -> bool {
self.incomplete_cds_start_action().should_warn()
}
pub fn initiator_met_canonicalization_action(&self) -> ResolvedAction {
self.error_config
.action_for(ErrorType::InitiatorMetCanonicalization)
}
pub fn should_warn_initiator_met_canonicalization(&self) -> bool {
let action = self.initiator_met_canonicalization_action();
action.should_warn() || action.should_reject()
}
pub fn should_reject_initiator_met_canonicalization(&self) -> bool {
matches!(
self.error_config
.explicit_override(ErrorType::InitiatorMetCanonicalization),
Some(ErrorOverride::Reject)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn for_entry_point_carries_the_error_config_the_builder_chain_defaults_away() {
let explicit =
NormalizeConfig::for_entry_point(ShuffleDirection::FivePrime, ErrorConfig::strict());
assert_eq!(explicit.shuffle_direction, ShuffleDirection::FivePrime);
assert!(
explicit.should_reject_ref_mismatch(),
"the strict config handed in must arrive",
);
let defaulted = NormalizeConfig::default().with_direction(ShuffleDirection::FivePrime);
assert!(
!defaulted.should_reject_ref_mismatch(),
"premise: the builder chain silently supplies a *lenient* error config, which is \
the omission that made `--error-mode` inert for five months (#1181)",
);
}
#[test]
fn for_entry_point_leaves_other_fields_at_their_defaults() {
let config =
NormalizeConfig::for_entry_point(ShuffleDirection::ThreePrime, ErrorConfig::silent());
let default = NormalizeConfig::default();
assert_eq!(config.cross_boundaries, default.cross_boundaries);
assert_eq!(config.window_size, default.window_size);
assert_eq!(config.prevent_overlap, default.prevent_overlap);
}
#[test]
fn test_default_config() {
let config = NormalizeConfig::default();
assert_eq!(config.shuffle_direction, ShuffleDirection::ThreePrime);
assert!(!config.cross_boundaries);
assert!(!config.should_reject_ref_mismatch());
assert!(config.should_warn_ref_mismatch());
}
#[test]
fn test_strict_config() {
let config = NormalizeConfig::strict();
assert!(config.should_reject_ref_mismatch());
assert!(!config.should_warn_ref_mismatch());
}
#[test]
fn test_lenient_config() {
let config = NormalizeConfig::lenient();
assert!(!config.should_reject_ref_mismatch());
assert!(config.should_warn_ref_mismatch());
}
#[test]
fn test_silent_config() {
let config = NormalizeConfig::silent();
assert!(!config.should_reject_ref_mismatch());
assert!(!config.should_warn_ref_mismatch());
}
#[test]
fn test_error_override() {
let config = NormalizeConfig::lenient()
.with_error_override(ErrorType::RefSeqMismatch, ErrorOverride::Reject);
assert!(config.should_reject_ref_mismatch());
}
#[test]
fn test_direction_parsing() {
assert_eq!(
"3prime".parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::ThreePrime
);
assert_eq!(
"5prime".parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::FivePrime
);
}
#[test]
fn test_parse_direction_three_prime() {
for spelling in ["3prime", "3'", "three_prime"] {
assert_eq!(
spelling.parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::ThreePrime,
"{spelling}"
);
}
}
#[test]
fn test_parse_direction_five_prime() {
for spelling in ["5prime", "5'", "five_prime"] {
assert_eq!(
spelling.parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::FivePrime,
"{spelling}"
);
}
}
#[test]
fn test_parse_direction_case_insensitive() {
assert_eq!(
"5PRIME".parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::FivePrime
);
assert_eq!(
"5Prime".parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::FivePrime
);
assert_eq!(
"3PRIME".parse::<ShuffleDirection>().unwrap(),
ShuffleDirection::ThreePrime
);
}
#[test]
fn test_parse_direction_unrecognized_is_err() {
for spelling in ["unknown", "", "5prim", "3prine", "five", "5", "3", "banana"] {
assert!(
spelling.parse::<ShuffleDirection>().is_err(),
"{spelling} must not silently resolve to a direction"
);
}
}
#[test]
#[allow(deprecated)]
fn test_skip_validation_deprecated() {
let config = NormalizeConfig::default().skip_validation();
assert!(!config.should_reject_ref_mismatch());
assert!(!config.should_warn_ref_mismatch());
}
}