eduboardapi/services/
parser.rs1use crate::models::{Grade, ResultData};
2use scraper::{Html, Selector};
3use std::sync::OnceLock;
4
5fn table_selector() -> &'static Selector {
6 static TABLE_SELECTOR: OnceLock<Selector> = OnceLock::new();
7 TABLE_SELECTOR.get_or_init(|| Selector::parse("table.black12").expect("valid table selector"))
8}
9
10fn td_selector() -> &'static Selector {
11 static TD_SELECTOR: OnceLock<Selector> = OnceLock::new();
12 TD_SELECTOR.get_or_init(|| Selector::parse("td").expect("valid td selector"))
13}
14
15fn tr_selector() -> &'static Selector {
16 static TR_SELECTOR: OnceLock<Selector> = OnceLock::new();
17 TR_SELECTOR.get_or_init(|| Selector::parse("tr").expect("valid tr selector"))
18}
19
20fn cell_text(cell: scraper::ElementRef<'_>) -> String {
21 cell.text().map(str::trim).collect::<String>()
22}
23
24fn normalize_label(label: &str) -> String {
25 label
26 .chars()
27 .filter(|c| c.is_alphanumeric() || c.is_whitespace())
28 .collect::<String>()
29 .to_ascii_lowercase()
30}
31
32fn set_if_present(slot: &mut Option<String>, value: String) {
33 if !value.is_empty() {
34 *slot = Some(value);
35 }
36}
37
38pub fn parse_result(html: &str) -> crate::exceptions::AppResult<ResultData> {
39 let document = Html::parse_document(html);
40
41 let mut result = ResultData::default();
42
43 if let Some(info_table) = document.select(table_selector()).next() {
44 let mut info_iter = info_table.select(td_selector());
45 while let (Some(key_td), Some(value_td)) = (info_iter.next(), info_iter.next()) {
46 let key = cell_text(key_td);
47 let value = cell_text(value_td);
48
49 match normalize_label(&key).as_str() {
50 "roll no" | "roll" => set_if_present(&mut result.roll, value),
51 "registration no" | "reg no" | "registration" | "reg" => {
52 set_if_present(&mut result.reg, value)
53 }
54 "name" => set_if_present(&mut result.name, value),
55 "board" => set_if_present(&mut result.board, value),
56 "fathers name" => set_if_present(&mut result.father_name, value),
57 "mothers name" => set_if_present(&mut result.mother_name, value),
58 "group" => set_if_present(&mut result.group, value),
59 "type" | "exam type" => set_if_present(&mut result.exam_type, value),
60 "date of birth" | "dob" => set_if_present(&mut result.dob, value),
61 "result" => set_if_present(&mut result.result, value),
62 "institute" | "institution" => set_if_present(&mut result.institute, value),
63 "gpa" => set_if_present(&mut result.gpa, value),
64 _ => {}
65 }
66 }
67 }
68
69 let mut tables = document.select(table_selector());
70 tables.next();
71 if let Some(grade_table) = tables.next() {
72 for row in grade_table.select(tr_selector()).skip(1) {
73 let mut tds = row.select(td_selector());
74 if let (Some(code_td), Some(subject_td), Some(grade_td), None) =
75 (tds.next(), tds.next(), tds.next(), tds.next())
76 {
77 let code = cell_text(code_td);
78 let subject = cell_text(subject_td);
79 let grade = cell_text(grade_td);
80 result.grades.push(Grade {
81 code,
82 subject,
83 grade,
84 });
85 }
86 }
87 }
88
89 if !result.has_identity() {
90 return Err(crate::exceptions::AppError::Parse(
91 "Result page did not contain a valid student record".to_string(),
92 ));
93 }
94
95 Ok(result)
96}