Skip to content

Commit aa7ce0b

Browse files
committed
Merge branch 'dev'
2 parents 94a7fba + ae9b253 commit aa7ce0b

11 files changed

Lines changed: 616 additions & 210 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Cargo.lock
1515

1616

1717
# Added by cargo
18-
1918
/target
2019
/Cargo.lock
20+
21+
# macOS
22+
.DS_Store

Cargo.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
[package]
22
name = "q"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
5-
authors = ["CRT-HAO"]
5+
authors = ["hayd1n", "Hayden Chang <m831718@gmail.com>"]
66
repository = "https://github.com/NTUST-Hack/q"
77

8-
98
[dependencies]
10-
reqwest = { version = "0.11", features = ["json", "blocking"] }
9+
reqwest = { version = "0.12.5", features = ["json"] }
1110
serde = "1.0.196"
1211
serde-aux = "4.4.0"
1312
serde_derive = "1.0.196"
1413
serde_json = "1.0.1"
14+
serde_with = "3.8.1"
1515
tokio = { version = "1", features = ["full"] }
16+
url = "2.5.2"
1617

1718
[dev-dependencies]
19+
anyhow = "1.0.86"
20+
futures = "0.3.30"
1821
tokio-task-pool = "0.1.5"

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ NTUST Query Course Library
1212
use q::Q;
1313

