1use std::collections::HashMap;
2use std::sync::OnceLock;
3
4use serde_json::Value;
5
6use crate::field_spec::{FieldType, LoadedSpec, PrefixType};
7
8#[derive(Debug, Clone)]
9pub struct ValidationError {
10 pub de: u32,
11 pub field_name: String,
12 pub rule: String,
13 pub message: String,
14}
15
16#[derive(Debug)]
17pub struct ValidationResult {
18 pub errors: Vec<ValidationError>,
19}
20
21impl ValidationResult {
22 pub fn is_valid(&self) -> bool {
23 self.errors.is_empty()
24 }
25}
26
27fn response_codes() -> &'static HashMap<String, String> {
28 static CODES: OnceLock<HashMap<String, String>> = OnceLock::new();
29 CODES.get_or_init(|| {
30 let json = include_str!("../specifications/response_codes.json");
31 serde_json::from_str(json).expect("invalid response_codes.json")
32 })
33}
34
35pub fn lookup_response_code(code: &str) -> Option<&'static str> {
36 response_codes().get(code).map(|s| s.as_str())
37}
38
39pub fn validate(message: &Value, spec: &LoadedSpec) -> ValidationResult {
40 let mut errors = Vec::new();
41
42 let mti = match message.get("mti").and_then(|v| v.as_str()) {
44 Some(m) => m,
45 None => {
46 errors.push(ValidationError {
47 de: 0,
48 field_name: "mti".to_string(),
49 rule: "mti".to_string(),
50 message: "MTI is missing".to_string(),
51 });
52 return ValidationResult { errors };
53 }
54 };
55
56 if mti.len() != 4 || !mti.chars().all(|c| c.is_ascii_digit()) {
57 errors.push(ValidationError {
58 de: 0,
59 field_name: "mti".to_string(),
60 rule: "mti".to_string(),
61 message: format!("MTI '{}' must be exactly 4 digits", mti),
62 });
63 } else {
64 let chars: Vec<char> = mti.chars().collect();
65 let version = chars[0];
66 let class = chars[1];
67 let function = chars[2];
68
69 if !['0', '1', '2', '9'].contains(&version) {
70 errors.push(ValidationError {
71 de: 0,
72 field_name: "mti".to_string(),
73 rule: "mti".to_string(),
74 message: format!("MTI version digit '{}' is invalid", version),
75 });
76 }
77 if !['1', '2', '3', '4', '5', '6', '7', '8'].contains(&class) {
78 errors.push(ValidationError {
79 de: 0,
80 field_name: "mti".to_string(),
81 rule: "mti".to_string(),
82 message: format!("MTI message class digit '{}' is invalid", class),
83 });
84 }
85 if !['0', '1', '2', '3', '4', '5'].contains(&function) {
86 errors.push(ValidationError {
87 de: 0,
88 field_name: "mti".to_string(),
89 rule: "mti".to_string(),
90 message: format!("MTI function digit '{}' is invalid", function),
91 });
92 }
93 }
94
95 if let Some(mandatory) = spec.mti_rules.get(mti)
97 && let Some(fields_obj) = message.get("fields").and_then(|v| v.as_object())
98 {
99 for &de_num in mandatory {
100 let prefix = format!("de{:03}_", de_num);
101 let found = fields_obj.keys().any(|k| k.starts_with(&prefix));
102 if !found {
103 let field_name = spec
104 .get_field(de_num)
105 .map(|f| f.name.clone())
106 .unwrap_or_else(|| format!("de{:03}", de_num));
107 errors.push(ValidationError {
108 de: de_num,
109 field_name,
110 rule: "mandatory".to_string(),
111 message: format!("DE {} is mandatory for MTI {}", de_num, mti),
112 });
113 }
114 }
115 }
116
117 if let Some(fields_obj) = message.get("fields").and_then(|v| v.as_object()) {
119 for (key, value) in fields_obj {
120 let de_num = match parse_de_number(key) {
121 Some(n) => n,
122 None => continue,
123 };
124
125 let field_spec = match spec.get_field(de_num) {
126 Some(f) => f,
127 None => continue,
128 };
129
130 if value.is_object() {
132 continue;
133 }
134
135 let val_str = match value.as_str() {
136 Some(s) => s,
137 None => continue,
138 };
139
140 match field_spec.field_type {
142 FieldType::Numeric => {
143 if !val_str.chars().all(|c| c.is_ascii_digit()) {
144 errors.push(ValidationError {
145 de: de_num,
146 field_name: field_spec.name.clone(),
147 rule: "format".to_string(),
148 message: format!(
149 "DE {} value '{}' must contain only digits",
150 de_num, val_str
151 ),
152 });
153 }
154 }
155 FieldType::Binary => {
156 if !val_str.chars().all(|c| c.is_ascii_hexdigit()) {
157 errors.push(ValidationError {
158 de: de_num,
159 field_name: field_spec.name.clone(),
160 rule: "format".to_string(),
161 message: format!(
162 "DE {} value '{}' must contain only hex characters",
163 de_num, val_str
164 ),
165 });
166 }
167 }
168 FieldType::XNumeric => {
169 if val_str.is_empty() {
170 errors.push(ValidationError {
171 de: de_num,
172 field_name: field_spec.name.clone(),
173 rule: "format".to_string(),
174 message: format!("DE {} x+n value must not be empty", de_num),
175 });
176 } else {
177 let first = val_str.chars().next().unwrap();
178 if first != 'C' && first != 'D' {
179 errors.push(ValidationError {
180 de: de_num,
181 field_name: field_spec.name.clone(),
182 rule: "format".to_string(),
183 message: format!(
184 "DE {} x+n value '{}' must start with C or D",
185 de_num, val_str
186 ),
187 });
188 }
189 if !val_str[1..].chars().all(|c| c.is_ascii_digit()) {
190 errors.push(ValidationError {
191 de: de_num,
192 field_name: field_spec.name.clone(),
193 rule: "format".to_string(),
194 message: format!(
195 "DE {} x+n value '{}' digits portion must be all digits",
196 de_num, val_str
197 ),
198 });
199 }
200 }
201 }
202 _ => {}
203 }
204
205 match field_spec.prefix {
207 PrefixType::Fixed => {
208 if val_str.len() != field_spec.length {
209 errors.push(ValidationError {
210 de: de_num,
211 field_name: field_spec.name.clone(),
212 rule: "length".to_string(),
213 message: format!(
214 "DE {} length {} does not match fixed length {}",
215 de_num,
216 val_str.len(),
217 field_spec.length
218 ),
219 });
220 }
221 }
222 _ => {
223 if val_str.len() > field_spec.length {
224 errors.push(ValidationError {
225 de: de_num,
226 field_name: field_spec.name.clone(),
227 rule: "length".to_string(),
228 message: format!(
229 "DE {} length {} exceeds maximum length {}",
230 de_num,
231 val_str.len(),
232 field_spec.length
233 ),
234 });
235 }
236 }
237 }
238
239 if let Some(ref validation) = field_spec.validation
241 && let Some(ref pattern) = validation.pattern
242 && let Some(err) = validate_pattern(de_num, &field_spec.name, val_str, pattern)
243 {
244 errors.push(err);
245 }
246
247 if de_num == 39 && lookup_response_code(val_str).is_none() {
249 errors.push(ValidationError {
250 de: 39,
251 field_name: field_spec.name.clone(),
252 rule: "response_code".to_string(),
253 message: format!("DE 39 response code '{}' is not recognized", val_str),
254 });
255 }
256 }
257 }
258
259 ValidationResult { errors }
260}
261
262fn parse_de_number(key: &str) -> Option<u32> {
263 if let Some(num_part) = key.strip_prefix("de") {
264 let end = num_part.find('_').unwrap_or(num_part.len());
265 num_part[..end].parse().ok()
266 } else {
267 None
268 }
269}
270
271fn validate_pattern(
272 de: u32,
273 field_name: &str,
274 value: &str,
275 pattern: &str,
276) -> Option<ValidationError> {
277 match pattern {
278 "MMDDhhmmss" => {
279 if value.len() != 10 {
280 return Some(ValidationError {
281 de,
282 field_name: field_name.to_string(),
283 rule: "pattern".to_string(),
284 message: format!(
285 "DE {} value '{}' must be 10 digits for MMDDhhmmss",
286 de, value
287 ),
288 });
289 }
290 let mm: u32 = value[0..2].parse().unwrap_or(0);
291 let dd: u32 = value[2..4].parse().unwrap_or(0);
292 let hh: u32 = value[4..6].parse().unwrap_or(0);
293 let mi: u32 = value[6..8].parse().unwrap_or(0);
294 let ss: u32 = value[8..10].parse().unwrap_or(0);
295 if !(1..=12).contains(&mm) || !(1..=31).contains(&dd) || hh > 23 || mi > 59 || ss > 59 {
296 return Some(ValidationError {
297 de,
298 field_name: field_name.to_string(),
299 rule: "pattern".to_string(),
300 message: format!(
301 "DE {} value '{}' has invalid date/time components for MMDDhhmmss",
302 de, value
303 ),
304 });
305 }
306 }
307 "hhmmss" => {
308 if value.len() != 6 {
309 return Some(ValidationError {
310 de,
311 field_name: field_name.to_string(),
312 rule: "pattern".to_string(),
313 message: format!("DE {} value '{}' must be 6 digits for hhmmss", de, value),
314 });
315 }
316 let hh: u32 = value[0..2].parse().unwrap_or(99);
317 let mi: u32 = value[2..4].parse().unwrap_or(99);
318 let ss: u32 = value[4..6].parse().unwrap_or(99);
319 if hh > 23 || mi > 59 || ss > 59 {
320 return Some(ValidationError {
321 de,
322 field_name: field_name.to_string(),
323 rule: "pattern".to_string(),
324 message: format!(
325 "DE {} value '{}' has invalid time components for hhmmss",
326 de, value
327 ),
328 });
329 }
330 }
331 "MMDD" => {
332 if value.len() != 4 {
333 return Some(ValidationError {
334 de,
335 field_name: field_name.to_string(),
336 rule: "pattern".to_string(),
337 message: format!("DE {} value '{}' must be 4 digits for MMDD", de, value),
338 });
339 }
340 let mm: u32 = value[0..2].parse().unwrap_or(0);
341 let dd: u32 = value[2..4].parse().unwrap_or(0);
342 if !(1..=12).contains(&mm) || !(1..=31).contains(&dd) {
343 return Some(ValidationError {
344 de,
345 field_name: field_name.to_string(),
346 rule: "pattern".to_string(),
347 message: format!(
348 "DE {} value '{}' has invalid date components for MMDD",
349 de, value
350 ),
351 });
352 }
353 }
354 "YYMM" => {
355 if value.len() != 4 {
356 return Some(ValidationError {
357 de,
358 field_name: field_name.to_string(),
359 rule: "pattern".to_string(),
360 message: format!("DE {} value '{}' must be 4 digits for YYMM", de, value),
361 });
362 }
363 let mm: u32 = value[2..4].parse().unwrap_or(0);
364 if !(1..=12).contains(&mm) {
365 return Some(ValidationError {
366 de,
367 field_name: field_name.to_string(),
368 rule: "pattern".to_string(),
369 message: format!(
370 "DE {} value '{}' has invalid month component for YYMM",
371 de, value
372 ),
373 });
374 }
375 }
376 _ => {}
377 }
378 None
379}