mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
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:
17
providers/base/Cargo.toml
Normal file
17
providers/base/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "base"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio-util = "0.7.16"
|
||||
async-trait = "0.1.89"
|
||||
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_repr = "0.1.20"
|
||||
|
||||
utoipa = { version = "5.4.0", features = ["macros", "chrono"] }
|
||||
|
||||
sha1 = "0.11.0-rc.0"
|
||||
53
providers/base/src/hasher.rs
Normal file
53
providers/base/src/hasher.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use sha1::Digest;
|
||||
use sha1::digest::OutputSizeUser;
|
||||
use sha1::digest::typenum::Unsigned;
|
||||
use std::hash::Hasher;
|
||||
|
||||
/// Hesher returning hash from the algorithm implementing Digest
|
||||
pub struct DigestHasher<D: Digest> {
|
||||
digest: D,
|
||||
}
|
||||
|
||||
impl<D> DigestHasher<D>
|
||||
where
|
||||
D: Digest,
|
||||
{
|
||||
/// Obtain hash.
|
||||
pub fn finalize(self) -> String {
|
||||
static ALPHABET: [char; 16] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
|
||||
];
|
||||
|
||||
let mut hex = String::with_capacity(<D as OutputSizeUser>::OutputSize::USIZE * 2);
|
||||
|
||||
for byte in self.digest.finalize().0.into_iter() {
|
||||
let byte: u8 = byte;
|
||||
|
||||
hex.push(ALPHABET[(byte >> 4) as usize]);
|
||||
hex.push(ALPHABET[(byte & 0xF) as usize]);
|
||||
}
|
||||
|
||||
hex
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> From<D> for DigestHasher<D>
|
||||
where
|
||||
D: Digest,
|
||||
{
|
||||
/// Creating a hash from an algorithm implementing Digest.
|
||||
fn from(digest: D) -> Self {
|
||||
DigestHasher { digest }
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Digest> Hasher for DigestHasher<D> {
|
||||
/// Stopper to prevent calling the standard Hasher result.
|
||||
fn finish(&self) -> u64 {
|
||||
unimplemented!("Do not call finish()");
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
self.digest.update(bytes);
|
||||
}
|
||||
}
|
||||
289
providers/base/src/lib.rs
Normal file
289
providers/base/src/lib.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use crate::hasher::DigestHasher;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
mod hasher;
|
||||
|
||||
// pub(crate) mod internal {
|
||||
// use super::{LessonBoundaries, LessonType};
|
||||
// use chrono::{DateTime, Utc};
|
||||
//
|
||||
// /// Data cell storing the group name.
|
||||
// pub struct GroupCellInfo {
|
||||
// /// Column index.
|
||||
// pub column: u32,
|
||||
//
|
||||
// /// Text in the cell.
|
||||
// pub name: String,
|
||||
// }
|
||||
//
|
||||
// /// Data cell storing the line.
|
||||
// pub struct DayCellInfo {
|
||||
// /// Line index.
|
||||
// pub row: u32,
|
||||
//
|
||||
// /// Column index.
|
||||
// pub column: u32,
|
||||
//
|
||||
// /// Day name.
|
||||
// pub name: String,
|
||||
//
|
||||
// /// Date of the day.
|
||||
// pub date: DateTime<Utc>,
|
||||
// }
|
||||
//
|
||||
// /// Data on the time of lessons from the second column of the schedule.
|
||||
// pub struct BoundariesCellInfo {
|
||||
// /// Temporary segment of the lesson.
|
||||
// pub time_range: LessonBoundaries,
|
||||
//
|
||||
// /// Type of lesson.
|
||||
// pub lesson_type: LessonType,
|
||||
//
|
||||
// /// The lesson index.
|
||||
// pub default_index: Option<u32>,
|
||||
//
|
||||
// /// The frame of the cell.
|
||||
// pub xls_range: ((u32, u32), (u32, u32)),
|
||||
// }
|
||||
// }
|
||||
|
||||
/// The beginning and end of the lesson.
|
||||
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct LessonBoundaries {
|
||||
/// The beginning of a lesson.
|
||||
pub start: DateTime<Utc>,
|
||||
|
||||
/// The end of the lesson.
|
||||
pub end: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Type of lesson.
|
||||
#[derive(Clone, Hash, PartialEq, Debug, Serialize_repr, Deserialize_repr, ToSchema)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[repr(u8)]
|
||||
pub enum LessonType {
|
||||
/// Обычная.
|
||||
Default = 0,
|
||||
|
||||
/// Допы.
|
||||
Additional,
|
||||
|
||||
/// Перемена.
|
||||
Break,
|
||||
|
||||
/// Консультация.
|
||||
Consultation,
|
||||
|
||||
/// Самостоятельная работа.
|
||||
IndependentWork,
|
||||
|
||||
/// Зачёт.
|
||||
Exam,
|
||||
|
||||
/// Зачёт с оценкой.
|
||||
ExamWithGrade,
|
||||
|
||||
/// Экзамен.
|
||||
ExamDefault,
|
||||
|
||||
/// Курсовой проект.
|
||||
CourseProject,
|
||||
|
||||
/// Защита курсового проекта.
|
||||
CourseProjectDefense,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct LessonSubGroup {
|
||||
/// Cabinet, if present.
|
||||
pub cabinet: Option<String>,
|
||||
|
||||
/// Full name of the teacher.
|
||||
pub teacher: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Lesson {
|
||||
/// Type.
|
||||
#[serde(rename = "type")]
|
||||
pub lesson_type: LessonType,
|
||||
|
||||
/// Lesson indexes, if present.
|
||||
pub range: Option<[u8; 2]>,
|
||||
|
||||
/// Name.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The beginning and end.
|
||||
pub time: LessonBoundaries,
|
||||
|
||||
/// List of subgroups.
|
||||
#[serde(rename = "subgroups")]
|
||||
pub subgroups: Option<Vec<Option<LessonSubGroup>>>,
|
||||
|
||||
/// Group name, if this is a schedule for teachers.
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Day {
|
||||
/// Day of the week.
|
||||
pub name: String,
|
||||
|
||||
/// Address of another corps.
|
||||
pub street: Option<String>,
|
||||
|
||||
/// Date.
|
||||
pub date: DateTime<Utc>,
|
||||
|
||||
/// List of lessons on this day.
|
||||
pub lessons: Vec<Lesson>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ScheduleEntry {
|
||||
/// The name of the group or name of the teacher.
|
||||
pub name: String,
|
||||
|
||||
/// List of six days.
|
||||
pub days: Vec<Day>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ParsedSchedule {
|
||||
/// List of groups.
|
||||
pub groups: HashMap<String, ScheduleEntry>,
|
||||
|
||||
/// List of teachers.
|
||||
pub teachers: HashMap<String, ScheduleEntry>,
|
||||
}
|
||||
|
||||
/// Represents a snapshot of the schedule parsed from an XLS file.
|
||||
#[derive(Clone)]
|
||||
pub struct ScheduleSnapshot {
|
||||
/// Timestamp when the Polytechnic website was queried for the schedule.
|
||||
pub fetched_at: DateTime<Utc>,
|
||||
|
||||
/// Timestamp indicating when the schedule was last updated on the Polytechnic website.
|
||||
///
|
||||
/// <note>
|
||||
/// This value is determined by the website's content and does not depend on the application.
|
||||
/// </note>
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
/// URL pointing to the XLS file containing the source schedule data.
|
||||
pub url: String,
|
||||
|
||||
/// Parsed schedule data in the application's internal representation.
|
||||
pub data: ParsedSchedule,
|
||||
}
|
||||
|
||||
impl ScheduleSnapshot {
|
||||
/// Converting the schedule data into a hash.
|
||||
/// ### Important!
|
||||
/// The hash does not depend on the dates.
|
||||
/// If the application is restarted, but the file with source schedule will remain unchanged, then the hash will not change.
|
||||
pub fn hash(&self) -> String {
|
||||
let mut hasher = DigestHasher::from(Sha1::new());
|
||||
|
||||
self.data.teachers.iter().for_each(|e| e.hash(&mut hasher));
|
||||
self.data.groups.iter().for_each(|e| e.hash(&mut hasher));
|
||||
|
||||
hasher.finalize()
|
||||
}
|
||||
|
||||
/// Simply updates the value of [`ScheduleSnapshot::fetched_at`].
|
||||
/// Used for auto-updates.
|
||||
pub fn update(&mut self) {
|
||||
self.fetched_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
// #[derive(Clone, Debug, Display, Error, ToSchema)]
|
||||
// #[display("row {row}, column {column}")]
|
||||
// pub struct ErrorCellPos {
|
||||
// pub row: u32,
|
||||
// pub column: u32,
|
||||
// }
|
||||
//
|
||||
// #[derive(Clone, Debug, Display, Error, ToSchema)]
|
||||
// #[display("'{data}' at {pos}")]
|
||||
// pub struct ErrorCell {
|
||||
// pub pos: ErrorCellPos,
|
||||
// pub data: String,
|
||||
// }
|
||||
//
|
||||
// impl ErrorCell {
|
||||
// pub fn new(row: u32, column: u32, data: String) -> Self {
|
||||
// Self {
|
||||
// pos: ErrorCellPos { row, column },
|
||||
// data,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Clone, Debug, Display, Error, ToSchema)]
|
||||
// pub enum ParseError {
|
||||
// /// Errors related to reading XLS file.
|
||||
// #[display("{_0:?}: Failed to read XLS file.")]
|
||||
// #[schema(value_type = String)]
|
||||
// BadXLS(Arc<calamine::XlsError>),
|
||||
//
|
||||
// /// Not a single sheet was found.
|
||||
// #[display("No work sheets found.")]
|
||||
// NoWorkSheets,
|
||||
//
|
||||
// /// There are no data on the boundaries of the sheet.
|
||||
// #[display("There is no data on work sheet boundaries.")]
|
||||
// UnknownWorkSheetRange,
|
||||
//
|
||||
// /// Failed to read the beginning and end of the lesson from the cell
|
||||
// #[display("Failed to read lesson start and end from {_0}.")]
|
||||
// LessonBoundaries(ErrorCell),
|
||||
//
|
||||
// /// Not found the beginning and the end corresponding to the lesson.
|
||||
// #[display("No start and end times matching the lesson (at {_0}) was found.")]
|
||||
// LessonTimeNotFound(ErrorCellPos),
|
||||
// }
|
||||
//
|
||||
// impl Serialize for ParseError {
|
||||
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
// where
|
||||
// S: Serializer,
|
||||
// {
|
||||
// match self {
|
||||
// ParseError::BadXLS(_) => serializer.serialize_str("BAD_XLS"),
|
||||
// ParseError::NoWorkSheets => serializer.serialize_str("NO_WORK_SHEETS"),
|
||||
// ParseError::UnknownWorkSheetRange => {
|
||||
// serializer.serialize_str("UNKNOWN_WORK_SHEET_RANGE")
|
||||
// }
|
||||
// ParseError::LessonBoundaries(_) => serializer.serialize_str("GLOBAL_TIME"),
|
||||
// ParseError::LessonTimeNotFound(_) => serializer.serialize_str("LESSON_TIME_NOT_FOUND"),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[async_trait]
|
||||
pub trait ScheduleProvider
|
||||
where
|
||||
Self: Sync + Send,
|
||||
{
|
||||
/// Returns ok when task has been canceled.
|
||||
/// Returns err when error appeared while trying to parse or download schedule
|
||||
async fn start_auto_update_task(
|
||||
&self,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
|
||||
|
||||
async fn get_schedule(&self) -> Arc<ScheduleSnapshot>;
|
||||
}
|
||||
Reference in New Issue
Block a user