regit_identifiers/
errors.rs1use core::fmt;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ValidationError {
47 Empty,
49 WrongLength {
51 expected: usize,
53 found: usize,
55 },
56 InvalidCharacter {
58 position: usize,
60 found: char,
62 },
63 BadCheckDigit {
67 expected: char,
69 found: char,
71 },
72 InvalidCountryCode,
74 Structure {
76 rule: &'static str,
78 },
79}
80
81impl fmt::Display for ValidationError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Empty => write!(f, "input string is empty"),
85 Self::WrongLength { expected, found } => {
86 write!(f, "wrong length: expected {expected}, found {found}")
87 }
88 Self::InvalidCharacter { position, found } => {
89 write!(f, "invalid character '{found}' at position {position}")
90 }
91 Self::BadCheckDigit { expected, found } => {
92 write!(
93 f,
94 "check digit mismatch: expected '{expected}', found '{found}'"
95 )
96 }
97 Self::InvalidCountryCode => write!(f, "unrecognised country code"),
98 Self::Structure { rule } => write!(f, "structural rule violated: {rule}"),
99 }
100 }
101}
102
103impl core::error::Error for ValidationError {}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ConversionError {
125 UnsupportedCountry,
128 NotConvertible {
131 reason: &'static str,
133 },
134 Validation(ValidationError),
136}
137
138impl fmt::Display for ConversionError {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::UnsupportedCountry => {
142 write!(
143 f,
144 "source country has no defined target for this conversion"
145 )
146 }
147 Self::NotConvertible { reason } => {
148 write!(f, "value is not convertible: {reason}")
149 }
150 Self::Validation(e) => write!(f, "converted identifier is invalid: {e}"),
151 }
152 }
153}
154
155impl core::error::Error for ConversionError {
156 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
157 match self {
158 Self::Validation(e) => Some(e),
159 _ => None,
160 }
161 }
162}
163
164impl From<ValidationError> for ConversionError {
165 fn from(e: ValidationError) -> Self {
166 Self::Validation(e)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::test_support::{debug, display};
174
175 #[test]
176 fn validation_error_display_empty() {
177 assert_eq!(
178 display(ValidationError::Empty).as_str(),
179 "input string is empty"
180 );
181 }
182
183 #[test]
184 fn validation_error_display_wrong_length() {
185 let err = ValidationError::WrongLength {
186 expected: 12,
187 found: 11,
188 };
189 assert_eq!(display(err).as_str(), "wrong length: expected 12, found 11");
190 }
191
192 #[test]
193 fn validation_error_display_invalid_character() {
194 let err = ValidationError::InvalidCharacter {
195 position: 3,
196 found: '/',
197 };
198 assert_eq!(display(err).as_str(), "invalid character '/' at position 3");
199 }
200
201 #[test]
202 fn validation_error_display_bad_check_digit() {
203 let err = ValidationError::BadCheckDigit {
204 expected: '5',
205 found: '4',
206 };
207 assert_eq!(
208 display(err).as_str(),
209 "check digit mismatch: expected '5', found '4'"
210 );
211 }
212
213 #[test]
214 fn validation_error_display_country_and_structure() {
215 assert_eq!(
216 display(ValidationError::InvalidCountryCode).as_str(),
217 "unrecognised country code"
218 );
219 assert_eq!(
220 display(ValidationError::Structure {
221 rule: "BIC length must be 8 or 11",
222 })
223 .as_str(),
224 "structural rule violated: BIC length must be 8 or 11"
225 );
226 }
227
228 #[test]
229 fn validation_error_display_has_no_trailing_period() {
230 for err in [
231 ValidationError::Empty,
232 ValidationError::WrongLength {
233 expected: 1,
234 found: 2,
235 },
236 ValidationError::InvalidCharacter {
237 position: 1,
238 found: 'x',
239 },
240 ValidationError::BadCheckDigit {
241 expected: '0',
242 found: '1',
243 },
244 ValidationError::InvalidCountryCode,
245 ValidationError::Structure { rule: "r" },
246 ] {
247 assert!(!display(err).as_str().ends_with('.'));
248 }
249 }
250
251 #[test]
252 fn validation_error_is_error_trait() {
253 let err: &dyn core::error::Error = &ValidationError::Empty;
254 assert!(err.source().is_none());
255 }
256
257 #[test]
258 fn validation_error_copy_eq() {
259 let err = ValidationError::InvalidCountryCode;
260 let copy = err;
261 assert_eq!(err, copy);
262 }
263
264 #[test]
265 fn conversion_error_display() {
266 assert_eq!(
267 display(ConversionError::UnsupportedCountry).as_str(),
268 "source country has no defined target for this conversion"
269 );
270 assert!(
271 display(ConversionError::NotConvertible {
272 reason: "leading 00 missing",
273 })
274 .as_str()
275 .contains("leading 00 missing")
276 );
277 assert!(
278 display(ConversionError::Validation(ValidationError::Empty))
279 .as_str()
280 .contains("empty")
281 );
282 }
283
284 #[test]
285 fn conversion_error_from_validation_and_source() {
286 let ve = ValidationError::WrongLength {
287 expected: 9,
288 found: 8,
289 };
290 let ce: ConversionError = ve.into();
291 assert!(matches!(ce, ConversionError::Validation(_)));
292 let dyn_err: &dyn core::error::Error = &ce;
293 assert!(dyn_err.source().is_some());
294
295 let no_src: &dyn core::error::Error = &ConversionError::UnsupportedCountry;
296 assert!(no_src.source().is_none());
297 }
298
299 #[test]
300 fn errors_debug() {
301 assert!(debug(ValidationError::Empty).as_str().contains("Empty"));
302 assert!(
303 debug(ConversionError::UnsupportedCountry)
304 .as_str()
305 .contains("UnsupportedCountry")
306 );
307 }
308}