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 anyhow::Result;
use redis::AsyncCommands; use redis::AsyncCommands;
use redis::Client; use redis::Client;
use serde_json;
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -31,7 +30,7 @@ impl AppState {
pub async fn get_course_or_fetch(&self, term: &str, crn: &str) -> Result<Course> { 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 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? { if let Some(serialized) = conn.get::<_, Option<String>>(&key).await? {
let course: Course = serde_json::from_str(&serialized)?; let course: Course = serde_json::from_str(&serialized)?;
return Ok(course); return Ok(course);
@@ -44,6 +43,6 @@ impl AppState {
return Ok(course); 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), ("searchTerm", search),
("offset", &page.to_string()), ("offset", &page.to_string()),
("max", &max_results.to_string()), ("max", &max_results.to_string()),
("_", &timestamp_nonce()), ("_", &SessionManager::nonce()),
]; ];
let response = self let response = self
@@ -104,7 +104,7 @@ impl BannerApi {
("offset", &offset.to_string()), ("offset", &offset.to_string()),
("max", &max_results.to_string()), ("max", &max_results.to_string()),
("uniqueSessionId", &session_id), ("uniqueSessionId", &session_id),
("_", &timestamp_nonce()), ("_", &SessionManager::nonce()),
]; ];
let response = self let response = self
@@ -143,7 +143,7 @@ impl BannerApi {
("offset", &offset.to_string()), ("offset", &offset.to_string()),
("max", &max_results.to_string()), ("max", &max_results.to_string()),
("uniqueSessionId", &session_id), ("uniqueSessionId", &session_id),
("_", &timestamp_nonce()), ("_", &SessionManager::nonce()),
]; ];
let response = self let response = self
@@ -182,7 +182,7 @@ impl BannerApi {
("offset", &offset.to_string()), ("offset", &offset.to_string()),
("max", &max_results.to_string()), ("max", &max_results.to_string()),
("uniqueSessionId", &session_id), ("uniqueSessionId", &session_id),
("_", &timestamp_nonce()), ("_", &SessionManager::nonce()),
]; ];
let response = self 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. /// Returns a browser-like user agent string.
fn user_agent() -> &'static str { 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" "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), Ok(value) => Ok(value),
Err(err) => { Err(err) => {
let (line, column) = (err.line(), err.column()); 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!( Err(anyhow::anyhow!(
"{} at line {}, column {}\nSnippet:\n{}", "{err} at line {line}, column {column}\nSnippet:\n{snippet}",
err,
line,
column,
snippet
)) ))
} }
} }
@@ -430,5 +417,5 @@ fn build_error_snippet(body: &str, line: usize, column: usize, max_len: usize) -
indicator.push('^'); indicator.push('^');
} }
format!("{}\n{}", slice, indicator) format!("{slice}\n{indicator}")
} }

View File

