|
| 1 | +//! CTV3 validator for validating CTV3 codes in a codelist |
| 2 | +//! |
| 3 | +//! Validation Rules |
| 4 | +//! 1. The code must be exactly 5 characters in length. |
| 5 | +//! 2. Only alphanumeric characters (a-z, A-Z, 0-9) and dots (.) are allowed. |
| 6 | +//! 3. The code starts with 0-5 alphanumeric characters followed by dots to pad to 5 characters. |
| 7 | +use std::sync::LazyLock; |
| 8 | + |
| 9 | +use codelist_rs::codelist::CodeList; |
| 10 | +use regex::Regex; |
| 11 | + |
| 12 | +use crate::{errors::CodeListValidatorError, validator::CodeValidator}; |
| 13 | + |
| 14 | +pub struct Ctv3Validator<'a>(pub &'a CodeList); |
| 15 | + |
| 16 | +static REGEX: LazyLock<Regex> = LazyLock::new(|| { |
| 17 | + Regex::new(r"^(?:[a-zA-Z0-9]{5}|[a-zA-Z0-9]{4}\.|[a-zA-Z0-9]{3}\.\.|[a-zA-Z0-9]{2}\.\.\.|[a-zA-Z0-9]{1}\.\.\.\.|\.{5})$").expect("Unable to create regex") |
| 18 | +}); |
| 19 | + |
| 20 | +impl CodeValidator for Ctv3Validator<'_> { |
| 21 | + fn validate_code(&self, code: &str) -> Result<(), CodeListValidatorError> { |
| 22 | + if code.len() > 5 { |
| 23 | + return Err(CodeListValidatorError::invalid_code_length( |
| 24 | + code, |
| 25 | + "Code is greater than 5 characters in length", |
| 26 | + self.0.codelist_type.to_string(), |
| 27 | + )); |
| 28 | + } |
| 29 | + |
| 30 | + if code.len() < 5 { |
| 31 | + return Err(CodeListValidatorError::invalid_code_length( |
| 32 | + code, |
| 33 | + "Code is less than 5 characters in length", |
| 34 | + self.0.codelist_type.to_string(), |
| 35 | + )); |
| 36 | + } |
| 37 | + |
| 38 | + if !REGEX.is_match(code) { |
| 39 | + return Err(CodeListValidatorError::invalid_code_contents( |
| 40 | + code, |
| 41 | + "Code does not match the expected format", |
| 42 | + self.0.codelist_type.to_string(), |
| 43 | + )); |
| 44 | + } |
| 45 | + |
| 46 | + Ok(()) |
| 47 | + } |
| 48 | + |
| 49 | + fn validate_all_code(&self) -> Result<(), CodeListValidatorError> { |
| 50 | + let mut reasons = Vec::new(); |
| 51 | + |
| 52 | + for (code, _) in self.0.entries.iter() { |
| 53 | + if let Err(err) = self.validate_code(code) { |
| 54 | + reasons.push(err.to_string()); |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + if reasons.is_empty() { |
| 59 | + Ok(()) |
| 60 | + } else { |
| 61 | + Err(CodeListValidatorError::invalid_codelist(reasons)) |
| 62 | + } |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +#[cfg(test)] |
| 67 | +mod tests { |
| 68 | + use codelist_rs::{ |
| 69 | + codelist::CodeList, errors::CodeListError, metadata::Metadata, types::CodeListType, |
| 70 | + }; |
| 71 | + |
| 72 | + use super::*; |
| 73 | + use crate::validator::Validator; |
| 74 | + |
| 75 | + // Helper function to create a test codelist with two entries, default options |
| 76 | + // and test metadata |
| 77 | + fn create_test_codelist() -> Result<CodeList, CodeListError> { |
| 78 | + let codelist = CodeList::new( |
| 79 | + "test_codelist".to_string(), |
| 80 | + CodeListType::CTV3, |
| 81 | + Metadata::default(), |
| 82 | + None, |
| 83 | + ); |
| 84 | + Ok(codelist) |
| 85 | + } |
| 86 | + |
| 87 | + #[test] |
| 88 | + fn test_validate_codelist_with_valid_code() -> Result<(), CodeListError> { |
| 89 | + let mut codelist = create_test_codelist()?; |
| 90 | + let _ = codelist.add_entry("A9f..".to_string(), None, None); |
| 91 | + assert!(codelist.validate_codes().is_ok()); |
| 92 | + Ok(()) |
| 93 | + } |
| 94 | + |
| 95 | + #[test] |
| 96 | + fn test_validate_code_with_invalid_code_length_too_long() -> Result<(), CodeListError> { |
| 97 | + let codelist = create_test_codelist()?; |
| 98 | + let validator = Ctv3Validator(&codelist); |
| 99 | + let code: &'static str = "A009000000"; |
| 100 | + let error = validator.validate_code(code).unwrap_err().to_string(); |
| 101 | + assert_eq!(error, "Code A009000000 is an invalid length for type CTV3. Reason: Code is greater than 5 characters in length"); |
| 102 | + Ok(()) |
| 103 | + } |
| 104 | + |
| 105 | + #[test] |
| 106 | + fn test_validate_code_with_invalid_code_length_too_short() -> Result<(), CodeListError> { |
| 107 | + let codelist = create_test_codelist()?; |
| 108 | + let validator = Ctv3Validator(&codelist); |
| 109 | + let code = "Af."; |
| 110 | + let error = validator.validate_code(code).unwrap_err().to_string(); |
| 111 | + assert_eq!(error, "Code Af. is an invalid length for type CTV3. Reason: Code is less than 5 characters in length"); |
| 112 | + Ok(()) |
| 113 | + } |
| 114 | + |
| 115 | + #[test] |
| 116 | + fn test_validate_invalid_code_dot_first_character() -> Result<(), CodeListError> { |
| 117 | + let codelist = create_test_codelist()?; |
| 118 | + let validator = Ctv3Validator(&codelist); |
| 119 | + let code = ".a009"; |
| 120 | + let error = validator.validate_code(code).unwrap_err().to_string(); |
| 121 | + assert_eq!(error, "Code .a009 contents is invalid for type CTV3. Reason: Code does not match the expected format"); |
| 122 | + Ok(()) |
| 123 | + } |
| 124 | + |
| 125 | + #[test] |
| 126 | + fn test_validate_invalid_code_dot_middle_character_between_letters() -> Result<(), CodeListError> |
| 127 | + { |
| 128 | + let codelist = create_test_codelist()?; |
| 129 | + let validator = Ctv3Validator(&codelist); |
| 130 | + let code = "10a.f"; |
| 131 | + let error = validator.validate_code(code).unwrap_err().to_string(); |
| 132 | + assert_eq!(error, "Code 10a.f contents is invalid for type CTV3. Reason: Code does not match the expected format"); |
| 133 | + Ok(()) |
| 134 | + } |
| 135 | + |
| 136 | + #[test] |
| 137 | + fn test_validate_invalid_code_invalid_characters() -> Result<(), CodeListError> { |
| 138 | + let codelist = create_test_codelist()?; |
| 139 | + let validator = Ctv3Validator(&codelist); |
| 140 | + let code = "Af!!!"; |
| 141 | + let error = validator.validate_code(code).unwrap_err().to_string(); |
| 142 | + assert_eq!(error, "Code Af!!! contents is invalid for type CTV3. Reason: Code does not match the expected format"); |
| 143 | + Ok(()) |
| 144 | + } |
| 145 | + |
| 146 | + #[test] |
| 147 | + fn test_validate_codelist_with_valid_codes() -> Result<(), CodeListError> { |
| 148 | + let mut codelist = create_test_codelist()?; |
| 149 | + codelist.add_entry("Af918".to_string(), None, None)?; |
| 150 | + codelist.add_entry("ABb..".to_string(), None, None)?; |
| 151 | + codelist.add_entry("alkif".to_string(), None, None)?; |
| 152 | + codelist.add_entry("F....".to_string(), None, None)?; |
| 153 | + codelist.add_entry("bn89.".to_string(), None, None)?; |
| 154 | + codelist.add_entry("Me...".to_string(), None, None)?; |
| 155 | + codelist.add_entry("99999".to_string(), None, None)?; |
| 156 | + codelist.add_entry(".....".to_string(), None, None)?; |
| 157 | + assert!(codelist.validate_codes().is_ok()); |
| 158 | + Ok(()) |
| 159 | + } |
| 160 | + |
| 161 | + #[test] |
| 162 | + fn test_validate_codelist_with_all_invalid_codes() -> Result<(), CodeListError> { |
| 163 | + let mut codelist = create_test_codelist()?; |
| 164 | + codelist.add_entry("A00900000".to_string(), None, None)?; |
| 165 | + codelist.add_entry("10".to_string(), None, None)?; |
| 166 | + codelist.add_entry("a.9jb".to_string(), None, None)?; |
| 167 | + codelist.add_entry("..9jJ".to_string(), None, None)?; |
| 168 | + codelist.add_entry("A00A".to_string(), None, None)?; |
| 169 | + codelist.add_entry("*unf.".to_string(), None, None)?; |
| 170 | + codelist.add_entry("..j..".to_string(), None, None)?; |
| 171 | + codelist.add_entry("9874ji".to_string(), None, None)?; |
| 172 | + let error = codelist.validate_codes().unwrap_err(); |
| 173 | + let error_string = error.to_string(); |
| 174 | + |
| 175 | + assert!(error_string.contains("Some codes in the list are invalid. Details:")); |
| 176 | + assert!(error_string.contains("Code A00900000 is an invalid length for type CTV3. Reason: Code is greater than 5 characters in length")); |
| 177 | + assert!(error_string.contains("Code 10 is an invalid length for type CTV3. Reason: Code is less than 5 characters in length")); |
| 178 | + assert!(error_string.contains("Code a.9jb contents is invalid for type CTV3. Reason: Code does not match the expected format")); |
| 179 | + assert!(error_string.contains("Code ..9jJ contents is invalid for type CTV3. Reason: Code does not match the expected format")); |
| 180 | + assert!(error_string.contains("Code A00A is an invalid length for type CTV3. Reason: Code is less than 5 characters in length")); |
| 181 | + assert!(error_string.contains("Code *unf. contents is invalid for type CTV3. Reason: Code does not match the expected format")); |
| 182 | + assert!(error_string.contains("Code ..j.. contents is invalid for type CTV3. Reason: Code does not match the expected format")); |
| 183 | + assert!(error_string.contains("Code 9874ji is an invalid length for type CTV3. Reason: Code is greater than 5 characters in length")); |
| 184 | + |
| 185 | + assert!( |
| 186 | + matches!(error, CodeListValidatorError::InvalidCodelist { reasons } if reasons.len() == 8) |
| 187 | + ); |
| 188 | + Ok(()) |
| 189 | + } |
| 190 | + |
| 191 | + #[test] |
| 192 | + fn test_validate_codelist_with_mixed_invalid_and_valid_codes() -> Result<(), CodeListError> { |
| 193 | + let mut codelist = create_test_codelist()?; |
| 194 | + codelist.add_entry("A54..".to_string(), None, None)?; |
| 195 | + codelist.add_entry("1009.".to_string(), None, None)?; |
| 196 | + codelist.add_entry("jk90L".to_string(), None, None)?; |
| 197 | + codelist.add_entry("LK...".to_string(), None, None)?; |
| 198 | + codelist.add_entry("N40".to_string(), None, None)?; |
| 199 | + codelist.add_entry("A00.l".to_string(), None, None)?; |
| 200 | + codelist.add_entry("Q90.....".to_string(), None, None)?; |
| 201 | + codelist.add_entry("A..9k".to_string(), None, None)?; |
| 202 | + let error = codelist.validate_codes().unwrap_err(); |
| 203 | + let error_string = error.to_string(); |
| 204 | + |
| 205 | + assert!(error_string.contains("Some codes in the list are invalid. Details:")); |
| 206 | + assert!(error_string.contains("Code N40 is an invalid length for type CTV3. Reason: Code is less than 5 characters in length")); |
| 207 | + assert!(error_string.contains("Code A00.l contents is invalid for type CTV3. Reason: Code does not match the expected format")); |
| 208 | + assert!(error_string.contains("Code Q90..... is an invalid length for type CTV3. Reason: Code is greater than 5 characters in length")); |
| 209 | + assert!(error_string.contains("Code A..9k contents is invalid for type CTV3. Reason: Code does not match the expected format")); |
| 210 | + |
| 211 | + assert!( |
| 212 | + matches!(error, CodeListValidatorError::InvalidCodelist { reasons } if reasons.len() == 4) |
| 213 | + ); |
| 214 | + Ok(()) |
| 215 | + } |
| 216 | +} |
0 commit comments