From cb8a595326bcf2aa679ac381070ddd54ceb047a9 Mon Sep 17 00:00:00 2001 From: Xevion Date: Wed, 27 Aug 2025 12:19:17 -0500 Subject: [PATCH] chore: solve lints, improve formatting --- src/app_state.rs | 5 ++- src/banner/api.rs | 27 ++++------------ src/banner/models/meetings.rs | 58 ++++++++++++--------------------- src/banner/models/terms.rs | 27 +++++++++------- src/banner/query.rs | 12 +++---- src/banner/scraper.rs | 60 +++++++++++++++++------------------ src/banner/session.rs | 40 +++++++++++------------ src/bot/commands/search.rs | 2 +- src/services/web.rs | 2 +- 9 files changed, 103 insertions(+), 130 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 49a28c4..c4228fa 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -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 { 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>(&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}")) } } diff --git a/src/banner/api.rs b/src/banner/api.rs index ecf8930..6228eb0 100644 --- a/src/banner/api.rs +++ b/src/banner/api.rs @@ -65,7 +65,7 @@ impl BannerApi { ("searchTerm", search), ("offset", &page.to_string()), ("max", &max_results.to_string()), - ("_", ×tamp_nonce()), + ("_", &SessionManager::nonce()), ]; let response = self @@ -104,7 +104,7 @@ impl BannerApi { ("offset", &offset.to_string()), ("max", &max_results.to_string()), ("uniqueSessionId", &session_id), - ("_", ×tamp_nonce()), + ("_", &SessionManager::nonce()), ]; let response = self @@ -143,7 +143,7 @@ impl BannerApi { ("offset", &offset.to_string()), ("max", &max_results.to_string()), ("uniqueSessionId", &session_id), - ("_", ×tamp_nonce()), + ("_", &SessionManager::nonce()), ]; let response = self @@ -182,7 +182,7 @@ impl BannerApi { ("offset", &offset.to_string()), ("max", &max_results.to_string()), ("uniqueSessionId", &session_id), - ("_", ×tamp_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(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}") } diff --git a/src/banner/models/meetings.rs b/src/banner/models/meetings.rs index 8921dfa..b8ac460 100644 --- a/src/banner/models/meetings.rs +++ b/src/banner/models/meetings.rs @@ -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 for DayOfWeek { @@ -196,10 +183,9 @@ impl TryFrom 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 ) } } diff --git a/src/banner/models/terms.rs b/src/banner/models/terms.rs index 8f98471..aec869b 100644 --- a/src/banner/models/terms.rs +++ b/src/banner/models/terms.rs @@ -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) } diff --git a/src/banner/query.rs b/src/banner/query.rs index 0bb0ed2..0def026 100644 --- a/src/banner/query.rs +++ b/src/banner/query.rs @@ -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::>() .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)); diff --git a/src/banner/scraper.rs b/src/banner/scraper.rs index 06e30b8..e322a48 100644 --- a/src/banner/scraper.rs +++ b/src/banner/scraper.rs @@ -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 = 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 = conn .get(&key) .await diff --git a/src/banner/session.rs b/src/banner/session.rs index 1a5e7e4..c1f0953 100644 --- a/src/banner/session.rs +++ b/src/banner/session.rs @@ -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 diff --git a/src/bot/commands/search.rs b/src/bot/commands/search.rs index 579f300..2097944 100644 --- a/src/bot/commands/search.rs +++ b/src/bot/commands/search.rs @@ -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)); diff --git a/src/services/web.rs b/src/services/web.rs index b176298..ef60592 100644 --- a/src/services/web.rs +++ b/src/services/web.rs @@ -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;