@@ -1,7 +1,7 @@
use bitflags::{Flags, bitflags}; use bitflags::{Flags, bitflags};
use chrono::{DateTime, NaiveDate, NaiveTime, Timelike, Utc}; use chrono::{DateTime, NaiveDate, NaiveTime, Timelike, Utc};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::{cmp::Ordering, str::FromStr}; use std::{cmp::Ordering, fmt::Display, str::FromStr};
use super::terms::Term; use super::terms::Term;
@@ -148,30 +148,17 @@ pub enum DayOfWeek {
impl DayOfWeek { impl DayOfWeek {
/// Convert to short string representation /// Convert to short string representation
pub fn to_short_string(&self) -> &'static str { pub fn to_short_string(self) -> &'static str {
match self { match self {
DayOfWeek::Monday => "M", DayOfWeek::Monday => "Mo",
DayOfWeek::Tuesday => "Tu", DayOfWeek::Tuesday => "Tu",
DayOfWeek::Wednesday => "W", DayOfWeek::Wednesday => "We",
DayOfWeek::Thursday => "Th", DayOfWeek::Thursday => "Th",
DayOfWeek::Friday => "F", DayOfWeek::Friday => "Fr",
DayOfWeek::Saturday => "Sa", DayOfWeek::Saturday => "Sa",
DayOfWeek::Sunday => "Su", 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 { impl TryFrom<MeetingDays> for DayOfWeek {
@@ -196,10 +183,9 @@ impl TryFrom<MeetingDays> for DayOfWeek {
}); });
} }
return Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"Cannot convert multiple days to a single day: {:?}", "Cannot convert multiple days to a single day: {days:?}"
days ))
));
} }
} }
@@ -252,15 +238,8 @@ impl TimeRange {
let hour = time.hour(); let hour = time.hour();
let minute = time.minute(); let minute = time.minute();
if hour == 0 { let meridiem = if hour < 12 { "AM" } else { "PM" };
format!("12:{:02}AM", minute) format!("{hour}:{minute:02}{meridiem}")
} 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)
}
} }
/// Get duration in minutes /// Get duration in minutes
@@ -376,15 +355,20 @@ impl MeetingLocation {
is_online, is_online,
} }
} }
}
/// Convert to formatted string impl Display for MeetingLocation {
pub fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_online { if self.is_online {
"Online".to_string() write!(f, "Online")
} else { } else {
format!( write!(
"{} | {} | {} {}", f,
self.campus, self.building_description, self.building, self.room "{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 /// Returns the current term status for a specific date
pub fn get_status_for_date(date: NaiveDate) -> TermPoint { pub fn get_status_for_date(date: NaiveDate) -> TermPoint {
let literal_year = date.year() as u32; 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); 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. // 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 { fn get_season_ranges(year: u32) -> SeasonRanges {
let spring_start = NaiveDate::from_ymd_opt(year as i32, 1, 14) let spring_start = NaiveDate::from_ymd_opt(year as i32, 1, 14)
.unwrap() .unwrap()
.ordinal() as u32; .ordinal();
let spring_end = NaiveDate::from_ymd_opt(year as i32, 5, 1) let spring_end = NaiveDate::from_ymd_opt(year as i32, 5, 1)
.unwrap() .unwrap()
.ordinal() as u32; .ordinal();
let summer_start = NaiveDate::from_ymd_opt(year as i32, 5, 25) let summer_start = NaiveDate::from_ymd_opt(year as i32, 5, 25)
.unwrap() .unwrap()
.ordinal() as u32; .ordinal();
let summer_end = NaiveDate::from_ymd_opt(year as i32, 8, 15) let summer_end = NaiveDate::from_ymd_opt(year as i32, 8, 15)
.unwrap() .unwrap()
.ordinal() as u32; .ordinal();
let fall_start = NaiveDate::from_ymd_opt(year as i32, 8, 18) let fall_start = NaiveDate::from_ymd_opt(year as i32, 8, 18)
.unwrap() .unwrap()
.ordinal() as u32; .ordinal();
let fall_end = NaiveDate::from_ymd_opt(year as i32, 12, 10) let fall_end = NaiveDate::from_ymd_opt(year as i32, 12, 10)
.unwrap() .unwrap()
.ordinal() as u32; .ordinal();
SeasonRanges { SeasonRanges {
spring: YearDayRange { spring: YearDayRange {
@@ -179,10 +179,15 @@ struct YearDayRange {
end: u32, 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 /// Returns the term in the format YYYYXX, where YYYY is the year and XX is the season code
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
format!("{}{}", self.year, self.season.to_str()) write!(
f,
"{year}{season}",
year = self.year,
season = self.season.to_str()
)
} }
} }
@@ -215,7 +220,7 @@ impl FromStr for Season {
"10" => Season::Fall, "10" => Season::Fall,
"20" => Season::Spring, "20" => Season::Spring,
"30" => Season::Summer, "30" => Season::Summer,
_ => return Err(anyhow::anyhow!("Invalid season: {}", s)), _ => return Err(anyhow::anyhow!("Invalid season: {s}")),
}; };
Ok(season) Ok(season)
} }

View File

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

View File

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

View File

@@ -38,8 +38,9 @@ impl SessionManager {
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
let mut session_guard = self.current_session.lock().unwrap(); let mut session_guard = self.current_session.lock().unwrap();
if let Some(ref session) = *session_guard { if let Some(ref session) = *session_guard
if session.created_at.elapsed() < Self::SESSION_EXPIRY { && session.created_at.elapsed() < Self::SESSION_EXPIRY
{
let elapsed = start_time.elapsed(); let elapsed = start_time.elapsed();
debug!( debug!(
session_id = session.session_id, session_id = session.session_id,
@@ -48,7 +49,6 @@ impl SessionManager {
); );
return Ok(session.session_id.clone()); return Ok(session.session_id.clone());
} }
}
// Generate new session // Generate new session
let session_id = self.generate_session_id(); let session_id = self.generate_session_id();
@@ -58,7 +58,7 @@ impl SessionManager {
}); });
let elapsed = start_time.elapsed(); let elapsed = start_time.elapsed();
info!( debug!(
session_id = session_id, session_id = session_id,
elapsed = format!("{:.2?}", elapsed), elapsed = format!("{:.2?}", elapsed),
"generated new banner session" "generated new banner session"
@@ -87,7 +87,7 @@ impl SessionManager {
let response = self let response = self
.client .client
.get(&url) .get(&url)
.query(&[("_", timestamp_nonce())]) .query(&[("_", Self::nonce())])
.header("User-Agent", user_agent()) .header("User-Agent", user_agent())
.send() .send()
.await?; .await?;
@@ -185,15 +185,15 @@ impl SessionManager {
Ok(()) Ok(())
} }
}
/// Generates a timestamp-based nonce /// Generates a timestamp-based nonce
fn timestamp_nonce() -> String { pub fn nonce() -> String {
std::time::SystemTime::now() std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap()
.as_millis() .as_millis()
.to_string() .to_string()
}
} }
/// Returns a browser-like user agent 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 // Handle single course code
if input.len() == 4 { if input.len() == 4 {
let code: i32 = input.parse()?; 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 Err("Course codes must be between 1000 and 9999".into());
} }
return Ok((code, code)); return Ok((code, code));

View File

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