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
165
//! Mappings of course/semester/career to internal ids.

use std::str::FromStr;

use thiserror::Error;

/// An enum of available courses in the catalog.
///
/// If a course is missing, manually specify its id with [`Course::Raw`](Course::Raw) and
/// consider sending a PR adding that mapping.
#[derive(Debug, Clone)]
pub enum Course {
    Cse115,
    Raw(String),
}

/// An enum of available semesters in the catalog.
///
/// If a semester is missing, manually specify its id with [`Semester::Raw`](Semester::Raw) and
/// consider sending a PR adding that mapping.
#[derive(Debug, Clone)]
pub enum Semester {
    Spring2023,
    Summer2023,
    Fall2023,
    Winter2023,
    Raw(String),
}

/// An enum of available careers in the catalog.
///
/// If a career is missing, manually specify its id with [`Career::Raw`](Career::Raw) and
/// consider sending a PR adding that mapping.
///
/// Specifying the career is an internal implementation detail exposed by the backend
/// network API. It doesn't make much sense to have, but nevertheless, it is required.
#[derive(Debug, Clone)]
pub enum Career {
    Undergraduate,
    Graduate,
    Law,
    DentalMedicine,
    Medicine,
    Pharmacy,
    Raw(String),
}

impl Course {
    /// Infer the career from the course.
    ///
    /// Note that this isn't always possible because a mapping does not yet exist. In
    /// that case, consider sending a PR adding the mapping.
    pub fn career(&self) -> Option<Career> {
        match self {
            Course::Cse115 => Some(Career::Undergraduate),
            // in this case it's highly dependent on the course to determine the career
            Course::Raw(_) => None,
        }
    }

    /// Internal id of the course.
    pub(crate) fn id(&self) -> &str {
        match self {
            Course::Cse115 => "004544",
            Course::Raw(id) => id,
        }
    }
}

impl FromStr for Course {
    type Err = ParseIdError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match &*normalize(s) {
            "CSE115" => Ok(Course::Cse115),
            // TODO: valid course id is 6 characters and an integer
            _ => Err(ParseIdError::InvalidId {
                id: "Course".to_owned(),
                given: s.to_owned(),
            }),
        }
    }
}

impl Semester {
    /// Internal id of the semester.
    pub(crate) fn id(&self) -> &str {
        match self {
            Semester::Spring2023 => "2231",
            Semester::Summer2023 => "",
            Semester::Fall2023 => "",
            Semester::Winter2023 => "",
            Semester::Raw(id) => id,
        }
    }
}

impl FromStr for Semester {
    type Err = ParseIdError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match &*normalize(s) {
            "SPRING2023" => Ok(Semester::Spring2023),
            "SUMMER2023" => Ok(Semester::Summer2023),
            "FALL2023" => Ok(Semester::Fall2023),
            "WINTER2023" => Ok(Semester::Winter2023),
            _ => Err(ParseIdError::InvalidId {
                id: "Semester".to_owned(),
                given: s.to_owned(),
            }),
        }
    }
}

impl Career {
    /// Internal id of the career.
    pub(crate) fn id(&self) -> &str {
        match self {
            Career::Undergraduate => "UGRD",
            Career::Graduate => "GRAD",
            Career::Law => "LAW",
            Career::DentalMedicine => "SDM",
            Career::Medicine => "MED",
            Career::Pharmacy => "PHRM",
            Career::Raw(career) => career,
        }
    }
}

impl FromStr for Career {
    type Err = ParseIdError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match &*normalize(s) {
            "UNDERGRADUATE" => Ok(Career::Undergraduate),
            "GRADUATE" => Ok(Career::Graduate),
            "LAW" => Ok(Career::Law),
            "DENTALMEDICINE" => Ok(Career::DentalMedicine),
            "MEDICINE" => Ok(Career::Medicine),
            "PHARMACY" => Ok(Career::Pharmacy),
            _ => Err(ParseIdError::InvalidId {
                id: "Career".to_owned(),
                given: s.to_owned(),
            }),
        }
    }
}

/// Normalize the input string for use in [`FromStr`](std::str:FromStr) implementations.
fn normalize(s: &str) -> String {
    s.chars()
        .filter(|c| !c.is_whitespace())
        .collect::<String>()
        .to_uppercase()
}

/// Error when parsing id.
#[derive(Debug, Error)]
pub enum ParseIdError {
    /// Specified id could not be converted to enum.
    ///
    /// Considering using the `Raw` variant for specifying raw ids.
    #[error("`{given}` is an invalid `{id}``")]
    InvalidId { id: String, given: String },
}