iqps_backend/
qp.rs

1//! Utils for parsing question paper details
2
3use color_eyre::eyre::eyre;
4use duplicate::duplicate_item;
5use serde::Deserialize;
6use serde::Serialize;
7
8use crate::env::EnvVars;
9
10#[derive(Clone, Copy)]
11/// Represents a semester.
12///
13/// It can be parsed from a [`String`] using the `.try_from()` function. An error will be returned if the given string has an invalid value.
14///
15/// This value can be converted back into a [`String`] using the [`From`] trait implementation.
16pub enum Semester {
17    /// Autumn semester, parsed from `autumn`
18    Autumn,
19    /// Spring semester, parsed from `spring`
20    Spring,
21    /// Unknown/wildcard semester, parsed from an empty string.
22    ///
23    /// Note that this is different from an invalid value and is used to represent papers for which the semester is not known. An invalid value would be `puppy` or `Hippopotomonstrosesquippedaliophobia` for example.
24    Unknown,
25}
26
27impl TryFrom<&String> for Semester {
28    type Error = color_eyre::eyre::Error;
29
30    fn try_from(value: &String) -> Result<Self, Self::Error> {
31        if value == "autumn" {
32            Ok(Semester::Autumn)
33        } else if value == "spring" {
34            Ok(Semester::Spring)
35        } else if value.is_empty() {
36            Ok(Semester::Unknown)
37        } else {
38            Err(eyre!("Error parsing semester: Invalid value."))
39        }
40    }
41}
42
43impl From<Semester> for String {
44    fn from(value: Semester) -> Self {
45        match value {
46            Semester::Autumn => "autumn".into(),
47            Semester::Spring => "spring".into(),
48            Semester::Unknown => "".into(),
49        }
50    }
51}
52
53#[derive(Clone, Copy)]
54/// Represents the exam type of the paper.
55///
56/// Can be converted to and parsed from a String using the [`From`] and [`TryFrom`] trait implementations.
57pub enum Exam {
58    /// Mid-semester examination, parsed from `midsem`
59    Midsem,
60    /// End-semester examination, parsed from `endsem`
61    Endsem,
62    /// Class test, parsed from either `ct` or `ct` followed by a number (eg: `ct1` or `ct10`).
63    ///
64    /// The optional number represents the number of the class test (eg: class test 1 or class test 21). This will be None if the number is not known, parsed from `ct`.
65    CT(Option<usize>),
66    /// Unknown class test, parsed from an empty string.
67    ///
68    /// Note that this is different from an invalid value and is used to represent papers for which the exam is not known. An invalid value would be `catto` or `metakgp` for example.
69    Unknown,
70}
71
72impl TryFrom<&String> for Exam {
73    type Error = color_eyre::eyre::Error;
74
75    fn try_from(value: &String) -> Result<Self, Self::Error> {
76        if value == "midsem" {
77            Ok(Exam::Midsem)
78        } else if value == "endsem" {
79            Ok(Exam::Endsem)
80        } else if let Some(stripped) = value.strip_prefix("ct") {
81            if stripped.is_empty() {
82                Ok(Exam::CT(None))
83            } else if let Ok(i) = stripped.parse::<usize>() {
84                Ok(Exam::CT(Some(i)))
85            } else {
86                Err(eyre!("Error parsing exam: Invalid class test number."))
87            }
88        } else if value.is_empty() {
89            Ok(Exam::Unknown)
90        } else {
91            Err(eyre!("Error parsing exam: Unknown exam type."))
92        }
93    }
94}
95
96impl From<Exam> for String {
97    fn from(value: Exam) -> Self {
98        match value {
99            Exam::Midsem => "midsem".into(),
100            Exam::Endsem => "endsem".into(),
101            Exam::Unknown => "".into(),
102            Exam::CT(None) => "ct".into(),
103            Exam::CT(Some(i)) => format!("ct{}", i),
104        }
105    }
106}
107
108#[duplicate_item(
109    Serializable;
110    [ Exam ];
111    [ Semester ];
112)]
113impl Serialize for Serializable {
114    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: serde::Serializer,
117    {
118        serializer.serialize_str(&String::from(*self))
119    }
120}
121
122pub trait WithUrl: Sized {
123    /// Returns the question paper with the full static files URL in the `filelink` field instead of just the slug. See the [`crate::pathutils`] module for what a slug is.
124    fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error>;
125}
126
127#[derive(Deserialize)]
128/// The details for a question paper in the library
129pub struct LibraryQP {
130    pub course_code: String,
131    pub course_name: String,
132    pub year: i32,
133    pub exam: String,
134    pub semester: String,
135    #[allow(dead_code)]
136    pub filename: String,
137    pub approve_status: bool,
138}
139
140#[derive(Serialize, Clone)]
141/// The fields of a question paper sent from the search endpoint
142pub struct BaseQP {
143    pub id: i32,
144    pub filelink: String,
145    pub from_library: bool,
146    pub course_code: String,
147    pub course_name: String,
148    pub year: i32,
149    pub semester: Semester,
150    pub exam: Exam,
151    pub note: String,
152}
153
154#[derive(Serialize, Clone)]
155/// The fields of a question paper sent from the admin dashboard endpoints.
156///
157/// This includes fields such as `approve_status` and `upload_timestamp` that would only be relevant to the dashboard.
158pub struct AdminDashboardQP {
159    #[serde(flatten)]
160    pub qp: BaseQP,
161    pub upload_timestamp: String,
162    pub approve_status: bool,
163}
164
165impl WithUrl for BaseQP {
166    fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error> {
167        Ok(Self {
168            filelink: env_vars.paths.get_url_from_slug(&self.filelink)?,
169            ..self
170        })
171    }
172}
173
174impl WithUrl for AdminDashboardQP {
175    fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error> {
176        Ok(Self {
177            qp: self.qp.with_url(env_vars)?,
178            ..self
179        })
180    }
181}