1use crate::util;
2
3#[derive(Default, Debug)]
5pub struct Pnr<'a> {
6 pub infos: Option<Vec<&'a str>>,
7 pub ssr_items: Option<Vec<SSR<'a>>>,
8 pub osi_items: Option<Vec<OSI<'a>>>,
9 pub seg_items: Option<Vec<SEG<'a>>>,
10 pub nm_items: Option<Vec<NM<'a>>>,
11 pub rmk_items: Option<Vec<RMK<'a>>>,
12 pub other_items: Option<Vec<OtherItem<'a>>>,
13 pub is_ticket_pnr: Option<bool>,
14 pub is_cancelled_pnr: Option<bool>,
15 pub is_group_pnr: Option<bool>,
16 pub group_pnr_name: Option<&'a str>,
17 pub pax_count: Option<u8>,
18 pub pnr_code: Option<&'a str>,
19 pub bpnr_code: Option<&'a str>,
20 pub has_married_segment: Option<bool>,
21 pub office_no: Option<&'a str>,
22}
23
24impl<'a> Pnr<'a> {
25 pub fn parse(text: &'a str) -> anyhow::Result<Self> {
26 if text.is_empty() {
27 return Err(anyhow::Error::msg(
28 "pnr parameter shouldn't be empty.".to_owned(),
29 ));
30 }
31 let mut pnr = Self {
32 ..Default::default()
33 };
34
35 let mut info_parsed = false;
36 let mut index = 0u8;
37 for line in text.lines() {
38 if !info_parsed && line.starts_with(" 1.") {
39 info_parsed = true;
40 }
41 if !info_parsed {
42 if line.contains("**ELECTRONIC TICKET PNR**") {
43 pnr.is_ticket_pnr = Some(true);
44 }
45 if line.contains("*THIS PNR WAS ENTIRELY CANCELLED*") {
46 pnr.is_cancelled_pnr = Some(true);
47 }
48 if line.contains("MARRIED SEGMENT EXIST IN THE PNR") {
49 pnr.has_married_segment = Some(true);
50 }
51 pnr.infos.get_or_insert(Vec::new()).push(line);
52 } else {
53 match line.trim_start() {
54 x if x.starts_with("1.") => {
55 let re = regex::Regex::new(r"(?<NMS>1\.(.*))(?<PNRCODE>\w{6})\s*$")?;
56 match re.captures(line) {
57 Some(caps) => match (caps.name("NMS"), caps.name("PNRCODE")) {
58 (Some(nms), Some(pnrcode)) => {
59 pnr.pnr_code = Some(pnrcode.as_str());
60 if let Ok(items) = NM::parse(index, nms.as_str()) {
61 pnr.nm_items = Some(items);
62 }
63 }
64 _ => {}
65 },
66 _ => {}
67 }
68 }
69 x if regex::Regex::is_match(
70 ®ex::Regex::new(
71 r"(?<GROUPPNRNAME>.*)\s*NM(?<PAXCOUNT>\d+)\s+(?<PNRCODE>\w{6})/(\w{2})",
72 )?,
73 x,
74 ) =>
75 {
76 pnr.is_group_pnr = Some(true);
77 if let Some(caps) = regex::Regex::captures(
78 ®ex::Regex::new(
79 r"(?<GROUPPNRNAME>.*)\s*NM(?<PAXCOUNT>\d+)\s+(?<PNRCODE>\w{6})/(\w{2})",
80 )?,
81 x,
82 ) {
83 match (
84 caps.name("GROUPPNRNAME"),
85 caps.name("PAXCOUNT"),
86 caps.name("PNRCODE"),
87 ) {
88 (Some(pnr_groupname), Some(paxcount), Some(pnrcode)) => {
89 pnr.group_pnr_name = Some(pnr_groupname.as_str());
90 pnr.pnr_code = Some(pnrcode.as_str());
91 pnr.pax_count = paxcount.as_str().parse::<u8>().ok();
92 }
93 _ => {}
94 }
95 }
96 }
97 x if x.starts_with(&format!("{}. ", index)) => {
98 if let Ok(item) = SEG::parse(index, line) {
99 pnr.seg_items.get_or_insert(Vec::new()).push(item);
100 }
101 }
102 x if x.starts_with(&format!("{}SSR", index)) => {
103 if let Ok(item) = SSR::parse(index, line) {
104 pnr.ssr_items.get_or_insert(Vec::new()).push(item);
105 }
106 }
107 x if x.starts_with(&format!("{}OSI", index)) => {
108 if let Ok(item) = OSI::parse(index, line) {
109 pnr.osi_items.get_or_insert(Vec::new()).push(item);
110 }
111 }
112 x if x.starts_with(&format!("{}RMK", index)) => {
113 if let Ok(item) = RMK::parse(index, line) {
114 pnr.rmk_items.get_or_insert(Vec::new()).push(item);
115 }
116 }
117 _ => {
118 if let Ok(item) = OtherItem::parse(index, line) {
119 pnr.other_items.get_or_insert(Vec::new()).push(item);
120 }
121 }
122 }
123 }
124 index += 1;
125 }
126 Self::fix_nm(&mut pnr);
127 if pnr.pax_count.is_none() {
128 pnr.pax_count = pnr.nm_items.as_ref().and_then(|x| Some(x.len() as u8));
129 }
130 pnr.office_no = pnr.other_items.as_ref().and_then(|x| {
131 x.iter().find_map(|n| {
132 if n.item_type == "OFFICE" {
133 Some(n.raw.trim())
134 } else {
135 None
136 }
137 })
138 });
139 pnr.bpnr_code = pnr.rmk_items.as_ref().and_then(|x| {
140 x.iter().find_map(|n| {
141 if n.service_code.is_some_and(|s| s == "CA") {
142 n.text.as_ref().and_then(|k| Some(&k[0..6]))
143 } else {
144 None
145 }
146 })
147 });
148 Ok(pnr)
149 }
150
151 fn fix_nm(pnr: &mut Pnr) {
153 match (&pnr.ssr_items, &mut pnr.nm_items) {
154 (Some(ssrs), Some(nms)) => {
155 nms.iter_mut().for_each(|x| {
156 if let Some(ssr) = ssrs.iter().find(|s| {
157 s.passenger_index.is_some_and(|n| n == x.index)
158 && s.service_code.is_some_and(|n| n == "FOID")
159 }) {
160 if let Some(tx) = &ssr.text {
161 x.id_type = Some(&tx[0..2]);
162 x.id_number = Some(&tx[2..]);
163 }
164 }
165 });
166 }
167 _ => {}
168 }
169 }
170}
171
172#[derive(Default, Debug)]
174pub struct OtherItem<'a> {
175 pub index: u8,
176 pub item_type: &'a str,
177 pub raw: &'a str,
178}
179
180impl<'a> OtherItem<'a> {
181 pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
182 let item_type = match raw {
183 x if x.starts_with(&format!("{}TL", index)) => "TL",
184 x if x.starts_with(&format!("{}FN", index)) => "FN",
185 x if x.starts_with(&format!("{}FC", index)) => "FC",
186 x if x.starts_with(&format!("{}FP", index)) => "FP",
187 x if x.starts_with(&format!("{}EI", index)) => "EI",
188 x if x.starts_with(&format!("{}XN", index)) => "XN",
189 x if x.starts_with(&format!("{}TC", index)) => "TC",
190 x if x.starts_with(&format!("{}TN/", index)) => "TN",
191 x if regex::Regex::is_match(®ex::Regex::new(r"^\d{2}\.[A-Z]{3}\d{3}$")?, x) => {
192 "OFFICE"
193 }
194 _ => "TEXT",
195 };
196 Ok(Self {
197 index,
198 item_type: item_type,
199 raw: raw,
200 })
201 }
202}
203
204#[derive(Default, Debug)]
206pub struct NM<'a> {
207 pub index: u8,
208 pub raw: &'a str,
209 pub name: Option<&'a str>,
210 pub id_number: Option<&'a str>,
213 pub id_type: Option<&'a str>,
214}
215
216impl<'a> NM<'a> {
217 pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Vec<Self>> {
218 let re = regex::Regex::new(r"(\d+\.)")?;
219 let nms = re
220 .split(raw)
221 .filter_map(|cap| match cap.trim() {
222 x if x.is_empty() => None,
223 x if x.ends_with(".") => None,
224 x => Some(Self {
225 index,
226 raw: cap.trim(),
227 name: Some(x),
228 ..Default::default()
229 }),
230 })
231 .collect::<Vec<_>>();
232 Ok(nms)
233 }
234}
235
236#[derive(Default, Debug)]
238pub struct SEG<'a> {
239 pub index: u8,
240 pub raw: &'a str,
241 pub org: Option<&'a str>,
242 pub dst: Option<&'a str>,
243 pub seat_class: Option<&'a str>,
244 pub flight_date: Option<&'a str>,
245 pub takeoff: Option<&'a str>,
246 pub landing: Option<&'a str>,
247 pub landing_addday: Option<u8>,
248 pub action_code: Option<&'a str>,
249 pub action_code_qty: Option<u8>,
250 pub flight_no: Option<&'a str>,
251 pub is_share: Option<bool>,
252}
253
254impl<'a> SEG<'a> {
255 pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
256 let re = regex::Regex::new(
257 r"(?<FLIGHTNO>\*?\w{5,6})\s+(?<SEATCLASS>[A-Z]\d?)\s+[A-Z]{2}(?<FLIGHTDATE>\d{2}[A-Z]{3}(?:\d{2})?)\s*(?<ORG>[A-Z]{3})(?<DST>[A-Z]{3})\s*(?<ACTIONCODE>[A-Z]{2})(?<ACTIONCODEQTY>\d{1,2})\s*(?<DEPTIME>\d{4})\s*(?<ARRTIME>\d{4})(?:\+(?<ADDDAY>\d))?",
258 )?;
259 match re.captures(raw) {
260 Some(caps) => match (
261 caps.name("FLIGHTNO"),
262 caps.name("SEATCLASS"),
263 caps.name("FLIGHTDATE"),
264 caps.name("ORG"),
265 caps.name("DST"),
266 caps.name("ACTIONCODE"),
267 caps.name("ACTIONCODEQTY"),
268 caps.name("DEPTIME"),
269 caps.name("ARRTIME"),
270 caps.name("ADDDAY"),
271 ) {
272 (
273 Some(flight_no),
274 Some(seat_class),
275 Some(flight_date),
276 Some(org),
277 Some(dst),
278 Some(action_code),
279 Some(action_code_qty),
280 Some(takeoff),
281 Some(landing),
282 addday,
283 ) => Ok(Self {
284 index,
285 raw: raw,
286 flight_no: Some(flight_no.as_str()),
287 seat_class: Some(seat_class.as_str()),
288 flight_date: Some(flight_date.as_str()),
289 org: Some(org.as_str()),
290 dst: Some(dst.as_str()),
291 action_code: Some(action_code.as_str()),
292 action_code_qty: action_code_qty.as_str().parse::<u8>().ok(), takeoff: Some(takeoff.as_str()),
294 landing: Some(landing.as_str()),
295 landing_addday: util::regex_extact_value::<u8>(addday), is_share: Some(flight_no.as_str().starts_with("*")),
297 }),
298 _ => Ok(Self {
299 index,
300 raw: raw,
301 ..Default::default()
302 }),
303 },
304 _ => Ok(Self {
305 index,
306 raw: raw,
307 ..Default::default()
308 }),
309 }
310 }
311}
312
313#[derive(Default, Debug)]
315pub struct SSR<'a> {
316 pub index: u8,
317 pub raw: &'a str,
318 pub service_code: Option<&'a str>,
319 pub action_code: Option<&'a str>,
320 pub action_code_qty: Option<u8>,
321 pub airline: Option<&'a str>,
322 pub text: Option<&'a str>,
323 pub passenger_index: Option<u8>,
324 pub segment_index: Option<u8>,
325}
326
327impl<'a> SSR<'a> {
328 pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
329 let re = regex::Regex::new(
330 r"SSR (?<SERVICECODE>[A-Z]+) (?<AIRLINE>\w{2}) (?:(?<ACTIONCODE>\w{2})(?<ACTIONCODEQTY>\d|/+)?\s+)?(?<TEXT>[^\r\n]*(?<!/P\d+)(?<!/S\d+))(/P(?<PASSENGERINDEX>\d+))?(/S(?<SEGMENTINDEX>\d+))?\s*$",
331 )?;
332 match re.captures(raw) {
333 Some(caps) => match (
334 caps.name("SERVICECODE"),
335 caps.name("AIRLINE"),
336 caps.name("ACTIONCODE"),
337 caps.name("ACTIONCODEQTY"),
338 caps.name("TEXT"),
339 caps.name("PASSENGERINDEX"),
340 caps.name("SEGMENTINDEX"),
341 ) {
342 (
343 Some(service_code),
344 Some(airline),
345 action_code,
346 action_code_qty,
347 Some(text),
348 passenger_index,
349 segment_index,
350 ) => Ok(Self {
351 index,
352 raw: raw,
353 service_code: Some(service_code.as_str()),
354 airline: Some(airline.as_str()),
355 action_code: action_code.map(|x| x.as_str()),
356 action_code_qty: util::regex_extact_value::<u8>(action_code_qty),
357 text: Some(text.as_str()),
358 passenger_index: util::regex_extact_value::<u8>(passenger_index),
359 segment_index: util::regex_extact_value::<u8>(segment_index),
360 }),
361 _ => Ok(Self {
362 index,
363 raw: raw,
364 ..Default::default()
365 }),
366 },
367 _ => Ok(Self {
368 index,
369 raw: raw,
370 ..Default::default()
371 }),
372 }
373 }
374}
375
376#[derive(Default, Debug)]
378pub struct OSI<'a> {
379 pub index: u8,
380 pub raw: &'a str,
381 pub service_code: Option<&'a str>,
382 pub airline: Option<&'a str>,
383 pub text: Option<&'a str>,
384 pub passenger_index: Option<u8>,
385}
386
387impl<'a> OSI<'a> {
388 pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
389 let re = regex::Regex::new(
390 r"OSI (?<AIRLINE>\w{2}) (?<SERVICECODE>[A-Z]+)?(?<TEXT>(.*(?<!/P\d+)))(/P(?<PASSENGERINDEX>\d+))?$",
391 )?;
392 match re.captures(raw) {
393 Some(caps) => match (
394 caps.name("AIRLINE"),
395 caps.name("SERVICECODE"),
396 caps.name("TEXT"),
397 caps.name("PASSENGERINDEX"),
398 ) {
399 (Some(airline), Some(service_code), Some(text), passenger_index) => Ok(Self {
400 index,
401 raw: raw,
402 service_code: Some(service_code.as_str()),
403 airline: Some(airline.as_str()),
404 text: Some(text.as_str()),
405 passenger_index: util::regex_extact_value::<u8>(passenger_index), }),
407 _ => Ok(Self {
408 index,
409 raw: raw,
410 ..Default::default()
411 }),
412 },
413 _ => Ok(Self {
414 index,
415 raw: raw,
416 ..Default::default()
417 }),
418 }
419 }
420}
421
422#[derive(Default, Debug)]
424pub struct RMK<'a> {
425 pub index: u8,
426 pub raw: &'a str,
427 pub service_code: Option<&'a str>,
428 pub text: Option<&'a str>,
429 pub passenger_index: Option<u8>,
430}
431
432impl<'a> RMK<'a> {
433 pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
434 let re = regex::Regex::new(
435 r"RMK[ :/](?<SERVICECODE>(MP|TJ AUTH|CA|CID|TID|EMAIL|1A|GMJC|RV|ORI))[ :/](?<TEXT>.*?)(/P(?<PASSENGERINDEX>\d))?$",
436 )?;
437 match re.captures(raw) {
438 Some(caps) => match (
439 caps.name("SERVICECODE"),
440 caps.name("TEXT"),
441 caps.name("PASSENGERINDEX"),
442 ) {
443 (Some(service_code), text, passenger_index) => Ok(Self {
444 index,
445 raw: raw,
446 service_code: Some(service_code.as_str()),
447 text: util::regex_extact_text(text),
448 passenger_index: util::regex_extact_value::<u8>(passenger_index),
449 }),
450 _ => Ok(Self {
451 index,
452 raw: raw,
453 ..Default::default()
454 }),
455 },
456 _ => Ok(Self {
457 index,
458 raw: raw,
459 ..Default::default()
460 }),
461 }
462 }
463}