use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum ValidationLevel {
Lenient = 0,
#[default]
Standard = 1,
Strict = 2,
}
impl ValidationLevel {
pub fn at_least(&self, other: ValidationLevel) -> bool {
(*self as u8) >= (other as u8)
}
pub fn is_lenient(&self) -> bool {
*self == ValidationLevel::Lenient
}
pub fn is_strict(&self) -> bool {
*self == ValidationLevel::Strict
}
}
static GLOBAL_VALIDATION_LEVEL: AtomicU8 = AtomicU8::new(ValidationLevel::Standard as u8);
pub fn get_validation_level() -> ValidationLevel {
match GLOBAL_VALIDATION_LEVEL.load(Ordering::Relaxed) {
0 => ValidationLevel::Lenient,
1 => ValidationLevel::Standard,
_ => ValidationLevel::Strict,
}
}
pub fn set_validation_level(level: ValidationLevel) {
GLOBAL_VALIDATION_LEVEL.store(level as u8, Ordering::Relaxed);
}
pub fn with_validation_level<T, F: FnOnce() -> T>(level: ValidationLevel, f: F) -> T {
let old_level = get_validation_level();
set_validation_level(level);
let result = f();
set_validation_level(old_level);
result
}
#[derive(Debug, Clone, Default)]
pub struct ParseConfig {
pub level: ValidationLevel,
pub allow_missing_version: bool,
pub allow_lowercase_prefix: bool,
pub allow_colon_separator: bool,
}
impl ParseConfig {
pub fn new() -> Self {
Self::default()
}
pub fn lenient() -> Self {
Self {
level: ValidationLevel::Lenient,
allow_missing_version: true,
allow_lowercase_prefix: true,
allow_colon_separator: true,
}
}
pub fn standard() -> Self {
Self {
level: ValidationLevel::Standard,
allow_missing_version: true,
allow_lowercase_prefix: false,
allow_colon_separator: false,
}
}
pub fn strict() -> Self {
Self {
level: ValidationLevel::Strict,
allow_missing_version: false,
allow_lowercase_prefix: false,
allow_colon_separator: false,
}
}
pub fn with_level(mut self, level: ValidationLevel) -> Self {
self.level = level;
self
}
pub fn with_allow_missing_version(mut self, allow: bool) -> Self {
self.allow_missing_version = allow;
self
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub warnings: Vec<ValidationWarning>,
pub errors: Vec<ValidationError>,
}
impl ValidationResult {
pub fn ok() -> Self {
Self {
valid: true,
warnings: Vec::new(),
errors: Vec::new(),
}
}
pub fn with_warning(mut self, warning: ValidationWarning) -> Self {
self.warnings.push(warning);
self
}
pub fn with_error(mut self, error: ValidationError) -> Self {
self.valid = false;
self.errors.push(error);
self
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct ValidationWarning {
pub code: &'static str,
pub message: String,
pub position: Option<usize>,
}
impl ValidationWarning {
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
position: None,
}
}
pub fn at_position(mut self, pos: usize) -> Self {
self.position = Some(pos);
self
}
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub code: &'static str,
pub message: String,
pub position: Option<usize>,
}
impl ValidationError {
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
position: None,
}
}
pub fn at_position(mut self, pos: usize) -> Self {
self.position = Some(pos);
self
}
}
pub mod rules {
use crate::hgvs::edit::{Base, NaEdit, ProteinEdit};
use crate::hgvs::variant::HgvsVariant;
use super::{ValidationError, ValidationResult, ValidationWarning};
pub fn validate_position_order(start: i64, end: i64) -> ValidationResult {
if start > end {
ValidationResult::ok().with_error(ValidationError::new(
"E001",
format!(
"Start position ({}) is greater than end position ({})",
start, end
),
))
} else {
ValidationResult::ok()
}
}
pub fn validate_na_edit(edit: &NaEdit) -> ValidationResult {
let mut result = ValidationResult::ok();
match edit {
NaEdit::Substitution {
reference,
alternative,
} if reference == alternative => {
result = result.with_warning(ValidationWarning::new(
"W001",
format!(
"Reference and alternative are the same: {} = {}",
reference, alternative
),
));
}
NaEdit::Deletion {
sequence: Some(seq),
..
} if seq.is_empty() => {
result = result.with_warning(ValidationWarning::new(
"W002",
"Deletion has empty sequence specified",
));
}
NaEdit::Insertion { sequence } if sequence.is_empty() => {
result = result.with_error(ValidationError::new(
"E002",
"Insertion must have at least one base",
));
}
NaEdit::Delins { sequence } if sequence.is_empty() => {
result = result.with_error(ValidationError::new(
"E003",
"Deletion-insertion must have at least one inserted base",
));
}
_ => {}
}
result
}
pub fn validate_protein_edit(_edit: &ProteinEdit) -> ValidationResult {
ValidationResult::ok()
}
pub fn is_valid_base(base: &Base) -> bool {
matches!(
base,
Base::A | Base::C | Base::G | Base::T | Base::U | Base::N
)
}
pub fn validate_variant_consistency(variant: &HgvsVariant) -> ValidationResult {
let mut result = ValidationResult::ok();
match variant {
HgvsVariant::Cds(v) => {
if let Some(start) = v.loc_edit.location.start.inner() {
if let Some(end) = v.loc_edit.location.end.inner() {
if start.utr3 != end.utr3 && !start.utr3 && end.utr3 {
}
}
}
}
HgvsVariant::Protein(v) => {
if let Some(ProteinEdit::Frameshift { .. }) = v.loc_edit.edit.inner() {
}
}
HgvsVariant::Allele(a) => {
for v in &a.variants {
let sub_result = validate_variant_consistency(v);
for err in sub_result.errors {
result = result.with_error(err);
}
for warn in sub_result.warnings {
result = result.with_warning(warn);
}
}
}
_ => {}
}
result
}
pub fn validate_variant(variant: &HgvsVariant) -> ValidationResult {
let mut result = ValidationResult::ok();
let consistency = validate_variant_consistency(variant);
for err in consistency.errors {
result = result.with_error(err);
}
for warn in consistency.warnings {
result = result.with_warning(warn);
}
result
}
}
pub struct Validator<P: crate::reference::ReferenceProvider> {
#[allow(dead_code)] provider: P,
config: ParseConfig,
}
impl<P: crate::reference::ReferenceProvider> Validator<P> {
pub fn new(provider: P) -> Self {
Self {
provider,
config: ParseConfig::default(),
}
}
pub fn with_config(provider: P, config: ParseConfig) -> Self {
Self { provider, config }
}
pub fn validate(&self, variant: &crate::hgvs::variant::HgvsVariant) -> ValidationResult {
let mut result = rules::validate_variant(variant);
if self.config.level.at_least(ValidationLevel::Standard) {
if let Some(ref_error) = self.validate_reference_base(variant) {
result = result.with_error(ref_error);
}
}
result
}
fn validate_reference_base(
&self,
_variant: &crate::hgvs::variant::HgvsVariant,
) -> Option<ValidationError> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_level_default() {
assert_eq!(ValidationLevel::default(), ValidationLevel::Standard);
}
#[test]
fn test_validation_level_at_least() {
assert!(ValidationLevel::Strict.at_least(ValidationLevel::Standard));
assert!(ValidationLevel::Standard.at_least(ValidationLevel::Lenient));
assert!(!ValidationLevel::Lenient.at_least(ValidationLevel::Strict));
}
#[test]
fn test_parse_config_lenient() {
let config = ParseConfig::lenient();
assert_eq!(config.level, ValidationLevel::Lenient);
assert!(config.allow_missing_version);
assert!(config.allow_lowercase_prefix);
}
#[test]
fn test_parse_config_strict() {
let config = ParseConfig::strict();
assert_eq!(config.level, ValidationLevel::Strict);
assert!(!config.allow_missing_version);
assert!(!config.allow_lowercase_prefix);
}
#[test]
fn test_validation_result() {
let result = ValidationResult::ok();
assert!(result.valid);
assert!(!result.has_warnings());
assert!(!result.has_errors());
let result = result.with_warning(ValidationWarning::new("W001", "test warning"));
assert!(result.valid);
assert!(result.has_warnings());
}
#[test]
fn test_global_validation_level() {
let original = get_validation_level();
set_validation_level(ValidationLevel::Lenient);
assert_eq!(get_validation_level(), ValidationLevel::Lenient);
set_validation_level(ValidationLevel::Strict);
assert_eq!(get_validation_level(), ValidationLevel::Strict);
set_validation_level(original);
}
#[test]
fn test_with_validation_level() {
let original = get_validation_level();
set_validation_level(ValidationLevel::Standard);
let result = with_validation_level(ValidationLevel::Strict, || {
assert_eq!(get_validation_level(), ValidationLevel::Strict);
42
});
assert_eq!(result, 42);
assert_eq!(get_validation_level(), ValidationLevel::Standard);
set_validation_level(original);
}
mod rules_tests {
use super::*;
use crate::hgvs::edit::{Base, InsertedSequence, NaEdit, Sequence};
#[test]
fn test_validate_position_order_valid() {
let result = rules::validate_position_order(10, 20);
assert!(result.valid);
assert!(!result.has_errors());
}
#[test]
fn test_validate_position_order_equal() {
let result = rules::validate_position_order(10, 10);
assert!(result.valid);
}
#[test]
fn test_validate_position_order_invalid() {
let result = rules::validate_position_order(20, 10);
assert!(!result.valid);
assert!(result.has_errors());
assert_eq!(result.errors[0].code, "E001");
}
#[test]
fn test_validate_substitution_same_base_warning() {
let edit = NaEdit::Substitution {
reference: Base::A,
alternative: Base::A,
};
let result = rules::validate_na_edit(&edit);
assert!(result.valid); assert!(result.has_warnings());
assert_eq!(result.warnings[0].code, "W001");
}
#[test]
fn test_validate_substitution_different_bases() {
let edit = NaEdit::Substitution {
reference: Base::A,
alternative: Base::G,
};
let result = rules::validate_na_edit(&edit);
assert!(result.valid);
assert!(!result.has_warnings());
}
#[test]
fn test_validate_insertion_empty_error() {
let edit = NaEdit::Insertion {
sequence: InsertedSequence::Literal(Sequence::new(vec![])),
};
let result = rules::validate_na_edit(&edit);
assert!(!result.valid);
assert!(result.has_errors());
assert_eq!(result.errors[0].code, "E002");
}
#[test]
fn test_validate_delins_empty_error() {
let edit = NaEdit::Delins {
sequence: InsertedSequence::Literal(Sequence::new(vec![])),
};
let result = rules::validate_na_edit(&edit);
assert!(!result.valid);
assert!(result.has_errors());
assert_eq!(result.errors[0].code, "E003");
}
#[test]
fn test_is_valid_base() {
assert!(rules::is_valid_base(&Base::A));
assert!(rules::is_valid_base(&Base::C));
assert!(rules::is_valid_base(&Base::G));
assert!(rules::is_valid_base(&Base::T));
assert!(rules::is_valid_base(&Base::U));
assert!(rules::is_valid_base(&Base::N));
}
#[test]
fn test_validate_variant_consistency_cds() {
use crate::hgvs::parser::parse_hgvs;
let variant = parse_hgvs("NM_000088.3:c.100A>G").unwrap();
let result = rules::validate_variant(&variant);
assert!(result.valid);
}
#[test]
fn test_validate_variant_consistency_allele() {
use crate::hgvs::parser::parse_hgvs;
let variant = parse_hgvs("[NM_000088.3:c.100A>G;NM_000088.3:c.200C>T]").unwrap();
let result = rules::validate_variant(&variant);
assert!(result.valid);
}
}
mod validator_tests {
use super::*;
use crate::hgvs::parser::parse_hgvs;
use crate::reference::MockProvider;
#[test]
fn test_validator_creation() {
let provider = MockProvider::with_test_data();
let _validator = Validator::new(provider);
}
#[test]
fn test_validator_with_config() {
let provider = MockProvider::with_test_data();
let config = ParseConfig::strict();
let validator = Validator::with_config(provider, config);
assert_eq!(validator.config.level, ValidationLevel::Strict);
}
#[test]
fn test_validate_simple_variant() {
let provider = MockProvider::with_test_data();
let validator = Validator::new(provider);
let variant = parse_hgvs("NM_000088.3:c.10A>G").unwrap();
let result = validator.validate(&variant);
assert!(result.valid);
}
#[test]
fn test_validate_protein_variant() {
let provider = MockProvider::with_test_data();
let validator = Validator::new(provider);
let variant = parse_hgvs("NP_000079.2:p.Val600Glu").unwrap();
let result = validator.validate(&variant);
assert!(result.valid);
}
}
}