From 8926cd7ecf5ce74b16219cf4cecfa8b6534588b6 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sat, 7 Feb 2026 13:53:54 +0100 Subject: [PATCH] feat: New V3 version --- Cargo.toml | 4 +- src/api.rs | 16 +++++ src/bin/test.rs | 97 +++++++++++++++++++++++++++++ src/lib.rs | 138 ++++------------------------------------- src/models.rs | 127 +++++++++++++++++++++++++++++++++++++ src/schedule.rs | 29 +++++++++ src/teacher_absence.rs | 16 +++++ 7 files changed, 300 insertions(+), 127 deletions(-) create mode 100644 src/api.rs create mode 100644 src/bin/test.rs create mode 100644 src/models.rs create mode 100644 src/schedule.rs create mode 100644 src/teacher_absence.rs diff --git a/Cargo.toml b/Cargo.toml index 9c7c3cb..d9682c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ uniffi = { version = "0.27" } uniffi-cli = ["uniffi/cli"] [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [[bin]] name = "uniffi-bindgen" @@ -26,4 +26,4 @@ opt-level = "z" lto = true codegen-units = 1 panic = "abort" -strip = true +strip = false diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..ba3f9fd --- /dev/null +++ b/src/api.rs @@ -0,0 +1,16 @@ +use crate::models::{ApiResponse, SuplError}; + +pub fn fetch_api(provider_url: &str) -> Result { + let trimmed = provider_url.trim_end_matches('/'); + let url = format!("{}/versioned/v3", trimmed); + + minreq::get(&url) + .send() + .map_err(|e| SuplError::NetworkError { + reason: e.to_string(), + })? + .json() + .map_err(|e| SuplError::ParseError { + reason: e.to_string(), + }) +} diff --git a/src/bin/test.rs b/src/bin/test.rs new file mode 100644 index 0000000..60439f4 --- /dev/null +++ b/src/bin/test.rs @@ -0,0 +1,97 @@ +use jecna_supl_client::{AbsenceEntry, JecnaSuplClient}; + +fn main() { + let client = JecnaSuplClient::new(); + + println!("Fetching schedule for E4..."); + match client.get_schedule("E4".to_string()) { + Ok(result) => { + println!("Last updated: {}", result.status.last_updated); + + if result.schedule.is_empty() { + println!("No substitution schedule found for the upcoming days."); + } + + for (date, day) in result.schedule { + println!("\nDate: {}", date); + println!("In work: {}", day.info.in_work); + if !day.takes_place.is_empty() { + println!("Extra info: {}", day.takes_place); + } + + println!("Changes:"); + for (i, change) in day.changes.iter().enumerate() { + if let Some(c) = change { + println!(" Lesson {}: {}", i + 1, c.text); + } + } + + println!("Absences:"); + for entry in day.absence { + print_absence(entry); + } + } + } + Err(e) => eprintln!("Error fetching schedule: {:?}", e), + } + + println!("\nFetching all teacher absences..."); + match client.get_teacher_absence() { + Ok(result) => { + for (date, entries) in result.absences { + println!("\nDate: {}", date); + for entry in entries { + print_absence(entry); + } + } + } + Err(e) => eprintln!("Error fetching teacher absences: {:?}", e), + } +} + +fn print_absence(entry: AbsenceEntry) { + match entry { + AbsenceEntry::WholeDay { + teacher, + teacher_code, + } => { + println!(" {} ({}): Whole day", teacher, teacher_code); + } + AbsenceEntry::Single { + teacher, + teacher_code, + hours, + } => { + println!(" {} ({}): Lesson {}", teacher, teacher_code, hours); + } + AbsenceEntry::Range { + teacher, + teacher_code, + hours, + } => { + println!( + " {} ({}): Lessons {}-{}", + teacher, teacher_code, hours.from, hours.to + ); + } + AbsenceEntry::Exkurze { + teacher, + teacher_code, + } => { + println!(" {} ({}): Excursion", teacher, teacher_code); + } + AbsenceEntry::Zastoupen { + teacher, + teacher_code, + zastupuje, + } => { + println!( + " {} ({}): Represented by {} ({})", + teacher, teacher_code, zastupuje.teacher, zastupuje.teacher_code + ); + } + AbsenceEntry::Invalid { original } => { + println!(" Invalid entry: {}", original); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 04a480f..bd93939 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,57 +1,14 @@ -use chrono::{Datelike, Local, NaiveDate, Weekday}; -use serde::Deserialize; -use std::collections::HashMap; +mod api; +mod models; +mod schedule; +mod teacher_absence; + use std::sync::RwLock; +pub use models::*; + uniffi::setup_scaffolding!(); -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum SuplError { - #[error("Network error: {reason}")] - NetworkError { reason: String }, - #[error("Parse error: {reason}")] - ParseError { reason: String }, - #[error("Invalid date format: {reason}")] - DateFormatError { reason: String }, - #[error("Internal runtime error: {reason}")] - RuntimeError { reason: String }, -} - -#[derive(Deserialize)] -struct ApiResponse { - schedule: Vec>, - props: Vec, - status: Status, -} - -#[derive(Deserialize)] -struct DayProp { - date: String, - #[allow(dead_code)] - priprava: bool, -} - -#[derive(Debug, Deserialize, uniffi::Record)] -pub struct Status { - #[serde(rename = "lastUpdated")] - pub last_updated: String, - - #[serde(rename = "currentUpdateSchedule")] - pub current_update_schedule: u16, -} - -#[derive(uniffi::Record)] -pub struct SuplResult { - pub status: Status, - pub schedule: Vec, -} - -#[derive(uniffi::Record)] -pub struct DailySchedule { - pub date: String, - pub lessons: Vec>, -} - #[derive(uniffi::Object)] pub struct JecnaSuplClient { provider_url: RwLock, @@ -72,81 +29,12 @@ impl JecnaSuplClient { } pub fn get_schedule(&self, class_name: String) -> Result { - let url = { - let provider = self.provider_url.read().unwrap(); - let trimmed = provider.trim_end_matches('/'); - format!("{}/versioned/v2", trimmed) - }; + let provider = self.provider_url.read().unwrap(); + schedule::get_schedule_impl(&provider, class_name) + } - let resp: ApiResponse = minreq::get(&url) - .send() - .map_err(|e| SuplError::NetworkError { - reason: e.to_string(), - })? - .json() - .map_err(|e| SuplError::ParseError { - reason: e.to_string(), - })?; - - let today = Local::now().date_naive(); - let current_weekday = today.weekday(); - - let start_date; - let end_date; - - match current_weekday { - Weekday::Sat | Weekday::Sun => { - let days_until_mon = 7 - current_weekday.num_days_from_monday(); - let next_mon = today + chrono::Duration::days(days_until_mon as i64); - start_date = next_mon; - end_date = next_mon + chrono::Duration::days(4); - } - _ => { - let days_from_mon = current_weekday.num_days_from_monday(); - let curr_mon = today - chrono::Duration::days(days_from_mon as i64); - start_date = curr_mon; - end_date = curr_mon + chrono::Duration::days(4); - } - } - - let mut output = Vec::new(); - - for (i, day_prop) in resp.props.iter().enumerate() { - let date = NaiveDate::parse_from_str(&day_prop.date, "%Y-%m-%d").map_err(|e| { - SuplError::DateFormatError { - reason: e.to_string(), - } - })?; - - if date >= start_date && date <= end_date { - if let Some(day_schedule_map) = resp.schedule.get(i) { - if let Some(class_schedule_val) = day_schedule_map.get(&class_name) { - match serde_json::from_value::>>( - class_schedule_val.clone(), - ) { - Ok(lessons) => { - output.push(DailySchedule { - date: day_prop.date.clone(), - lessons, - }); - } - Err(e) => { - return Err(SuplError::ParseError { - reason: format!( - "Invalid schedule format for class {}: {}", - class_name, e - ), - }); - } - } - } - } - } - } - - Ok(SuplResult { - status: resp.status, - schedule: output, - }) + pub fn get_teacher_absence(&self) -> Result { + let provider = self.provider_url.read().unwrap(); + teacher_absence::get_teacher_absence_impl(&provider) } } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..82487f2 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,127 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum SuplError { + #[error("Network error: {reason}")] + NetworkError { reason: String }, + #[error("Parse error: {reason}")] + ParseError { reason: String }, + #[error("Invalid date format: {reason}")] + DateFormatError { reason: String }, + #[error("Internal runtime error: {reason}")] + RuntimeError { reason: String }, +} + +#[derive(Deserialize, Debug, uniffi::Enum, Clone)] +#[serde(tag = "type")] +pub enum AbsenceEntry { + #[serde(rename = "wholeDay")] + WholeDay { + teacher: String, + #[serde(rename = "teacherCode")] + teacher_code: String, + }, + #[serde(rename = "single")] + Single { + teacher: String, + #[serde(rename = "teacherCode")] + teacher_code: String, + hours: u16, + }, + #[serde(rename = "range")] + Range { + teacher: String, + #[serde(rename = "teacherCode")] + teacher_code: String, + hours: AbsenceRange, + }, + #[serde(rename = "exkurze")] + Exkurze { + teacher: String, + #[serde(rename = "teacherCode")] + teacher_code: String, + }, + #[serde(rename = "zastoupen")] + Zastoupen { + teacher: String, + #[serde(rename = "teacherCode")] + teacher_code: String, + zastupuje: SubstituteInfo, + }, + #[serde(rename = "invalid")] + Invalid { original: String }, +} + +#[derive(Deserialize, Debug, uniffi::Record, Clone)] +pub struct AbsenceRange { + pub from: u16, + pub to: u16, +} + +#[derive(Deserialize, Debug, uniffi::Record, Clone)] +pub struct SubstituteInfo { + pub teacher: String, + #[serde(rename = "teacherCode")] + pub teacher_code: String, +} + +#[derive(Deserialize, Debug, uniffi::Record, Clone)] +pub struct ChangeEntry { + pub text: String, + #[serde(rename = "backgroundColor")] + pub background_color: Option, + #[serde(rename = "willBeSpecified")] + pub will_be_specified: Option, +} + +#[derive(Deserialize)] +pub struct ApiResponse { + pub status: Status, + pub schedule: HashMap, +} + +#[derive(Deserialize, Clone)] +pub struct DailyData { + pub info: DayInfo, + pub changes: HashMap>>, + pub absence: Vec, + #[serde(rename = "takesPlace")] + pub takes_place: String, + #[serde(rename = "reservedRooms")] + pub reserved_rooms: Vec, +} + +#[derive(Deserialize, Debug, uniffi::Record, Clone)] +pub struct DayInfo { + #[serde(rename = "inWork")] + pub in_work: bool, +} + +#[derive(Debug, Deserialize, uniffi::Record, Clone)] +pub struct Status { + #[serde(rename = "lastUpdated")] + pub last_updated: String, + + #[serde(rename = "currentUpdateSchedule")] + pub current_update_schedule: u16, +} + +#[derive(uniffi::Record)] +pub struct SuplResult { + pub status: Status, + pub schedule: HashMap, +} + +#[derive(uniffi::Record)] +pub struct DailySchedule { + pub info: DayInfo, + pub changes: Vec>, + pub absence: Vec, + pub takes_place: String, +} + +#[derive(uniffi::Record)] +pub struct TeacherAbsenceResult { + pub absences: HashMap>, +} diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000..1bc103f --- /dev/null +++ b/src/schedule.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use crate::api::fetch_api; +use crate::models::{DailySchedule, SuplError, SuplResult}; + +pub fn get_schedule_impl(provider_url: &str, class_name: String) -> Result { + let resp = fetch_api(provider_url)?; + + let mut schedule_output = HashMap::new(); + + for (date, daily_data) in resp.schedule { + if let Some(class_changes) = daily_data.changes.get(&class_name) { + schedule_output.insert( + date, + DailySchedule { + info: daily_data.info, + changes: class_changes.clone(), + absence: daily_data.absence, + takes_place: daily_data.takes_place, + }, + ); + } + } + + Ok(SuplResult { + status: resp.status, + schedule: schedule_output, + }) +} diff --git a/src/teacher_absence.rs b/src/teacher_absence.rs new file mode 100644 index 0000000..0038d69 --- /dev/null +++ b/src/teacher_absence.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use crate::api::fetch_api; +use crate::models::{SuplError, TeacherAbsenceResult}; + +pub fn get_teacher_absence_impl(provider_url: &str) -> Result { + let resp = fetch_api(provider_url)?; + + let mut output = HashMap::new(); + + for (date, daily_data) in resp.schedule { + output.insert(date, daily_data.absence); + } + + Ok(TeacherAbsenceResult { absences: output }) +}