Skip to main content

mrz_parser/
parser.rs

1//! This module provides the `MRZParser` convenience API which chooses the
2//! correct format-specific parser (TD1 / TD2 / TD3) based on the polished input.
3//!
4//! Note: this module is exposed as `mrz_parser::parser` (with the
5//! [`MRZParser`] type re-exported at the crate root). It expects sibling
6//! modules to provide the following functions / types:
7//! - `super::exceptions::MRZError`
8//! - `super::result::MRZResult`
9//! - `super::string_extensions::is_valid_mrz_input`
10//! - `super::td1_format_mrz_parser::{is_valid_input, parse}` (and similarly for td2/td3)
11//!
12//! Those format-specific parsers should implement `is_valid_input(&[String]) -> bool`
13//! and `parse(&[String]) -> Result<MRZResult, MRZError>` to interoperate cleanly
14//! with this module.
15
16use super::exceptions::MRZError;
17use super::result::MRZResult;
18
19/// Top-level MRZ parser.
20///
21/// - `try_parse` returns `Option<MRZResult>` (`None` on error).
22/// - `parse` returns `Result<MRZResult, MRZError>` (error when input is
23///   invalid or parsing fails).
24pub struct MRZParser;
25
26impl MRZParser {
27    /// Attempt to parse the given MRZ lines, returning `Some(MRZResult)` on
28    /// success or `None` if parsing fails for any reason.
29    ///
30    /// Input accepts an optional outer vector whose elements may themselves
31    /// be optional strings (to model upstream OCR results that may be null).
32    pub fn try_parse(input: Option<Vec<Option<String>>>) -> Option<MRZResult> {
33        Self::parse(input).ok()
34    }
35
36    /// Parse MRZ input and return a `MRZResult` or a `MRZError`.
37    ///
38    /// High-level flow:
39    /// 1. Polish input (filter nulls, uppercase, validate allowed MRZ chars).
40    /// 2. Attempt to parse with the TD1 parser if it recognises the format.
41    /// 3. Otherwise attempt TD2, then TD3.
42    /// 4. If none match, return `InvalidMRZInput`.
43    pub fn parse(input: Option<Vec<Option<String>>>) -> Result<MRZResult, MRZError> {
44        let polished = polish_input(input).ok_or_else(MRZError::invalid_mrz_input)?;
45
46        // Try format-specific parsers (order: TD1, TD2, TD3).
47        // Each format module is expected to provide:
48        // - `is_valid_input(&[String]) -> bool`
49        // - `parse(&[String]) -> Result<MRZResult, MRZError>`
50        //
51        // We call them via the sibling modules. If those modules are not yet
52        // implemented, these calls will fail to compile; they will be added as
53        // we continue converting files one-by-one.
54        if super::td1_format_mrz_parser::is_valid_input(&polished) {
55            return super::td1_format_mrz_parser::parse(&polished);
56        }
57
58        if super::td2_format_mrz_parser::is_valid_input(&polished) {
59            return super::td2_format_mrz_parser::parse(&polished);
60        }
61
62        if super::td3_format_mrz_parser::is_valid_input(&polished) {
63            return super::td3_format_mrz_parser::parse(&polished);
64        }
65
66        Err(MRZError::invalid_mrz_input())
67    }
68}
69
70/// Transform the raw input (Option<Vec<Option<String>>>) into a cleaned
71/// Vec<String> suitable for validation and parsing.
72///
73/// Steps:
74/// - If input is None -> return None
75/// - Remove null lines
76/// - Convert each line to ASCII uppercase
77/// - Validate each line contains only valid MRZ characters using sibling helper
78fn polish_input(input: Option<Vec<Option<String>>>) -> Option<Vec<String>> {
79    let lines = input?;
80    // Filter out None values and convert to uppercase String
81    let polished: Vec<String> = lines
82        .into_iter()
83        .filter_map(|opt| opt.map(|s| s.to_ascii_uppercase()))
84        .collect();
85
86    if polished.is_empty() {
87        return None;
88    }
89
90    // Validate every line using the sibling `mrz_string_extensions` helper.
91    // That module should provide `is_valid_mrz_input(&str) -> bool`.
92    if polished
93        .iter()
94        .any(|line| !super::string_extensions::is_valid_mrz_input(line))
95    {
96        return None;
97    }
98
99    Some(polished)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    // These tests are intentionally high-level and will require the TD
107    // format parsers to be available to fully exercise the end-to-end flow.
108    // They still validate the polishing behavior and the try_parse wrapper.
109
110    #[test]
111    fn polish_input_filters_and_upcases() {
112        let input = Some(vec![
113            Some("p<uto123".to_string()),
114            None,
115            Some("abcdef".to_string()),
116        ]);
117        // `abcdef` contains lowercase letters but is valid MRZ characters,
118        // it will be uppercased by polishing.
119        let polished = polish_input(input).expect("should polish");
120        assert_eq!(polished.len(), 2);
121        assert_eq!(polished[0], "P<UTO123");
122        assert_eq!(polished[1], "ABCDEF");
123    }
124
125    #[test]
126    fn polish_input_rejects_invalid_chars() {
127        let input = Some(vec![
128            Some("valid<MRZ".to_string()),
129            Some("has space".to_string()),
130        ]);
131        assert!(polish_input(input).is_none());
132    }
133
134    #[test]
135    fn try_parse_returns_none_on_invalid_input() {
136        // completely invalid (None) input
137        assert!(MRZParser::try_parse(None).is_none());
138
139        // invalid because polishing fails (spaces not allowed)
140        let input = Some(vec![Some("HAS SPACE".to_string())]);
141        assert!(MRZParser::try_parse(input).is_none());
142    }
143
144    // NOTE:
145    // Full parsing tests (TD1/TD2/TD3) will be added when format parsers are
146    // implemented. Here, we add a light sanity test to ensure the parse function
147    // returns the expected error variant on empty/invalid input.
148    #[test]
149    fn parse_returns_invalid_error_on_bad_input() {
150        let err = MRZParser::parse(None).unwrap_err();
151        assert!(format!("{}", err).contains("Invalid MRZ parser input"));
152    }
153}