1414
#[tokio::main]
15-
async fn main() -> Result<(), Box<dyn std::error::Error>> {
15+
async fn main() -> anyhow::Result<()> {
1616
let c = Q::new();
1717

1818
let details = c
19-
.query("1122", "AT2005701", "en")
19+
.query("1131", "TCG046301", q::Language::Zh)
2020
.await
2121
.expect("failed to query course description");
2222

2323
println!("{:#?}", details);
2424

2525
Ok(())
2626
}
27+
2728
```

examples/blocking_query.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use q::blocking::Q;
2+
3+
fn main() -> anyhow::Result<()> {
4+
let c = Q::new();
5+
6+
let details = c
7+
.query("1122", "AT2005701", q::Language::En)
8+
.expect("failed to query course description");
9+
10+
println!("{:#?}", details);
11+
12+
Ok(())
13+
}

examples/custom_reqwest.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use std::time::Duration;
2+
3+
#[tokio::main]
4+
async fn main() -> anyhow::Result<()> {
5+
// Use a custom reqwest client builder
6+
let mut reqwest_client_builder = q::default_reqwest_builder();
7+
8+
// Set a timeout of 10 seconds
9+
reqwest_client_builder = reqwest_client_builder.timeout(Duration::from_secs(10));
10+
11+
// Build the reqwest client
12+
let reqwest_client = reqwest_client_builder
13+
.build()
14+
.expect("failed to build reqwest client");
15+
16+
// Create a new client with the custom reqwest client
17+
let c = q::ClientBuilder::new()
18+
.reqwest_client(reqwest_client)
19+
.build();
20+
21+
// Query course details
22+
let details = c
23+
.query("1131", "TCG046301", q::Language::Zh)
24+
.await
25+
.expect("failed to query course description");
26+
27+
println!("{:#?}", details);
28+
29+
Ok(())
30+
}

examples/query.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use q::Q;
22

33
#[tokio::main]
4-
async fn main() -> Result<(), Box<dyn std::error::Error>> {
4+
async fn main() -> anyhow::Result<()> {
55
let c = Q::new();
66

77
let details = c
8-
.query("1122", "AT2005701", "en")
8+
.query("1131", "TCG046301", q::Language::Zh)
99
.await
1010
.expect("failed to query course description");
1111

examples/search.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use q::Q;
2+
3+
#[tokio::main]
4+
async fn main() -> anyhow::Result<()> {
5+
let c = Q::new();
6+
7+
let mut options = q::SearchOptions::new("1131", q::Language::Zh);
8+
9+
options.course_no = "cs".to_string();
10+
11+
let details = c
12+
.search(&options, true)
13+
.await
14+
.expect("failed to search courses");
15+
16+
println!("{:#?}", details);
17+
18+
Ok(())
19+
}

examples/worker_pool.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ use std::{
66
time::Duration,
77
};
88

9+
use q::QueryError;
910
use tokio_task_pool::Pool;
1011

1112
const SEMESTER: &'static str = "1122";
12-
const LANGUAGE: &'static str = "zh";
13+
const LANGUAGE: q::Language = q::Language::Zh;
1314

14-
async fn worker(client: &q::Q, course_no: &str) {
15-
let details = client.query(SEMESTER, course_no, LANGUAGE).await.unwrap();
15+
async fn worker(client: &q::Q, course_no: &str) -> Result<(), QueryError> {
16+
let details = client.query(SEMESTER, course_no, LANGUAGE).await?;
1617
println!(
1718
"{: <10} | {: <10} | {}/{}",
1819
details.course_no, details.course_name, details.choose_student, details.restrict2
1920
);
21+
22+
Ok(())
2023
}
2124

2225
#[tokio::main]
23-
async fn main() {
26+
async fn main() -> anyhow::Result<()> {
2427
const THREADS: usize = 64;
2528

2629
let duration = Duration::from_secs(10);
@@ -66,7 +69,7 @@ async fn main() {
6669
let c = clients_clone.get(i % THREADS).unwrap();
6770
let no = courses_clone.get(i % courses_length).unwrap();
6871

69-
worker(c, no).await;
72+
worker(c, no).await.unwrap();
7073

7174
times_clone.fetch_add(1, Ordering::Relaxed);
7275
})
@@ -87,4 +90,6 @@ async fn main() {
8790
times.load(Ordering::Relaxed),
8891
duration
8992
);
93+
94+
Ok(())
9095
}

src/async_impl/mod.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
use std::collections::HashMap;
2+
3+
use url::Url;
4+
5+
use crate::{
6+
default_reqwest_builder, CourseDetails, CourseInfo, Language, QueryError, SearchOptions,
7+
DEFAULT_API_URL,
8+
};
9+
10+
#[derive(Debug)]
11+
pub struct ClientBuilder {
12+
reqwest_client: reqwest::Client,
13+
base_url: Url,
14+
}
15+
16+
impl ClientBuilder {
17+
pub fn new() -> Self {
18+
ClientBuilder {
19+
reqwest_client: default_reqwest_builder().build().unwrap(),
20+
base_url: Url::parse(&DEFAULT_API_URL).unwrap(),
21+
}
22+
}
23+
24+
pub fn reqwest_client(mut self, client: reqwest::Client) -> Self {
25+
self.reqwest_client = client;
26+
self
27+
}
28+
29+
pub fn api_url(mut self, url: Url) -> Self {
30+
self.base_url = url;
31+
self
32+
}
33+
34+
pub fn build(self) -> Q {
35+
Q {
36+
http_client: self.reqwest_client,
37+
base_url: self.base_url,
38+
}
39+
}
40+
}
41+
42+
pub struct Q {
43+
http_client: reqwest::Client,
44+
base_url: Url,
45+
}
46+
47+
impl Q {
48+
pub fn new() -> Self {
49+
ClientBuilder::new().build()
50+
}
51+
52+
pub async fn search(
53+
&self,
54+
options: &SearchOptions,
55+
merge_courses: bool,
56+
) -> Result<Vec<CourseInfo>, QueryError> {
57+
let url = self.base_url.join("courses").unwrap();
58+
59+
let resp = match self.http_client.post(url).json(&options).send().await {
60+
Ok(resp) => resp,
61+
Err(e) => return Err(QueryError::HttpError(format!("{}", e))),
62+
};
63+
64+
match resp.json::<Vec<CourseInfo>>().await {
65+
Ok(json) => Ok(if merge_courses {
66+
crate::merge_courses(json)
67+
} else {
68+
json
69+
}),
70+
Err(e) => return Err(QueryError::ParseError(format!("{}", e))),
71+
}
72+
}
73+
74+
pub async fn query(
75+
&self,
76+
semester: &str,
77+
course_no: &str,
78+
language: Language,
79+
) -> Result<CourseDetails, QueryError> {
80+
// is "coursedetials" not "coursedetails"
81+
// looks like an idiotic typo in the API
82+
let url = self.base_url.join("coursedetials").unwrap();
83+
84+
let mut params = HashMap::new();
85+
params.insert("semester", semester);
86+
params.insert("course_no", course_no);
87+
params.insert("language", language.as_str());
88+
89+
let resp = match self.http_client.get(url).query(&params).send().await {
90+
Ok(resp) => resp,
91+
Err(e) => return Err(QueryError::HttpError(format!("{}", e))),
92+
};
93+
94+
let full = match resp.bytes().await {
95+
Ok(bytes) => bytes,
96+
Err(e) => return Err(QueryError::ParseError(format!("{}", e))),
97+
};
98+
99+
// # debug
100+
// let text = match String::from_utf8(full.to_vec()) {
101+
// Ok(bytes) => bytes,
102+
// Err(e) => return Err(QueryError::ParseError(format!("{}", e))),
103+
// };
104+
// println!("{}", text);
105+
106+
let json = match serde_json::from_slice::<Vec<CourseDetails>>(&full) {
107+
Ok(json) => json,
108+
Err(e) => return Err(QueryError::ParseError(format!("{}", e))),
109+
};
110+
111+
if let Some(course_details) = json.get(0) {
112+
Ok(course_details.clone())
113+
} else {
114+
Err(QueryError::ParseError(format!("No course found")))
115+
}
116+
}
117+
}
118+
119+
#[cfg(test)]
120+
mod tests {
121+
use super::*;
122+
use futures::future::join_all;
123+
124+
#[tokio::test]
125+
async fn new() {
126+
let _client = Q::new();
127+
}
128+
129+
#[tokio::test]
130+
async fn search() {
131+
let client = Q::new();
132+
133+
let mut options = SearchOptions::new("1131", Language::Zh);
134+
135+
options.course_no = "cs".to_string();
136+
137+
let _details = client
138+
.search(&options, true)
139+
.await
140+
.expect("failed to search courses");
141+
142+
println!("{:#?}", _details);
143+
}
144+
145+
#[tokio::test]
146+
async fn query() {
147+
let client = Q::new();
148+
149+
let _details = client
150+
.query("1122", "AT2005701", Language::Zh)
151+
.await
152+
.expect("Failed to query");
153+
154+
println!("{:#?}", _details)
155+
}
156+
157+
#[tokio::test]
158+
async fn query_all_cs() {
159+
let client = Q::new();
160+
161+
let mut options = SearchOptions::new("1131", Language::Zh);
162+
163+
options.course_no = "cs".to_string();
164+
165+
let search_results = client
166+
.search(&options, true)
167+
.await
168+
.expect("failed to search courses");
169+
170+
let futures = search_results.into_iter().map(|c| async move {
171+
let query_client = Q::new();
172+
let details = query_client
173+
.query(&c.semester, &c.course_no, Language::Zh)
174+
.await
175+
.expect("Failed to query");
176+
177+
println!("{:#?}", details);
178+
});
179+
180+
join_all(futures).await;
181+
}
182+
}

0 commit comments

Comments
 (0)