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        match Self::parse(input) {
34            Ok(r) => Some(r),
35            Err(_) => None,
36        }
37    }
38
39    /// Parse MRZ input and return a `MRZResult` or a `MRZError`.
40    ///
41    /// High-level flow:
42    /// 1. Polish input (filter nulls, uppercase, validate allowed MRZ chars).
43    /// 2. Attempt to parse with the TD1 parser if it recognises the format.
44    /// 3. Otherwise attempt TD2, then TD3.
45    /// 4. If none match, return `InvalidMRZInput`.
46    pub fn parse(input: Option<Vec<Option<String>>>) -> Result<MRZResult, MRZError> {
47        let polished = polish_input(input).ok_or_else(|| MRZError::invalid_mrz_input())?;
48
49        // Try format-specific parsers (order: TD1, TD2, TD3).
50        // Each format module is expected to provide:
51        // - `is_valid_input(&[String]) -> bool`
52        // - `parse(&[String]) -> Result<MRZResult, MRZError>`
53        //
54        // We call them via the sibling modules. If those modules are not yet
55        // implemented, these calls will fail to compile; they will be added as
56        // we continue converting files one-by-one.
57        if super::td1_format_mrz_parser::is_valid_input(&polished) {
58            return super::td1_format_mrz_parser::parse(&polished);
59        }
60
61        if super::td2_format_mrz_parser::is_valid_input(&polished) {
62            return super::td2_format_mrz_parser::parse(&polished);
63        }
64
65        if super::td3_format_mrz_parser::is_valid_input(&polished) {
66            return super::td3_format_mrz_parser::parse(&polished);
67        }
68
69        Err(MRZError::invalid_mrz_input())
70    }
71}
72
73/// Transform the raw input (Option<Vec<Option<String>>>) into a cleaned
74/// Vec<String> suitable for validation and parsing.
75///
76/// Steps:
77/// - If input is None -> return None
78/// - Remove null lines
79/// - Convert each line to ASCII uppercase
80/// - Validate each line contains only valid MRZ characters using sibling helper
81fn polish_input(input: Option<Vec<Option<String>>>) -> Option<Vec<String>> {
82    let lines = input?;
83    // Filter out None values and convert to uppercase String
84    let polished: Vec<String> = lines
85        .into_iter()
86        .filter_map(|opt| opt.map(|s| s.to_ascii_uppercase()))
87        .collect();
88
89    if polished.is_empty() {
90        return None;
91    }
92
93    // Validate every line using the sibling `mrz_string_extensions` helper.
94    // That module should provide `is_valid_mrz_input(&str) -> bool`.
95    if polished
96        .iter()
97        .any(|line| !super::string_extensions::is_valid_mrz_input(line))
98    {
99        return None;
100    }
101
102    Some(polished)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    // These tests are intentionally high-level and will require the TD
110    // format parsers to be available to fully exercise the end-to-end flow.
111    // They still validate the polishing behavior and the try_parse wrapper.
112
113    #[test]
114    fn polish_input_filters_and_upcases() {
115        let input = Some(vec![
116            Some("p<uto123".to_string()),
117            None,
118            Some("abcdef".to_string()),
119        ]);
120        // `abcdef` contains lowercase letters but is valid MRZ characters,
121        // it will be uppercased by polishing.
122        let polished = polish_input(input).expect("should polish");
123        assert_eq!(polished.len(), 2);
124        assert_eq!(polished[0], "P<UTO123");
125        assert_eq!(polished[1], "ABCDEF");
126    }
127
128    #[test]
129    fn polish_input_rejects_invalid_chars() {
130        let input = Some(vec![
131            Some("valid<MRZ".to_string()),
132            Some("has space".to_string()),
133        ]);
134        assert!(polish_input(input).is_none());
135    }
136
137    #[test]
138    fn try_parse_returns_none_on_invalid_input() {
139        // completely invalid (None) input
140        assert!(MRZParser::try_parse(None).is_none());
141
142        // invalid because polishing fails (spaces not allowed)
143        let input = Some(vec![Some("HAS SPACE".to_string())]);
144        assert!(MRZParser::try_parse(input).is_none());
145    }
146
147    // NOTE:
148    // Full parsing tests (TD1/TD2/TD3) will be added when format parsers are
149    // implemented. Here, we add a light sanity test to ensure the parse function
150    // returns the expected error variant on empty/invalid input.
151    #[test]
152    fn parse_returns_invalid_error_on_bad_input() {
153        let err = MRZParser::parse(None).unwrap_err();
154        assert_eq!(
155            format!("{}", err).contains("Invalid MRZ parser input"),
156            true
157        );
158    }
159}