//! Application state shared across components (bot, web, scheduler). use crate::banner::BannerApi; use crate::banner::Course; use anyhow::Result; use redis::AsyncCommands; use redis::Client; use std::sync::Arc; #[derive(Clone)] pub struct AppState { pub banner_api: Arc, pub redis: Arc, } impl AppState { pub fn new( banner_api: Arc, redis_url: &str, ) -> Result> { let redis_client = Client::open(redis_url)?; Ok(Self { banner_api, redis: Arc::new(redis_client), }) } /// Get a course by CRN with Redis cache fallback to Banner API 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}"); if let Some(serialized) = conn.get::<_, Option>(&key).await? { let course: Course = serde_json::from_str(&serialized)?; return Ok(course); } // Fallback: fetch from Banner API if let Some(course) = self.banner_api.get_course_by_crn(term, crn).await? { let serialized = serde_json::to_string(&course)?; let _: () = conn.set(&key, serialized).await?; return Ok(course); } Err(anyhow::anyhow!("Course not found for CRN {crn}")) } }