chore: solve lints, improve formatting

This commit is contained in:
2025-08-27 12:19:17 -05:00
parent ac70306c04
commit cb8a595326
9 changed files with 103 additions and 130 deletions

View File

@@ -5,7 +5,6 @@ use crate::banner::Course;
use anyhow::Result;
use redis::AsyncCommands;
use redis::Client;
use serde_json;
use std::sync::Arc;
#[derive(Clone, Debug)]
@@ -31,7 +30,7 @@ impl AppState {
pub async fn get_course_or_fetch(&self, term: &str, crn: &str) -> Result<Course> {
let mut conn = self.redis.get_multiplexed_async_connection().await?;
let key = format!("class:{}", crn);
let key = format!("class:{crn}");
if let Some(serialized) = conn.get::<_, Option<String>>(&key).await? {
let course: Course = serde_json::from_str(&serialized)?;
return Ok(course);
@@ -44,6 +43,6 @@ impl AppState {
return Ok(course);
}
Err(anyhow::anyhow!("Course not found for CRN {}", crn))
Err(anyhow::anyhow!("Course not found for CRN {crn}"))
}
}

View File

@@ -65,7 +65,7 @@ impl BannerApi {
("searchTerm", search),
("offset", &page.to_string()),
("max", &max_results.to_string()),
("_", &timestamp_nonce()),
("_", &SessionManager::nonce()),
];
let response = self
@@ -104,7 +104,7 @@ impl BannerApi {
("offset", &offset.to_string()),
("max", &max_results.to_string()),
("uniqueSessionId", &session_id),
("_", &timestamp_nonce()),
("_", &SessionManager::nonce()),
];
let response = self
@@ -143,7 +143,7 @@ impl BannerApi {
("offset", &offset.to_string()),
("max", &max_results.to_string()),
("uniqueSessionId", &session_id),
("_", &timestamp_nonce()),
("_", &SessionManager::nonce()),
];
let response = self
@@ -182,7 +182,7 @@ impl BannerApi {
("offset", &offset.to_string()),
("max", &max_results.to_string()),
("uniqueSessionId", &session_id),
("_", &timestamp_nonce()),
("_", &SessionManager::nonce()),
];
let response = self
@@ -382,15 +382,6 @@ impl BannerApi {
}
}
/// Generates a timestamp-based nonce.
fn timestamp_nonce() -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
.to_string()
}
/// Returns a browser-like user agent string.
fn user_agent() -> &'static str {
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
@@ -402,13 +393,9 @@ fn parse_json_with_context<T: serde::de::DeserializeOwned>(body: &str) -> Result
Ok(value) => Ok(value),
Err(err) => {
let (line, column) = (err.line(), err.column());
let snippet = build_error_snippet(body, line as usize, column as usize, 120);
let snippet = build_error_snippet(body, line, column, 120);
Err(anyhow::anyhow!(
"{} at line {}, column {}\nSnippet:\n{}",
err,
line,
column,
snippet
"{err} at line {line}, column {column}\nSnippet:\n{snippet}",
))
}
}
@@ -430,5 +417,5 @@ fn build_error_snippet(body: &str, line: usize, column: usize, max_len: usize) -
indicator.push('^');
}
format!("{}\n{}", slice, indicator)
format!("{slice}\n{indicator}")
}

View File

