ubs_lib/
lib.rs

1//! # ubs
2//! `ubs` provides an interface for fetching real-time Univeristy at Buffalo class schedules.
3//! ## Usage
4//! ```rust
5//! use ubs_lib::{Course, Semester};
6//!
7//! let mut schedule_iter = ubs_lib::schedule_iter(
8//!     Course::Cse115,
9//!     Semester::Spring2023,
10//! ).await?;
11//!
12//! while let Some(schedule) = schedule_iter.try_next().await? {
13//!     for group in schedule?.group_iter() {
14//!         for class in group.class_iter() {
15//!             // do stuff
16//!         }
17//!     }
18//! }
19//!```
20
21mod ids;
22pub mod parser;
23pub mod session;
24
25pub use ids::{Career, Course, ParseIdError, Semester};
26use parser::{ClassSchedule, ParseError};
27use session::{Query, Session, SessionError, Token};
28
29use futures::{TryStream, TryStreamExt};
30use hyper::Client;
31
32/// Iterator over each page of the specified query.
33///
34/// If there is no course to career mapping for the specified course, this function
35/// will return [`ScheduleError::FailedToInferCareer`](ScheduleError::FailedToInferCareer).
36/// In that case, consider manually specifying the career via [`schedule_iter_with_career`](schedule_iter_with_career),
37/// and sending a PR to add the inference.
38#[cfg(feature = "rustls")]
39pub async fn schedule_iter<'a>(
40    course: Course,
41    semester: Semester,
42) -> Result<
43    impl TryStream<Ok = Result<ClassSchedule, ParseError>, Error = SessionError> + 'a,
44    ScheduleError,
45> {
46    let career = course
47        .career()
48        .ok_or_else(|| ScheduleError::FailedToInferCareer(course.clone()))?;
49    schedule_iter_with_career(course, semester, career).await
50}
51
52/// iterator over each page of the specified query with an explicit career.
53///
54/// Note that in some cases the career cannot be inferred from the course, thus it
55/// is necessary to manually specify the career. Considering sending a PR with the
56/// course to career mapping.
57#[cfg(feature = "rustls")]
58pub async fn schedule_iter_with_career<'a>(
59    course: Course,
60    semester: Semester,
61    career: Career,
62) -> Result<
63    impl TryStream<Ok = Result<ClassSchedule, ParseError>, Error = SessionError> + 'a,
64    ScheduleError,
65> {
66    let client = Client::builder().build(
67        hyper_rustls::HttpsConnectorBuilder::new()
68            .with_native_roots()
69            .https_only()
70            .enable_http1()
71            .build(),
72    );
73    let token = Token::new(&client).await?;
74    Ok(Session::new(client, token)
75        .schedule_iter(Query::new(course, semester, career))
76        // TODO: set page accordingly. Ideally, the schedule should be able to figure it out itself
77        .map_ok(|bytes| ClassSchedule::new(bytes.into(), 1)))
78}
79
80/// Error when iterating schedules.
81#[derive(Debug, thiserror::Error)]
82pub enum ScheduleError {
83    /// Failed to connect to host.
84    #[error(transparent)]
85    ConnectionFailed(#[from] SessionError),
86    /// Failed to parse data returned from host.
87    #[error(transparent)]
88    ParseFailed(#[from] ParseError),
89    /// Failed to infer career from course.
90    #[error("failed to infer career from course `{0:?}`, consider passing it explicitly via `schedule_iter_with_career`")]
91    FailedToInferCareer(Course),
92}