Compare commits
13 Commits
f8e8077963
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
021e030ded
|
|||
|
bb0b31e84d
|
|||
|
549968dbf3
|
|||
|
9c9fbfeede
|
|||
|
5d21a8ad9e
|
|||
|
7738043a80
|
|||
|
7090595505
|
|||
|
0f48c24009
|
|||
|
2595cc5cf0
|
|||
|
c59b55c66b
|
|||
|
a3cfcd52c5
|
|||
|
8926cd7ecf
|
|||
|
0176945bd6
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,9 +1,7 @@
|
|||||||
# Rust
|
# Rust
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
/build_all.sh
|
/bindings/kotlin/src/main/kotlin/cz/jzitnik
|
||||||
|
|
||||||
/bindings/kotlin/src/main/kotlin/uniffi/
|
|
||||||
/bindings/kotlin/src/main/resources/lib/
|
/bindings/kotlin/src/main/resources/lib/
|
||||||
/bindings/kotlin/src/main/resources/linux-x86-64/
|
/bindings/kotlin/src/main/resources/linux-x86-64/
|
||||||
/bindings/kotlin/src/main/resources/win32-x86-64/
|
/bindings/kotlin/src/main/resources/win32-x86-64/
|
||||||
|
|||||||
1386
Cargo.lock
generated
1386
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
44
Cargo.toml
44
Cargo.toml
@@ -1,22 +1,36 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "jecna_supl_client"
|
name = "jecna_supl_client"
|
||||||
version = "0.1.1"
|
version = "0.2.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||||
futures = "0.3.31"
|
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
|
||||||
once_cell = "1.21.3"
|
thiserror = { version = "2.0.11", default-features = false }
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
|
uniffi = { version = "0.27", default-features = false }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
thiserror = "2.0.18"
|
minreq = { version = "2.14", default-features = false, features = ["json-using-serde", "https-native"] }
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
|
||||||
tracing = "0.1.44"
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
tracing-subscriber = "0.3.22"
|
minreq = { version = "2.14", default-features = false, features = ["json-using-serde", "https-rustls"] }
|
||||||
android_logger = "0.14"
|
|
||||||
log = "0.4"
|
|
||||||
uniffi = { version = "0.27", features = ["cli"] }
|
[features]
|
||||||
|
uniffi-cli = ["uniffi/cli"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "uniffi-bindgen"
|
||||||
|
required-features = ["uniffi-cli"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
strip = false
|
||||||
|
incremental = false
|
||||||
|
debug = false
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "cz.jzitnik"
|
group = "cz.jzitnik"
|
||||||
version = "0.1.1"
|
version = "0.2.3"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>cz.jzitnik</groupId>
|
<groupId>cz.jzitnik</groupId>
|
||||||
<artifactId>jecna-supl-client</artifactId>
|
<artifactId>jecna-supl-client</artifactId>
|
||||||
<version>0.1.1</version>
|
<version>0.2.3</version>
|
||||||
<name>Jecna Supl Client</name>
|
<name>Jecna Supl Client</name>
|
||||||
<description>Kotlin bindings for the Jecna Supl Rust library</description>
|
<description>Kotlin bindings for the Jecna Supl Rust library</description>
|
||||||
<url>https://gitea.jzitnik.dev/jzitnik/jecna-supl-client</url>
|
<url>https://gitea.jzitnik.dev/jzitnik/jecna-supl-client</url>
|
||||||
|
|||||||
88
build_all.sh
Executable file
88
build_all.sh
Executable file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#export ANDROID_NDK_HOME=/home/kuba/.Android/Sdk/ndk/29.0.14206865/
|
||||||
|
export ANDROID_NDK_HOME=/home/kuba/.Android/Sdk/ndk/27.0.12077973/
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PROJECT_NAME="jecna_supl_client"
|
||||||
|
LIB_NAME="lib${PROJECT_NAME}"
|
||||||
|
KOTLIN_RES_DIR="bindings/kotlin/src/main/resources"
|
||||||
|
KOTLIN_SRC_DIR="bindings/kotlin/src/main/kotlin"
|
||||||
|
|
||||||
|
# Targets
|
||||||
|
LINUX_TARGET="x86_64-unknown-linux-gnu"
|
||||||
|
ANDROID_TARGETS=("aarch64-linux-android" "armv7-linux-androideabi" "x86_64-linux-android")
|
||||||
|
WINDOWS_TARGET="x86_64-pc-windows-gnu"
|
||||||
|
|
||||||
|
echo "=== Building for Linux ($LINUX_TARGET) ==="
|
||||||
|
cargo build --release --target $LINUX_TARGET
|
||||||
|
|
||||||
|
echo "=== Building for Android ==="
|
||||||
|
for target in "${ANDROID_TARGETS[@]}"; do
|
||||||
|
echo "--- Building $target ---"
|
||||||
|
cargo ndk -t $target build --release
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Building for Windows ($WINDOWS_TARGET) ==="
|
||||||
|
if rustup target list --installed | grep -q "$WINDOWS_TARGET" && command -v x86_64-w64-mingw32-gcc >/dev/null; then
|
||||||
|
cargo build --release --target $WINDOWS_TARGET
|
||||||
|
else
|
||||||
|
echo "Warning: Windows target or x86_64-w64-mingw32-gcc not found. Skipping Windows build."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Generating Kotlin Bindings ==="
|
||||||
|
# Use the Linux library for metadata extraction
|
||||||
|
GEN_LIB_PATH="target/$LINUX_TARGET/release/${LIB_NAME}.so"
|
||||||
|
if [ ! -f "$GEN_LIB_PATH" ]; then
|
||||||
|
# Fallback to host build if linux target failed/was different
|
||||||
|
GEN_LIB_PATH="target/release/${LIB_NAME}.so"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo run --features uniffi-cli --bin uniffi-bindgen generate \
|
||||||
|
--library "$GEN_LIB_PATH" \
|
||||||
|
--language kotlin \
|
||||||
|
--out-dir "$KOTLIN_SRC_DIR" \
|
||||||
|
--no-format
|
||||||
|
|
||||||
|
echo "=== Organizing Native Libraries ==="
|
||||||
|
|
||||||
|
if [ -n "$ANDROID_NDK_HOME" ] && [ -f "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" ]; then
|
||||||
|
STRIP="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip"
|
||||||
|
else
|
||||||
|
STRIP="strip"
|
||||||
|
fi
|
||||||
|
echo "Using strip tool: $STRIP"
|
||||||
|
|
||||||
|
# Linux x86_64
|
||||||
|
mkdir -p "$KOTLIN_RES_DIR/linux-x86-64"
|
||||||
|
cp "target/$LINUX_TARGET/release/${LIB_NAME}.so" "$KOTLIN_RES_DIR/linux-x86-64/${LIB_NAME}.so"
|
||||||
|
"$STRIP" --strip-all "$KOTLIN_RES_DIR/linux-x86-64/${LIB_NAME}.so"
|
||||||
|
|
||||||
|
# Android
|
||||||
|
mkdir -p "$KOTLIN_RES_DIR/lib/arm64-v8a"
|
||||||
|
cp "target/aarch64-linux-android/release/${LIB_NAME}.so" "$KOTLIN_RES_DIR/lib/arm64-v8a/${LIB_NAME}.so"
|
||||||
|
"$STRIP" --strip-all "$KOTLIN_RES_DIR/lib/arm64-v8a/${LIB_NAME}.so"
|
||||||
|
|
||||||
|
mkdir -p "$KOTLIN_RES_DIR/lib/armeabi-v7a"
|
||||||
|
cp "target/armv7-linux-androideabi/release/${LIB_NAME}.so" "$KOTLIN_RES_DIR/lib/armeabi-v7a/${LIB_NAME}.so"
|
||||||
|
"$STRIP" --strip-all "$KOTLIN_RES_DIR/lib/armeabi-v7a/${LIB_NAME}.so"
|
||||||
|
|
||||||
|
mkdir -p "$KOTLIN_RES_DIR/lib/x86_64"
|
||||||
|
cp "target/x86_64-linux-android/release/${LIB_NAME}.so" "$KOTLIN_RES_DIR/lib/x86_64/${LIB_NAME}.so"
|
||||||
|
"$STRIP" --strip-all "$KOTLIN_RES_DIR/lib/x86_64/${LIB_NAME}.so"
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
WIN_DLL="target/$WINDOWS_TARGET/release/${PROJECT_NAME}.dll"
|
||||||
|
if [ -f "$WIN_DLL" ]; then
|
||||||
|
mkdir -p "$KOTLIN_RES_DIR/win32-x86-64"
|
||||||
|
cp "$WIN_DLL" "$KOTLIN_RES_DIR/win32-x86-64/${PROJECT_NAME}.dll"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This is just for me cuz I have memory like a goldfish and I will forget how to upload it to central... Leave me alone
|
||||||
|
echo "=== Build Complete ==="
|
||||||
|
echo "To upload to Sonatype Central:"
|
||||||
|
echo "1. cd bindings/kotlin"
|
||||||
|
echo "2. JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew clean zipBundle"
|
||||||
|
echo "3. Upload the file 'bindings/kotlin/build/distributions/sonatype-bundle.zip'"
|
||||||
|
echo " at https://central.sonatype.com/publishing/deployments"
|
||||||
5
src/all.rs
Normal file
5
src/all.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use crate::{api::fetch_api, ApiResponse, SuplError};
|
||||||
|
|
||||||
|
pub fn get_all_impl(provider_url: &str) -> Result<ApiResponse, SuplError> {
|
||||||
|
fetch_api(provider_url)
|
||||||
|
}
|
||||||
49
src/api.rs
Normal file
49
src/api.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::models::{ApiResponse, SuplError};
|
||||||
|
|
||||||
|
pub fn fetch_api(provider_url: &str) -> Result<ApiResponse, SuplError> {
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct StatusResponse {
|
||||||
|
working: bool,
|
||||||
|
message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_api(provider_url: &str) -> Result<(), SuplError> {
|
||||||
|
let trimmed = provider_url.trim_end_matches('/');
|
||||||
|
let url = format!("{}/status", trimmed);
|
||||||
|
|
||||||
|
let status: StatusResponse = minreq::get(&url)
|
||||||
|
.send()
|
||||||
|
.map_err(|e| SuplError::NetworkError {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?
|
||||||
|
.json()
|
||||||
|
.map_err(|e| SuplError::ParseError {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if status.working {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SuplError::ParseError {
|
||||||
|
reason: status
|
||||||
|
.message
|
||||||
|
.unwrap_or_else(|| "Provider reported not working".to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
238
src/lib.rs
238
src/lib.rs
@@ -1,93 +1,30 @@
|
|||||||
use chrono::{Datelike, Local, NaiveDate, Weekday};
|
mod api;
|
||||||
use once_cell::sync::Lazy;
|
mod models;
|
||||||
use serde::Deserialize;
|
mod schedule;
|
||||||
use std::collections::HashMap;
|
mod teacher_absence;
|
||||||
use std::sync::{Once, RwLock};
|
mod all;
|
||||||
use tokio::runtime::Runtime;
|
mod report;
|
||||||
use tracing::{debug, error, info};
|
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
pub use models::*;
|
||||||
|
pub use report::ReportLocation;
|
||||||
|
|
||||||
|
use crate::report::{report_impl, ReportLocation};
|
||||||
|
|
||||||
uniffi::setup_scaffolding!();
|
uniffi::setup_scaffolding!();
|
||||||
|
|
||||||
static RUNTIME: Lazy<Runtime> =
|
|
||||||
Lazy::new(|| Runtime::new().expect("Failed to create Tokio runtime"));
|
|
||||||
|
|
||||||
static INIT_TRACING: Once = Once::new();
|
|
||||||
|
|
||||||
#[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<HashMap<String, serde_json::Value>>,
|
|
||||||
props: Vec<DayProp>,
|
|
||||||
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<DailySchedule>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(uniffi::Record)]
|
|
||||||
pub struct DailySchedule {
|
|
||||||
pub date: String,
|
|
||||||
pub lessons: Vec<Option<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(uniffi::Object)]
|
#[derive(uniffi::Object)]
|
||||||
pub struct JecnaSuplClient {
|
pub struct JecnaSuplClient {
|
||||||
provider_url: RwLock<String>,
|
provider_url: RwLock<String>,
|
||||||
client: reqwest::Client,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
impl JecnaSuplClient {
|
impl JecnaSuplClient {
|
||||||
#[uniffi::constructor]
|
#[uniffi::constructor]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
INIT_TRACING.call_once(|| {
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
android_logger::init_once(
|
|
||||||
android_logger::Config::default()
|
|
||||||
.with_max_level(log::LevelFilter::Debug)
|
|
||||||
.with_tag("JecnaSuplClient"),
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
|
||||||
.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
info!("Initializing JecnaSuplClient");
|
|
||||||
Self {
|
Self {
|
||||||
provider_url: RwLock::new("https://jecnarozvrh.jzitnik.dev".to_string()),
|
provider_url: RwLock::new("https://jecnarozvrh.jzitnik.dev".to_string()),
|
||||||
client: reqwest::Client::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,143 +34,30 @@ impl JecnaSuplClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_schedule(&self, class_name: String) -> Result<SuplResult, SuplError> {
|
pub fn get_schedule(&self, class_name: String) -> Result<SuplResult, SuplError> {
|
||||||
let url = {
|
let provider = self.provider_url.read().unwrap();
|
||||||
let provider = self.provider_url.read().unwrap();
|
|
||||||
let trimmed = provider.trim_end_matches('/');
|
|
||||||
format!("{}/versioned/v2", trimmed)
|
|
||||||
};
|
|
||||||
debug!("Target URL: {}", url);
|
|
||||||
|
|
||||||
let client = self.client.clone();
|
api::test_api(&provider)?;
|
||||||
let class_name_clone = class_name.clone();
|
schedule::get_schedule_impl(&provider, class_name)
|
||||||
|
}
|
||||||
|
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
pub fn get_teacher_absence(&self) -> Result<TeacherAbsenceResult, SuplError> {
|
||||||
|
let provider = self.provider_url.read().unwrap();
|
||||||
|
|
||||||
RUNTIME.spawn(async move {
|
api::test_api(&provider)?;
|
||||||
let result = async {
|
teacher_absence::get_teacher_absence_impl(&provider)
|
||||||
let resp = client.get(&url).send().await.map_err(|e| {
|
}
|
||||||
error!("Network request failed: {}", e);
|
|
||||||
SuplError::NetworkError {
|
|
||||||
reason: e.to_string(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let resp_text = resp.text().await.map_err(|e| {
|
pub fn get_all(&self) -> Result<ApiResponse, SuplError> {
|
||||||
error!("Failed to read response text: {}", e);
|
let provider = self.provider_url.read().unwrap();
|
||||||
SuplError::NetworkError {
|
|
||||||
reason: e.to_string(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if resp_text.trim().is_empty() {
|
api::test_api(&provider)?;
|
||||||
error!("Received empty response body from {}", url);
|
all::get_all_impl(&provider)
|
||||||
return Err(SuplError::ParseError {
|
}
|
||||||
reason: "Empty response body".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp: ApiResponse = serde_json::from_str(&resp_text).map_err(|e| {
|
pub fn report(&self, content: String, class: String, report_location: ReportLocation) -> Result<(), SuplError> {
|
||||||
error!("JSON parse error: {}. Position: line {}, col {}. Response content: {}", e, e.line(), e.column(), resp_text);
|
let provider = self.provider_url.read().unwrap();
|
||||||
SuplError::ParseError {
|
|
||||||
reason: e.to_string(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
debug!("Successfully parsed response");
|
api::test_api(&provider)?;
|
||||||
Ok::<ApiResponse, SuplError>(resp)
|
report_impl(&provider, content, class, report_location)
|
||||||
}
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let _ = tx.send(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = rx.recv().map_err(|e| {
|
|
||||||
error!("Mpsc receive error: {}", e);
|
|
||||||
SuplError::RuntimeError {
|
|
||||||
reason: "Channel closed".to_string(),
|
|
||||||
}
|
|
||||||
})??;
|
|
||||||
|
|
||||||
let today = Local::now().date_naive();
|
|
||||||
let current_weekday = today.weekday();
|
|
||||||
debug!("Current date: {}, weekday: {:?}", today, current_weekday);
|
|
||||||
|
|
||||||
let start_date;
|
|
||||||
let end_date;
|
|
||||||
|
|
||||||
match current_weekday {
|
|
||||||
Weekday::Sat | Weekday::Sun => {
|
|
||||||
// Next Monday
|
|
||||||
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); // Mon -> Fri is 4 days diff
|
|
||||||
info!(
|
|
||||||
"Weekend detected. Showing next week: {} to {}",
|
|
||||||
start_date, end_date
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Current Monday
|
|
||||||
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);
|
|
||||||
info!(
|
|
||||||
"Weekday detected. Showing current week: {} to {}",
|
|
||||||
start_date, end_date
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
|
|
||||||
for (i, day_prop) in result.props.iter().enumerate() {
|
|
||||||
let date = NaiveDate::parse_from_str(&day_prop.date, "%Y-%m-%d").map_err(|e| {
|
|
||||||
error!("Date parsing error for {}: {}", day_prop.date, e);
|
|
||||||
SuplError::DateFormatError {
|
|
||||||
reason: e.to_string(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if date >= start_date && date <= end_date {
|
|
||||||
if let Some(day_schedule_map) = result.schedule.get(i) {
|
|
||||||
if let Some(class_schedule_val) = day_schedule_map.get(&class_name_clone) {
|
|
||||||
match serde_json::from_value::<Vec<Option<String>>>(
|
|
||||||
class_schedule_val.clone(),
|
|
||||||
) {
|
|
||||||
Ok(lessons) => {
|
|
||||||
output.push(DailySchedule {
|
|
||||||
date: day_prop.date.clone(),
|
|
||||||
lessons,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Failed to parse schedule for class {}: {}",
|
|
||||||
class_name_clone, e
|
|
||||||
);
|
|
||||||
return Err(SuplError::ParseError {
|
|
||||||
reason: format!(
|
|
||||||
"Invalid schedule format for class {}",
|
|
||||||
class_name_clone
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
"Class {} not found in schedule for {}",
|
|
||||||
class_name_clone, date
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(SuplResult {
|
|
||||||
status: result.status,
|
|
||||||
schedule: output,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/models.rs
Normal file
129
src/models.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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: Option<String>,
|
||||||
|
#[serde(rename = "teacherCode")]
|
||||||
|
teacher_code: String,
|
||||||
|
},
|
||||||
|
#[serde(rename = "single")]
|
||||||
|
Single {
|
||||||
|
teacher: Option<String>,
|
||||||
|
#[serde(rename = "teacherCode")]
|
||||||
|
teacher_code: String,
|
||||||
|
hours: u16,
|
||||||
|
},
|
||||||
|
#[serde(rename = "range")]
|
||||||
|
Range {
|
||||||
|
teacher: Option<String>,
|
||||||
|
#[serde(rename = "teacherCode")]
|
||||||
|
teacher_code: String,
|
||||||
|
hours: AbsenceRange,
|
||||||
|
},
|
||||||
|
#[serde(rename = "exkurze")]
|
||||||
|
Exkurze {
|
||||||
|
teacher: Option<String>,
|
||||||
|
#[serde(rename = "teacherCode")]
|
||||||
|
teacher_code: String,
|
||||||
|
},
|
||||||
|
#[serde(rename = "zastoupen")]
|
||||||
|
Zastoupen {
|
||||||
|
teacher: Option<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: Option<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<String>,
|
||||||
|
#[serde(rename = "foregroundColor")]
|
||||||
|
pub foreground_color: Option<String>,
|
||||||
|
#[serde(rename = "willBeSpecified")]
|
||||||
|
pub will_be_specified: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, uniffi::Record)]
|
||||||
|
pub struct ApiResponse {
|
||||||
|
pub status: Status,
|
||||||
|
pub schedule: HashMap<String, DailyData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Debug, uniffi::Record)]
|
||||||
|
pub struct DailyData {
|
||||||
|
pub info: DayInfo,
|
||||||
|
pub changes: HashMap<String, Vec<Option<ChangeEntry>>>,
|
||||||
|
pub absence: Vec<AbsenceEntry>,
|
||||||
|
#[serde(rename = "takesPlace")]
|
||||||
|
pub takes_place: String,
|
||||||
|
#[serde(rename = "reservedRooms")]
|
||||||
|
pub reserved_rooms: Vec<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String, DailySchedule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(uniffi::Record)]
|
||||||
|
pub struct DailySchedule {
|
||||||
|
pub info: DayInfo,
|
||||||
|
pub changes: Vec<Option<ChangeEntry>>,
|
||||||
|
pub absence: Vec<AbsenceEntry>,
|
||||||
|
pub takes_place: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(uniffi::Record)]
|
||||||
|
pub struct TeacherAbsenceResult {
|
||||||
|
pub absences: HashMap<String, Vec<AbsenceEntry>>,
|
||||||
|
}
|
||||||
61
src/report.rs
Normal file
61
src/report.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::SuplError;
|
||||||
|
|
||||||
|
#[derive(uniffi::Enum)]
|
||||||
|
pub enum ReportLocation {
|
||||||
|
Timetable,
|
||||||
|
TeacherAbsence,
|
||||||
|
TakesPlace
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportLocation {
|
||||||
|
fn to_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ReportLocation::Timetable => "TIMETABLE",
|
||||||
|
ReportLocation::TeacherAbsence => "ABSENCE",
|
||||||
|
ReportLocation::TakesPlace => "TAKES_PLACE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ReportRequest {
|
||||||
|
content: String,
|
||||||
|
class: String,
|
||||||
|
location: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_impl(
|
||||||
|
provider: &str,
|
||||||
|
content: String,
|
||||||
|
class: String,
|
||||||
|
report_location: ReportLocation,
|
||||||
|
) -> Result<(), SuplError> {
|
||||||
|
let trimmed = provider.trim_end_matches('/');
|
||||||
|
let url = format!("{}/report", trimmed);
|
||||||
|
|
||||||
|
let body = ReportRequest {
|
||||||
|
content,
|
||||||
|
class,
|
||||||
|
location: report_location.to_str().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&body)
|
||||||
|
.map_err(|e| SuplError::ParseError { reason: e.to_string() })?;
|
||||||
|
|
||||||
|
let response = minreq::post(url)
|
||||||
|
.with_header("Content-Type", "application/json")
|
||||||
|
.with_body(json)
|
||||||
|
.send()
|
||||||
|
.map_err(|e| SuplError::NetworkError { reason: e.to_string() })?;
|
||||||
|
|
||||||
|
if response.status_code >= 400 {
|
||||||
|
return Err(SuplError::RuntimeError {
|
||||||
|
reason: format!("Server returned HTTP {}", response.status_code),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
29
src/schedule.rs
Normal file
29
src/schedule.rs
Normal file
@@ -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<SuplResult, SuplError> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
16
src/teacher_absence.rs
Normal file
16
src/teacher_absence.rs
Normal file
@@ -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<TeacherAbsenceResult, SuplError> {
|
||||||
|
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 })
|
||||||
|
}
|
||||||
2
uniffi.toml
Normal file
2
uniffi.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[bindings.kotlin]
|
||||||
|
package_name = "cz.jzitnik.jecna_supl_client"
|
||||||
Reference in New Issue
Block a user