initial commit
This commit is contained in:
3
src/bin/uniffi-bindgen.rs
Normal file
3
src/bin/uniffi-bindgen.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
239
src/lib.rs
Normal file
239
src/lib.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use chrono::{Datelike, Local, NaiveDate, Weekday};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Once, RwLock};
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
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)]
|
||||
pub struct JecnaSuplClient {
|
||||
provider_url: RwLock<String>,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl JecnaSuplClient {
|
||||
#[uniffi::constructor]
|
||||
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 {
|
||||
provider_url: RwLock::new("https://jecnarozvrh.jzitnik.dev".to_string()),
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_provider(&self, url: String) {
|
||||
let mut provider = self.provider_url.write().unwrap();
|
||||
*provider = url;
|
||||
}
|
||||
|
||||
pub fn get_schedule(&self, class_name: String) -> Result<SuplResult, SuplError> {
|
||||
let url = {
|
||||
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();
|
||||
let class_name_clone = class_name.clone();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
RUNTIME.spawn(async move {
|
||||
let result = async {
|
||||
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| {
|
||||
error!("Failed to read response text: {}", e);
|
||||
SuplError::NetworkError {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
if resp_text.trim().is_empty() {
|
||||
error!("Received empty response body from {}", url);
|
||||
return Err(SuplError::ParseError {
|
||||
reason: "Empty response body".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let resp: ApiResponse = serde_json::from_str(&resp_text).map_err(|e| {
|
||||
error!("JSON parse error: {}. Position: line {}, col {}. Response content: {}", e, e.line(), e.column(), resp_text);
|
||||
SuplError::ParseError {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
debug!("Successfully parsed response");
|
||||
Ok::<ApiResponse, SuplError>(resp)
|
||||
}
|
||||
.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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user