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}