Skip to main content

eduboardapi/services/
parser.rs

1use 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}