feat(schedule)!: move schedule parser, downloader, and updater to external library

This can be used to support more schedule formats in the future.
This commit is contained in:
2025-09-02 08:54:22 +04:00
parent 7c973bfda0
commit 5e39fc9acc
37 changed files with 1364 additions and 1271 deletions

View File

@@ -1,69 +1,88 @@
mod env;
mod fcm_client;
mod schedule;
pub use crate::state::env::AppEnv;
use crate::state::fcm_client::FCMClientData;
use crate::xls_downloader::basic_impl::BasicXlsDownloader;
use actix_web::web;
use diesel::{Connection, PgConnection};
use firebase_messaging_rs::FCMClient;
use std::ops::DerefMut;
use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard};
pub use self::schedule::{Schedule, ScheduleSnapshot};
pub use crate::state::env::AppEnv;
use providers::base::{ScheduleProvider, ScheduleSnapshot};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, MutexGuard};
use tokio_util::sync::CancellationToken;
/// Common data provided to endpoints.
pub struct AppState {
cancel_token: CancellationToken,
database: Mutex<PgConnection>,
downloader: Mutex<BasicXlsDownloader>,
schedule: Mutex<Schedule>,
providers: HashMap<String, Arc<dyn ScheduleProvider>>,
env: AppEnv,
fcm_client: Option<Mutex<FCMClient>>,
}
impl AppState {
pub async fn new() -> Result<Self, self::schedule::Error> {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let mut _self = Self {
downloader: Mutex::new(BasicXlsDownloader::new()),
let env = AppEnv::default();
let providers: HashMap<String, Arc<dyn ScheduleProvider>> = HashMap::from([(
"eng_polytechnic".to_string(),
providers::EngelsPolytechnicProvider::new({
#[cfg(test)]
{
providers::EngelsPolytechnicUpdateSource::Prepared(ScheduleSnapshot {
url: "".to_string(),
fetched_at: chrono::DateTime::default(),
updated_at: chrono::DateTime::default(),
data: providers::test_utils::engels_polytechnic::test_result().unwrap(),
})
}
schedule: Mutex::new(Schedule::default()),
#[cfg(not(test))]
{
if let Some(url) = &env.schedule.url {
providers::EngelsPolytechnicUpdateSource::Url(url.clone())
} else {
providers::EngelsPolytechnicUpdateSource::GrabFromSite {
yandex_api_key: env.yandex_cloud.api_key.clone(),
yandex_func_id: env.yandex_cloud.func_id.clone(),
}
}
}
})
.await?,
)]);
let this = Self {
cancel_token: CancellationToken::new(),
database: Mutex::new(
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url)),
),
env: AppEnv::default(),
env,
providers,
fcm_client: FCMClientData::new().await,
};
if _self.env.schedule.auto_update {
_self
.get_schedule()
.await
.init(_self.get_downloader().await.deref_mut(), &_self.env)
.await?;
if this.env.schedule.auto_update {
for (_, provider) in &this.providers {
let provider = provider.clone();
let cancel_token = this.cancel_token.clone();
tokio::spawn(async move { provider.start_auto_update_task(cancel_token).await });
}
}
Ok(_self)
Ok(this)
}
pub async fn get_downloader(&'_ self) -> MutexGuard<'_, BasicXlsDownloader> {
self.downloader.lock().await
}
pub async fn get_schedule_snapshot(&'_ self, provider: &str) -> Option<Arc<ScheduleSnapshot>> {
if let Some(provider) = self.providers.get(provider) {
return Some(provider.get_schedule().await);
}
pub async fn get_schedule(&'_ self) -> MutexGuard<'_, Schedule> {
self.schedule.lock().await
}
pub async fn get_schedule_snapshot(&'_ self) -> MappedMutexGuard<'_, ScheduleSnapshot> {
let snapshot =
MutexGuard::<'_, Schedule>::map(self.schedule.lock().await, |schedule| unsafe {
schedule.snapshot.assume_init_mut()
});
snapshot
None
}
pub async fn get_database(&'_ self) -> MutexGuard<'_, PgConnection> {
@@ -83,6 +102,6 @@ impl AppState {
}
/// Create a new object web::Data<AppState>.
pub async fn new_app_state() -> Result<web::Data<AppState>, self::schedule::Error> {
pub async fn new_app_state() -> Result<web::Data<AppState>, Box<dyn std::error::Error>> {
Ok(web::Data::new(AppState::new().await?))
}