feat: implement comprehensive course data model with reference cache and search

This commit is contained in:
2026-01-28 21:06:29 -06:00
parent e3b855b956
commit 6df4303bd6
16 changed files with 1121 additions and 76 deletions
+66
View File
@@ -2,16 +2,71 @@
use crate::banner::BannerApi;
use crate::banner::Course;
use crate::data::models::ReferenceData;
use crate::status::ServiceStatusRegistry;
use anyhow::Result;
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// In-memory cache for reference data (code→description lookups).
///
/// Loaded from the `reference_data` table on startup and refreshed periodically.
pub struct ReferenceCache {
/// `(category, code)` → `description`
data: HashMap<(String, String), String>,
}
impl Default for ReferenceCache {
fn default() -> Self {
Self::new()
}
}
impl ReferenceCache {
/// Create an empty cache.
pub fn new() -> Self {
Self {
data: HashMap::new(),
}
}
/// Build cache from a list of reference data entries.
pub fn from_entries(entries: Vec<ReferenceData>) -> Self {
let data = entries
.into_iter()
.map(|e| ((e.category, e.code), e.description))
.collect();
Self { data }
}
/// Look up a description by category and code.
pub fn lookup(&self, category: &str, code: &str) -> Option<&str> {
self.data
.get(&(category.to_string(), code.to_string()))
.map(|s| s.as_str())
}
/// Get all `(code, description)` pairs for a category, sorted by description.
pub fn entries_for_category(&self, category: &str) -> Vec<(&str, &str)> {
let mut entries: Vec<(&str, &str)> = self
.data
.iter()
.filter(|((cat, _), _)| cat == category)
.map(|((_, code), desc)| (code.as_str(), desc.as_str()))
.collect();
entries.sort_by(|a, b| a.1.cmp(b.1));
entries
}
}
#[derive(Clone)]
pub struct AppState {
pub banner_api: Arc<BannerApi>,
pub db_pool: PgPool,
pub service_statuses: ServiceStatusRegistry,
pub reference_cache: Arc<RwLock<ReferenceCache>>,
}
impl AppState {
@@ -20,9 +75,20 @@ impl AppState {
banner_api,
db_pool,
service_statuses: ServiceStatusRegistry::new(),
reference_cache: Arc::new(RwLock::new(ReferenceCache::new())),
}
}
/// Initialize the reference cache from the database.
pub async fn load_reference_cache(&self) -> Result<()> {
let entries = crate::data::reference::get_all(&self.db_pool).await?;
let count = entries.len();
let cache = ReferenceCache::from_entries(entries);
*self.reference_cache.write().await = cache;
tracing::info!(entries = count, "Reference cache loaded");
Ok(())
}
/// Get a course by CRN directly from Banner API
pub async fn get_course_or_fetch(&self, term: &str, crn: &str) -> Result<Course> {
self.banner_api