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
//! # ubs
//! `ubs` provides an interface for fetching real-time Univeristy at Buffalo class schedules.
//! ## Usage
//! ```rust
//! use ubs_lib::{Course, Semester};
//!
//! let mut schedule_iter = ubs_lib::schedule_iter(
//!     Course::Cse115,
//!     Semester::Spring2023,
//! ).await?;
//!
//! while let Some(schedule) = schedule_iter.try_next().await? {
//!     for group in schedule?.group_iter() {
//!         for class in group.class_iter() {
//!             // do stuff
//!         }
//!     }
//! }
//!```

mod ids;
pub mod parser;
pub mod session;

pub use ids::{Career, Course, ParseIdError, Semester};
use parser::{ClassSchedule, ParseError};
use session::{Query, Session, SessionError, Token};

use futures::{TryStream, TryStreamExt};
use hyper::Client;

/// Iterator over each page of the specified query.
///
/// If there is no course to career mapping for the specified course, this function
/// will return [`ScheduleError::FailedToInferCareer`](ScheduleError::FailedToInferCareer).
/// In that case, consider manually specifying the career via [`schedule_iter_with_career`](schedule_iter_with_career),
/// and sending a PR to add the inference.
#[cfg(feature = "rustls")]
pub async fn schedule_iter<'a>(
    course: Course,
    semester: Semester,
) -> Result<
    impl TryStream<Ok = Result<ClassSchedule, ParseError>, Error = SessionError> + 'a,
    ScheduleError,
> {
    let career = course
        .career()
        .ok_or_else(|| ScheduleError::FailedToInferCareer(course.clone()))?;
    schedule_iter_with_career(course, semester, career).await
}

/// iterator over each page of the specified query with an explicit career.
///
/// Note that in some cases the career cannot be inferred from the course, thus it
/// is necessary to manually specify the career. Considering sending a PR with the
/// course to career mapping.
#[cfg(feature = "rustls")]
pub async fn schedule_iter_with_career<'a>(
    course: Course,
    semester: Semester,
    career: Career,
) -> Result<
    impl TryStream<Ok = Result<ClassSchedule, ParseError>, Error = SessionError> + 'a,
    ScheduleError,
> {
    let client = Client::builder().build(
        hyper_rustls::HttpsConnectorBuilder::new()
            .with_native_roots()
            .https_only()
            .enable_http1()
            .build(),
    );
    let token = Token::new(&client).await?;
    Ok(Session::new(client, token)
        .schedule_iter(Query::new(course, semester, career))
        // TODO: set page accordingly. Ideally, the schedule should be able to figure it out itself
        .map_ok(|bytes| ClassSchedule::new(bytes.into(), 1)))
}

/// Error when iterating schedules.
#[derive(Debug, thiserror::Error)]
pub enum ScheduleError {
    /// Failed to connect to host.
    #[error(transparent)]
    ConnectionFailed(#[from] SessionError),
    /// Failed to parse data returned from host.
    #[error(transparent)]
    ParseFailed(#[from] ParseError),
    /// Failed to infer career from course.
    #[error("failed to infer career from course `{0:?}`, consider passing it explicitly via `schedule_iter_with_career`")]
    FailedToInferCareer(Course),
}