@@ -1,7 +1,7 @@
use bitflags::{Flags, bitflags};
use chrono::{DateTime, NaiveDate, NaiveTime, Timelike, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use std::{cmp::Ordering, str::FromStr};
use std::{cmp::Ordering, fmt::Display, str::FromStr};
use super::terms::Term;
@@ -148,30 +148,17 @@ pub enum DayOfWeek {
impl DayOfWeek {
/// Convert to short string representation
pub fn to_short_string(&self) -> &'static str {
pub fn to_short_string(self) -> &'static str {
match self {
DayOfWeek::Monday => "M",
DayOfWeek::Monday => "Mo",
DayOfWeek::Tuesday => "Tu",
DayOfWeek::Wednesday => "W",
DayOfWeek::Wednesday => "We",
DayOfWeek::Thursday => "Th",
DayOfWeek::Friday => "F",
DayOfWeek::Friday => "Fr",
DayOfWeek::Saturday => "Sa",
DayOfWeek::Sunday => "Su",
}
}
/// Convert to full string representation
pub fn to_string(&self) -> &'static str {
match self {
DayOfWeek::Monday => "Monday",
DayOfWeek::Tuesday => "Tuesday",
DayOfWeek::Wednesday => "Wednesday",
DayOfWeek::Thursday => "Thursday",
DayOfWeek::Friday => "Friday",
DayOfWeek::Saturday => "Saturday",
DayOfWeek::Sunday => "Sunday",
}
}
}
impl TryFrom<MeetingDays> for DayOfWeek {
@@ -196,10 +183,9 @@ impl TryFrom<MeetingDays> for DayOfWeek {
});
}
return Err(anyhow::anyhow!(
"Cannot convert multiple days to a single day: {:?}",
days
));
Err(anyhow::anyhow!(
"Cannot convert multiple days to a single day: {days:?}"
))
}
}
@@ -252,15 +238,8 @@ impl TimeRange {
let hour = time.hour();
let minute = time.minute();
if hour == 0 {
format!("12:{:02}AM", minute)
} else if hour < 12 {
format!("{}:{:02}AM", hour, minute)
} else if hour == 12 {
format!("12:{:02}PM", minute)
} else {
format!("{}:{:02}PM", hour - 12, minute)
}
let meridiem = if hour < 12 { "AM" } else { "PM" };
format!("{hour}:{minute:02}{meridiem}")
}
/// Get duration in minutes
@@ -376,15 +355,20 @@ impl MeetingLocation {
is_online,
}
}
}
/// Convert to formatted string
pub fn to_string(&self) -> String {
impl Display for MeetingLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_online {
"Online".to_string()
write!(f, "Online")
} else {
format!(
"{} | {} | {} {}",
self.campus, self.building_description, self.building, self.room
write!(
f,
"{campus} | {building_name} | {building_code} {room}",
campus = self.campus,
building_name = self.building_description,
building_code = self.building,
room = self.room
)
}
}

View File

@@ -46,7 +46,7 @@ impl Term {
/// Returns the current term status for a specific date
pub fn get_status_for_date(date: NaiveDate) -> TermPoint {
let literal_year = date.year() as u32;
let day_of_year = date.ordinal() as u32;
let day_of_year = date.ordinal();
let ranges = Self::get_season_ranges(literal_year);
// If we're past the end of the summer term, we're 'in' the next school year.
@@ -115,22 +115,22 @@ impl Term {
fn get_season_ranges(year: u32) -> SeasonRanges {
let spring_start = NaiveDate::from_ymd_opt(year as i32, 1, 14)
.unwrap()
.ordinal() as u32;
.ordinal();
let spring_end = NaiveDate::from_ymd_opt(year as i32, 5, 1)
.unwrap()
.ordinal() as u32;
.ordinal();
let summer_start = NaiveDate::from_ymd_opt(year as i32, 5, 25)
.unwrap()
.ordinal() as u32;
.ordinal();
let summer_end = NaiveDate::from_ymd_opt(year as i32, 8, 15)
.unwrap()
.ordinal() as u32;
.ordinal();
let fall_start = NaiveDate::from_ymd_opt(year as i32, 8, 18)
.unwrap()
.ordinal() as u32;
.ordinal();
let fall_end = NaiveDate::from_ymd_opt(year as i32, 12, 10)
.unwrap()
.ordinal() as u32;
.ordinal();
SeasonRanges {
spring: YearDayRange {
@@ -179,10 +179,15 @@ struct YearDayRange {
end: u32,
}
impl ToString for Term {
impl std::fmt::Display for Term {
/// Returns the term in the format YYYYXX, where YYYY is the year and XX is the season code
fn to_string(&self) -> String {
format!("{}{}", self.year, self.season.to_str())
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{year}{season}",
year = self.year,
season = self.season.to_str()
)
}
}
@@ -215,7 +220,7 @@ impl FromStr for Season {
"10" => Season::Fall,
"20" => Season::Spring,
"30" => Season::Summer,
_ => return Err(anyhow::anyhow!("Invalid season: {}", s)),
_ => return Err(anyhow::anyhow!("Invalid season: {s}")),
};
Ok(season)
}

View File

@@ -270,7 +270,7 @@ impl std::fmt::Display for SearchQuery {
let mut parts = Vec::new();
if let Some(ref subject) = self.subject {
parts.push(format!("subject={}", subject));
parts.push(format!("subject={subject}"));
}
if let Some(ref title) = self.title {
parts.push(format!("title={}", title.trim()));
@@ -296,21 +296,21 @@ impl std::fmt::Display for SearchQuery {
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",");
parts.push(format!("instructor={}", instructor_str));
parts.push(format!("instructor={instructor_str}"));
}
if let Some(start_time) = self.start_time {
let (hour, minute, meridiem) = format_time_parameter(start_time);
parts.push(format!("startTime={}:{}:{}", hour, minute, meridiem));
parts.push(format!("startTime={hour}:{minute}:{meridiem}"));
}
if let Some(end_time) = self.end_time {
let (hour, minute, meridiem) = format_time_parameter(end_time);
parts.push(format!("endTime={}:{}:{}", hour, minute, meridiem));
parts.push(format!("endTime={hour}:{minute}:{meridiem}"));
}
if let Some(min_credits) = self.min_credits {
parts.push(format!("minCredits={}", min_credits));
parts.push(format!("minCredits={min_credits}"));
}
if let Some(max_credits) = self.max_credits {
parts.push(format!("maxCredits={}", max_credits));
parts.push(format!("maxCredits={max_credits}"));
}
if let Some(ref range) = self.course_number_range {
parts.push(format!("courseNumberRange={}-{}", range.low, range.high));

View File

@@ -39,7 +39,7 @@ impl CourseScraper {
.context("Failed to get subjects for scraping")?;
if subjects.is_empty() {
return Err(anyhow::anyhow!("No subjects found for term {}", term));
return Err(anyhow::anyhow!("no subjects found for term {term}"));
}
// Categorize subjects
@@ -58,15 +58,17 @@ impl CourseScraper {
}
info!(
"scraping {} subjects for term {}",
expired_subjects.len(),
term
"scraping {count} subjects for term {term}",
count = expired_subjects.len()
);
// Scrape each expired subject
for subject in expired_subjects {
if let Err(e) = self.scrape_subject(&subject.code, term).await {
error!("failed to scrape subject {}: {}", subject.code, e);
error!(
"failed to scrape subject {subject}: {e}",
subject = subject.code
);
}
// Rate limiting between subjects
@@ -87,7 +89,7 @@ impl CourseScraper {
let mut expired = Vec::new();
for subject in subjects {
let key = format!("scraped:{}:{}", subject.code, term);
let key = format!("scraped:{code}:{term}", code = subject.code);
let scraped: Option<String> = conn
.get(&key)
.await
@@ -121,16 +123,12 @@ impl CourseScraper {
.search(term, &query, "subjectDescription", false)
.await
.with_context(|| {
format!(
"Failed to search for subject {} at offset {}",
subject, offset
)
format!("failed to search for subject {subject} at offset {offset}")
})?;
if !result.success {
return Err(anyhow::anyhow!(
"Search marked unsuccessful for subject {}",
subject
"search marked unsuccessful for subject {subject}"
));
}
@@ -138,16 +136,16 @@ impl CourseScraper {
total_courses += course_count;
debug!(
"retrieved {} courses for subject {} at offset {}",
course_count, subject, offset
"retrieved {count} courses for subject {subject} at offset {offset}",
count = course_count
);
// Store each course in Redis
for course in result.data.unwrap_or_default() {
if let Err(e) = self.store_course(&course).await {
error!(
"failed to store course {}: {}",
course.course_reference_number, e
"failed to store course {crn}: {e}",
crn = course.course_reference_number
);
}
}
@@ -156,16 +154,14 @@ impl CourseScraper {
if course_count >= MAX_PAGE_SIZE {
if course_count > MAX_PAGE_SIZE {
warn!(
"course count {} exceeds max page size {}",
course_count, MAX_PAGE_SIZE
"course count {count} exceeds max page size {max_page_size}",
count = course_count,
max_page_size = MAX_PAGE_SIZE
);
}
offset += MAX_PAGE_SIZE;
debug!(
"continuing to next page for subject {} at offset {}",
subject, offset
);
debug!("continuing to next page for subject {subject} at offset {offset}");
// Rate limiting between pages
time::sleep(Duration::from_secs(3)).await;
@@ -176,8 +172,8 @@ impl CourseScraper {
}
info!(
"scraped {} total courses for subject {}",
total_courses, subject
"scraped {count} total courses for subject {subject}",
count = total_courses
);
// Mark subject as scraped with expiry
@@ -195,7 +191,7 @@ impl CourseScraper {
.await
.context("Failed to get Redis connection")?;
let key = format!("class:{}", course.course_reference_number);
let key = format!("class:{crn}", crn = course.course_reference_number);
let serialized = serde_json::to_string(course).context("Failed to serialize course")?;
let _: () = conn
@@ -219,19 +215,21 @@ impl CourseScraper {
.await
.context("Failed to get Redis connection")?;
let key = format!("scraped:{}:{}", subject, term);
let key = format!("scraped:{subject}:{term}", subject = subject);
let expiry = self.calculate_expiry(subject, course_count);
let value = if course_count == 0 { -1 } else { course_count };
let _: () = conn
.set_ex(&key, value, expiry.as_secs() as u64)
.set_ex(&key, value, expiry.as_secs())
.await
.context("Failed to mark subject as scraped")?;
debug!(
"marked subject {} as scraped with {} courses, expiry: {:?}",
subject, course_count, expiry
"marked subject {subject} as scraped with {count} courses, expiry: {expiry:?}",
subject = subject,
count = course_count,
expiry = expiry
);
Ok(())
@@ -251,7 +249,7 @@ impl CourseScraper {
// Priority subjects get shorter expiry (more frequent updates)
if PRIORITY_MAJORS.contains(&subject) {
base_expiry = base_expiry / 3;
base_expiry /= 3;
}
// Add random variance (±15%)
@@ -276,7 +274,7 @@ impl CourseScraper {
.await
.context("Failed to get Redis connection")?;
let key = format!("class:{}", crn);
let key = format!("class:{crn}");
let serialized: Option<String> = conn
.get(&key)
.await

View File

@@ -38,16 +38,16 @@ impl SessionManager {
let start_time = std::time::Instant::now();
let mut session_guard = self.current_session.lock().unwrap();
if let Some(ref session) = *session_guard {
if session.created_at.elapsed() < Self::SESSION_EXPIRY {
let elapsed = start_time.elapsed();
debug!(
session_id = session.session_id,
elapsed = format!("{:.2?}", elapsed),
"reusing existing banner session"
);
return Ok(session.session_id.clone());
}
if let Some(ref session) = *session_guard
&& session.created_at.elapsed() < Self::SESSION_EXPIRY
{
let elapsed = start_time.elapsed();
debug!(
session_id = session.session_id,
elapsed = format!("{:.2?}", elapsed),
"reusing existing banner session"
);
return Ok(session.session_id.clone());
}
// Generate new session
@@ -58,7 +58,7 @@ impl SessionManager {
});
let elapsed = start_time.elapsed();
info!(
debug!(
session_id = session_id,
elapsed = format!("{:.2?}", elapsed),
"generated new banner session"
@@ -87,7 +87,7 @@ impl SessionManager {
let response = self
.client
.get(&url)
.query(&[("_", timestamp_nonce())])
.query(&[("_", Self::nonce())])
.header("User-Agent", user_agent())
.send()
.await?;
@@ -185,15 +185,15 @@ impl SessionManager {
Ok(())
}
}
/// Generates a timestamp-based nonce
fn timestamp_nonce() -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
.to_string()
/// Generates a timestamp-based nonce
pub fn nonce() -> String {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
.to_string()
}
}
/// Returns a browser-like user agent string

View File

@@ -103,7 +103,7 @@ fn parse_course_code(input: &str) -> Result<(i32, i32), Error> {
// Handle single course code
if input.len() == 4 {
let code: i32 = input.parse()?;
if code < 1000 || code > 9999 {
if !(1000..=9999).contains(&code) {
return Err("Course codes must be between 1000 and 9999".into());
}
return Ok((code, code));

View File

@@ -1,5 +1,5 @@
use super::Service;
use crate::web::routes::{BannerState, create_banner_router};
use crate::web::{BannerState, create_banner_router};
use std::net::SocketAddr;
use tokio::net::TcpListener;
use tokio::sync::broadcast;