Skip to main content

ftracker_identifiers/isin/
error.rs

1use core::fmt;
2
3/// The class of characters permitted at a given position of an ISIN.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum CharacterClass {
6    /// An ASCII digit, `'0'...='9'` (the check digit at position 12).
7    Digit,
8    /// An uppercase ASCII letter, `'A'...='Z'` (the two-character country code).
9    Letter,
10    /// An ASCII digit or an uppercase ASCII letter, `'0'...='9' | 'A'...='Z'` (the NSIN body).
11    Alphanumeric,
12}
13
14impl fmt::Display for CharacterClass {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            CharacterClass::Digit => write!(f, "a digit (0-9)"),
18            CharacterClass::Letter => write!(f, "an uppercase letter (A-Z)"),
19            CharacterClass::Alphanumeric => {
20                write!(f, "a digit (0-9) or an uppercase letter (A-Z)")
21            }
22        }
23    }
24}
25
26/// The set of reasons an ISIN string can fail validation.
27///
28/// Every fallible constructor of [`Isin`](super::Isin) returns this type; each variant maps to a
29/// single, specific failure so callers can react programmatically (for example, highlighting the
30/// offending character in a form field) rather than parsing a human-readable message.
31#[non_exhaustive]
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum IsinError {
34    /// The input was an empty string.
35    Empty,
36
37    /// After trimming surrounding whitespace, the input did not contain exactly 12 characters.
38    InvalidLength {
39        /// The number of characters found after trimming.
40        found: usize,
41    },
42
43    /// A character outside the allowed set was found at a given position.
44    InvalidCharacter {
45        /// The offending character, as originally provided (before case folding).
46        character: char,
47        /// 1-indexed position within the 12 characters.
48        position: u8,
49        /// The character class that was expected at this position.
50        expected: CharacterClass,
51    },
52
53    /// The Luhn check digit (position 12) did not match the value computed from the first 11
54    /// characters.
55    InvalidCheckDigit {
56        /// The check digit computed by the ISO 6166 Luhn algorithm.
57        expected: u8,
58        /// The check digit actually present in the input.
59        found: u8,
60    },
61}
62
63impl fmt::Display for IsinError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            IsinError::Empty => f.write_str("ISIN input is empty"),
67            IsinError::InvalidLength { found } => {
68                write!(f, "ISIN must contain exactly 12 characters, found {found}")
69            }
70            IsinError::InvalidCharacter {
71                character,
72                position,
73                expected,
74            } => write!(
75                f,
76                "invalid character '{character}' at position {position} of 12: expected {expected}"
77            ),
78            IsinError::InvalidCheckDigit { expected, found } => write!(
79                f,
80                "invalid check digit at position 12 of 12: expected {expected}, found {found}"
81            ),
82        }
83    }
84}
85
86impl core::error::Error for IsinError {}