ucas_iclass/
lib.rs

1//! # `ucas-iclass` library crate
2//!
3//! If you are reading this, you are reading the documentation for the `ucas-iclass` library crate. For the cli, kindly refer to the README file.
4
5#![deny(missing_docs)]
6#![warn(clippy::all, clippy::nursery, clippy::pedantic, clippy::cargo)]
7#![allow(clippy::multiple_crate_versions, reason = "Dependency issues")]
8
9mod checkin;
10mod login;
11mod query;
12pub mod util;
13
14pub use checkin::CheckInResult;
15pub use login::UserSessionInfo;
16pub use query::{Course, DailySchedule, Schedule, Semester};
17
18use cyper::{Client, Error as CyperError};
19use serde::Deserialize;
20use std::fmt::Debug;
21use url::{ParseError, Url};
22
23/// The root URL of the iClass platform.
24pub const API_ROOT: &str = "https://iclass.ucas.edu.cn:8181/";
25
26/// The iClass struct.
27pub struct IClass {
28    /// API root URL.
29    api_root: Url,
30    /// The HTTP client.
31    client: Client,
32    /// User session information.
33    pub user_session: Option<UserSessionInfo>,
34}
35
36/// Possible errors when interacting with the iClass platform.
37#[derive(Debug, thiserror::Error)]
38pub enum IClassError {
39    /// The user has not logged in.
40    #[error("user not logged in")]
41    NotLoggedIn,
42    /// Other API errors.
43    #[error("API error: {0}")]
44    ApiError(String),
45    /// Cyper-related error.
46    #[error("cyper error: {0}")]
47    CyperError(#[from] CyperError),
48    /// Error parsing data from the server.
49    #[error("data parsing error")]
50    DataParsingError,
51}
52
53/// Generic response structure from the iClass API.
54#[derive(Clone, Debug, Deserialize)]
55pub struct Response<T>
56where
57    T: Debug,
58{
59    /// The status code of the response, 0 for success.
60    #[serde(rename = "STATUS", deserialize_with = "util::deserialize_str_to_int")]
61    pub status: i8,
62    /// Optional error code.
63    ///
64    /// - 100: 参数错误
65    /// - 106: 用户不存在
66    /// - 107: 密码错误
67    #[serde(
68        rename = "ERRCODE",
69        default,
70        deserialize_with = "util::deserialize_opt_str_to_int"
71    )]
72    pub err_code: Option<i8>,
73    /// Optional error message.
74    #[serde(rename = "ERRMSG")]
75    pub err_msg: Option<String>,
76    /// The result data.
77    pub result: Option<T>,
78}
79
80impl Default for IClass {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl IClass {
87    /// Creates a new instance of [`IClass`].
88    #[allow(clippy::missing_panics_doc, reason = "URL is constant and valid")]
89    #[must_use]
90    pub fn new() -> Self {
91        Self::with_api_root(Url::parse(API_ROOT).unwrap())
92    }
93
94    /// Creates a new instance of [`IClass`] with given API root URL.
95    #[must_use]
96    pub fn with_api_root(url: Url) -> Self {
97        Self {
98            api_root: url,
99            client: Client::new(),
100            user_session: None,
101        }
102    }
103
104    /// Gets a reference to user session info, or raises [`IClassError::NotLoggedIn`].
105    ///
106    /// # Errors
107    ///
108    /// [`IClassError::NotLoggedIn`] if the user is not logged in.
109    fn get_user_session(&self) -> Result<&UserSessionInfo, IClassError> {
110        self.user_session.as_ref().ok_or(IClassError::NotLoggedIn)
111    }
112}
113
114impl From<ParseError> for IClassError {
115    fn from(e: ParseError) -> Self {
116        Self::CyperError(CyperError::UrlParse(e))
117    }
118}
119
120impl<T> Response<T>
121where
122    T: Debug,
123{
124    /// Converts the response into a [`Result`], translating status codes into errors.
125    ///
126    /// # Errors
127    ///
128    /// See [`IClassError`].
129    pub fn into_result(self) -> Result<T, IClassError> {
130        if self.status == 0 {
131            self.result.ok_or(IClassError::DataParsingError)
132        } else {
133            Err(IClassError::ApiError(if let Some(msg) = self.err_msg {
134                msg
135            } else {
136                format!("Unknown error, {self:?}")
137            }))
138        }
139    }
140}