1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//! Utils for parsing question paper details

use color_eyre::eyre::eyre;
use duplicate::duplicate_item;
use serde::Serialize;

use crate::env::EnvVars;

#[derive(Clone, Copy)]
/// Represents a semester.
///
/// 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.
///
/// This value can be converted back into a [`String`] using the [`From`] trait implementation.
pub enum Semester {
    /// Autumn semester, parsed from `autumn`
    Autumn,
    /// Spring semester, parsed from `spring`
    Spring,
    /// Unknown/wildcard semester, parsed from an empty string.
    ///
    /// 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.
    Unknown,
}

impl TryFrom<&String> for Semester {
    type Error = color_eyre::eyre::Error;

    fn try_from(value: &String) -> Result<Self, Self::Error> {
        if value == "autumn" {
            Ok(Semester::Autumn)
        } else if value == "spring" {
            Ok(Semester::Spring)
        } else if value.is_empty() {
            Ok(Semester::Unknown)
        } else {
            Err(eyre!("Error parsing semester: Invalid value."))
        }
    }
}

impl From<Semester> for String {
    fn from(value: Semester) -> Self {
        match value {
            Semester::Autumn => "autumn".into(),
            Semester::Spring => "spring".into(),
            Semester::Unknown => "".into(),
        }
    }
}

#[derive(Clone, Copy)]
/// Represents the exam type of the paper.
///
/// Can be converted to and parsed from a String using the [`From`] and [`TryFrom`] trait implementations.
pub enum Exam {
    /// Mid-semester examination, parsed from `midsem`
    Midsem,
    /// End-semester examination, parsed from `endsem`
    Endsem,
    /// Class test, parsed from either `ct` or `ct` followed by a number (eg: `ct1` or `ct10`).
    ///
    /// 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`.
    CT(Option<usize>),
    /// Unknown class test, parsed from an empty string.
    ///
    /// 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.
    Unknown,
}

impl TryFrom<&String> for Exam {
    type Error = color_eyre::eyre::Error;

    fn try_from(value: &String) -> Result<Self, Self::Error> {
        if value == "midsem" {
            Ok(Exam::Midsem)
        } else if value == "endsem" {
            Ok(Exam::Endsem)
        } else if let Some(stripped) = value.strip_prefix("ct") {
            if stripped.is_empty() {
                Ok(Exam::CT(None))
            } else if let Ok(i) = stripped.parse::<usize>() {
                Ok(Exam::CT(Some(i)))
            } else {
                Err(eyre!("Error parsing exam: Invalid class test number."))
            }
        } else if value.is_empty() {
            Ok(Exam::Unknown)
        } else {
            Err(eyre!("Error parsing exam: Unknown exam type."))
        }
    }
}

impl From<Exam> for String {
    fn from(value: Exam) -> Self {
        match value {
            Exam::Midsem => "midsem".into(),
            Exam::Endsem => "endsem".into(),
            Exam::Unknown => "".into(),
            Exam::CT(None) => "ct".into(),
            Exam::CT(Some(i)) => format!("ct{}", i),
        }
    }
}

#[duplicate_item(
    ExamSem;
    [ Exam ];
    [ Semester ];
)]
impl Serialize for ExamSem {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&String::from(*self))
    }
}

#[derive(Serialize, Clone)]
/// The fields of a question paper sent from the search endpoint
pub struct SearchQP {
    pub id: i32,
    pub filelink: String,
    pub from_library: bool,
    pub course_code: String,
    pub course_name: String,
    pub year: i32,
    pub semester: Semester,
    pub exam: Exam,
}

#[derive(Serialize, Clone)]
/// The fields of a question paper sent from the admin dashboard endpoints.
///
/// This includes fields such as `approve_status` and `upload_timestamp` that would only be relevant to the dashboard.
pub struct AdminDashboardQP {
    pub id: i32,
    pub filelink: String,
    pub from_library: bool,
    pub course_code: String,
    pub course_name: String,
    pub year: i32,
    pub semester: Semester,
    pub exam: Exam,
    pub upload_timestamp: String,
    pub approve_status: bool,
}

#[duplicate_item(
    QP;
    [ SearchQP ];
    [ AdminDashboardQP ];
)]
impl QP {
    /// 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.
    pub fn with_url(self, env_vars: &EnvVars) -> Result<Self, color_eyre::eyre::Error> {
        Ok(Self {
            filelink: env_vars.paths.get_url_from_slug(&self.filelink)?,
            ..self
        })
    }
}