1#![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
23pub const API_ROOT: &str = "https://iclass.ucas.edu.cn:8181/";
25
26pub struct IClass {
28 api_root: Url,
30 client: Client,
32 pub user_session: Option<UserSessionInfo>,
34}
35
36#[derive(Debug, thiserror::Error)]
38pub enum IClassError {
39 #[error("user not logged in")]
41 NotLoggedIn,
42 #[error("API error: {0}")]
44 ApiError(String),
45 #[error("cyper error: {0}")]
47 CyperError(#[from] CyperError),
48 #[error("data parsing error")]
50 DataParsingError,
51}
52
53#[derive(Clone, Debug, Deserialize)]
55pub struct Response<T>
56where
57 T: Debug,
58{
59 #[serde(rename = "STATUS", deserialize_with = "util::deserialize_str_to_int")]
61 pub status: i8,
62 #[serde(
68 rename = "ERRCODE",
69 default,
70 deserialize_with = "util::deserialize_opt_str_to_int"
71 )]
72 pub err_code: Option<i8>,
73 #[serde(rename = "ERRMSG")]
75 pub err_msg: Option<String>,
76 pub result: Option<T>,
78}
79
80impl Default for IClass {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl IClass {
87 #[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 #[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 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 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}