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}