Реализованы все требуемые эндпоинты schedule.

Улучшена документация.
This commit is contained in:
2025-03-28 23:24:37 +04:00
parent 30c985a3d7
commit 680419ea78
32 changed files with 998 additions and 257 deletions

View File

@@ -5,7 +5,7 @@ use crate::routes::auth::shared::parse_vk_id;
use crate::routes::auth::sign_in::schema::SignInData::{Default, Vk};
use crate::routes::schema::user::UserResponse;
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use crate::{AppState, utility};
use crate::{utility, AppState};
use actix_web::{post, web};
use diesel::SaveChangesDsl;
use std::ops::DerefMut;
@@ -55,7 +55,7 @@ async fn sign_in(
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
))]
#[post("/sign-in")]
pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>) -> Response {
pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>) -> ServiceResponse {
sign_in(Default(data.into_inner()), &app_state).await.into()
}
@@ -64,7 +64,7 @@ pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
))]
#[post("/sign-in-vk")]
pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> ServiceResponse {
let data = data_json.into_inner();
match parse_vk_id(&data.access_token) {
@@ -74,51 +74,57 @@ pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppSt
}
mod schema {
use crate::routes::schema::PartialStatusCode;
use crate::routes::schema::user::UserResponse;
use actix_macros::IntoResponseError;
use actix_web::http::StatusCode;
use actix_macros::{IntoResponseError, StatusCode};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
#[derive(Deserialize, Serialize, ToSchema)]
#[schema(as = SignIn::Request)]
pub struct Request {
/// Имя пользователя
#[schema(examples("n08i40k"))]
pub username: String,
/// Пароль
pub password: String,
}
pub mod vk {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(as = SignInVk::Request)]
pub struct Request {
/// Токен VK ID
pub access_token: String,
}
}
pub type Response = crate::routes::schema::Response<UserResponse, ErrorCode>;
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
#[derive(Serialize, utoipa::ToSchema, Clone, IntoResponseError)]
#[derive(Serialize, ToSchema, Clone, IntoResponseError, StatusCode)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = SignIn::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
pub enum ErrorCode {
/// Некорректное имя пользователя или пароль
IncorrectCredentials,
/// Недействительный токен VK ID
InvalidVkAccessToken,
}
impl PartialStatusCode for ErrorCode {
fn status_code(&self) -> StatusCode {
StatusCode::NOT_ACCEPTABLE
}
}
/// Internal
/// Тип авторизации
pub enum SignInData {
/// Имя пользователя и пароль
Default(Request),
/// Идентификатор привязанного аккаунта VK
Vk(i32),
}
}
@@ -136,7 +142,7 @@ mod tests {
use actix_web::http::Method;
use actix_web::http::StatusCode;
use actix_web::test;
use sha2::{Digest, Sha256};
use sha1::{Digest, Sha1};
use std::fmt::Write;
async fn sign_in_client(data: Request) -> ServiceResponse {
@@ -152,7 +158,7 @@ mod tests {
fn prepare(username: String) {
let id = {
let mut sha = Sha256::new();
let mut sha = Sha1::new();
sha.update(&username);
let result = sha.finalize();

View File

@@ -50,7 +50,10 @@ async fn sign_up(
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
))]
#[post("/sign-up")]
pub async fn sign_up_default(data_json: Json<Request>, app_state: web::Data<AppState>) -> Response {
pub async fn sign_up_default(
data_json: Json<Request>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
let data = data_json.into_inner();
sign_up(
@@ -73,7 +76,10 @@ pub async fn sign_up_default(data_json: Json<Request>, app_state: web::Data<AppS
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
))]
#[post("/sign-up-vk")]
pub async fn sign_up_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
pub async fn sign_up_vk(
data_json: Json<vk::Request>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
let data = data_json.into_inner();
match parse_vk_id(&data.access_token) {
@@ -107,11 +113,9 @@ pub async fn sign_up_vk(data_json: Json<vk::Request>, app_state: web::Data<AppSt
mod schema {
use crate::database::models::{User, UserRole};
use crate::routes::schema::PartialStatusCode;
use crate::routes::schema::user::UserResponse;
use crate::utility;
use actix_macros::{IntoResponseError, StatusCode};
use actix_web::http::StatusCode;
use objectid::ObjectId;
use serde::{Deserialize, Serialize};
@@ -120,16 +124,21 @@ mod schema {
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = SignUp::Request)]
pub struct Request {
/// Имя пользователя
#[schema(examples("n08i40k"))]
pub username: String,
/// Пароль
pub password: String,
/// Группа
#[schema(examples("ИС-214/23"))]
pub group: String,
/// Роль
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
#[schema(examples("3.0.0"))]
pub version: String,
}
@@ -142,43 +151,71 @@ mod schema {
#[serde(rename_all = "camelCase")]
#[schema(as = SignUpVk::Request)]
pub struct Request {
/// Токен VK ID
pub access_token: String,
/// Имя пользователя
#[schema(examples("n08i40k"))]
pub username: String,
/// Группа
#[schema(examples("ИС-214/23"))]
pub group: String,
/// Роль
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
#[schema(examples("3.0.0"))]
pub version: String,
}
}
pub type Response = crate::routes::schema::Response<UserResponse, ErrorCode>;
pub type ServiceResponse = crate::routes::schema::Response<UserResponse, ErrorCode>;
#[derive(Clone, Serialize, utoipa::ToSchema, IntoResponseError, StatusCode)]
#[status_code = "StatusCode::NOT_ACCEPTABLE"]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = SignUp::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
pub enum ErrorCode {
/// Передана роль ADMIN
DisallowedRole,
/// Неизвестное название группы
InvalidGroupName,
/// Пользователь с таким именем уже зарегистрирован
UsernameAlreadyExists,
/// Недействительный токен VK ID
InvalidVkAccessToken,
/// Пользователь с таким аккаунтом VK уже зарегистрирован
VkAlreadyExists,
}
/// Internal
/// Данные для регистрации
pub struct SignUpData {
/// Имя пользователя
pub username: String,
/// Пароль
///
/// Должен присутствовать даже если регистрация происходит с помощью токена VK ID
pub password: String,
/// Идентификатор аккаунта VK
pub vk_id: Option<i32>,
/// Группа
pub group: String,
/// Роль
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
pub version: String,
}

View File

@@ -0,0 +1,23 @@
use crate::AppState;
use crate::routes::schedule::schema::CacheStatus;
use actix_web::{get, web};
#[utoipa::path(responses(
(status = OK, body = CacheStatus),
))]
#[get("/cache-status")]
pub async fn get_cache_status(app_state: web::Data<AppState>) -> CacheStatus {
// Prevent thread lock
let has_schedule = app_state
.schedule
.lock()
.as_ref()
.map(|res| res.is_some())
.unwrap();
match has_schedule {
true => CacheStatus::from(&app_state),
false => CacheStatus::default(),
}
.into()
}

View File

@@ -0,0 +1,99 @@
use self::schema::*;
use crate::AppState;
use crate::database::models::User;
use crate::extractors::base::SyncExtractor;
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use actix_web::{get, web};
#[utoipa::path(responses(
(status = OK, body = Response),
(
status = SERVICE_UNAVAILABLE,
body = ResponseError<ErrorCode>,
example = json!({
"code": "NO_SCHEDULE",
"message": "Schedule not parsed yet."
})
),
(
status = NOT_FOUND,
body = ResponseError<ErrorCode>,
example = json!({
"code": "NOT_FOUND",
"message": "Required group not found."
})
),
))]
#[get("/group")]
pub async fn get_group(
user: SyncExtractor<User>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
// Prevent thread lock
let schedule_lock = app_state.schedule.lock().unwrap();
match schedule_lock.as_ref() {
None => ErrorCode::NoSchedule.into_response(),
Some(schedule) => match schedule.data.groups.get(&user.into_inner().group) {
None => ErrorCode::NotFound.into_response(),
Some(entry) => Ok(entry.clone().into()).into(),
},
}
}
mod schema {
use crate::parser::schema::ScheduleEntry;
use actix_macros::{IntoResponseErrorNamed, StatusCode};
use chrono::{DateTime, NaiveDateTime, Utc};
use derive_more::Display;
use serde::Serialize;
use utoipa::ToSchema;
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
#[derive(Serialize, ToSchema)]
#[schema(as = GetGroup::Response)]
#[serde(rename_all = "camelCase")]
pub struct Response {
/// Расписание группы
pub group: ScheduleEntry,
/// Устаревшая переменная
///
/// По умолчанию возвращается пустой список
#[deprecated = "Will be removed in future versions"]
pub updated: Vec<i32>,
/// Устаревшая переменная
///
/// По умолчанию начальная дата по Unix
#[deprecated = "Will be removed in future versions"]
pub updated_at: DateTime<Utc>,
}
#[allow(deprecated)]
impl From<ScheduleEntry> for Response {
fn from(group: ScheduleEntry) -> Self {
Self {
group,
updated: Vec::new(),
updated_at: NaiveDateTime::default().and_utc(),
}
}
}
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = GroupSchedule::ErrorCode)]
pub enum ErrorCode {
/// Расписания ещё не получены
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
#[display("Schedule not parsed yet.")]
NoSchedule,
/// Группа не найдена
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
#[display("Required group not found.")]
NotFound,
}
}

View File

@@ -0,0 +1,48 @@
use self::schema::*;
use crate::AppState;
use crate::routes::schedule::schema::ErrorCode;
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use actix_web::{get, web};
#[utoipa::path(responses(
(status = OK, body = Response),
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>),
))]
#[get("/group-names")]
pub async fn get_group_names(app_state: web::Data<AppState>) -> ServiceResponse {
// Prevent thread lock
let schedule_lock = app_state.schedule.lock().unwrap();
match schedule_lock.as_ref() {
None => ErrorCode::NoSchedule.into_response(),
Some(schedule) => {
let mut names: Vec<String> = schedule.data.groups.keys().cloned().collect();
names.sort();
Ok(names.into()).into()
}
}
.into()
}
mod schema {
use crate::routes::schedule::schema::ErrorCode;
use serde::Serialize;
use utoipa::ToSchema;
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
#[derive(Serialize, ToSchema)]
#[schema(as = GetGroupNames::Response)]
pub struct Response {
/// Список названий групп отсортированный в алфавитном порядке
#[schema(examples(json!(["ИС-214/23"])))]
pub names: Vec<String>,
}
impl From<Vec<String>> for Response {
fn from(names: Vec<String>) -> Self {
Self { names }
}
}
}

View File

@@ -1,6 +1,6 @@
use self::schema::*;
use crate::app_state::AppState;
use crate::routes::schedule::schema::{Error, ScheduleView};
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use actix_web::{get, web};
@@ -9,30 +9,17 @@ use actix_web::{get, web};
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>)
))]
#[get("/")]
pub async fn get_schedule(app_state: web::Data<AppState>) -> Response {
match ScheduleView::try_from(app_state.get_ref()) {
pub async fn get_schedule(app_state: web::Data<AppState>) -> ServiceResponse {
match ScheduleView::try_from(&app_state) {
Ok(res) => Ok(res).into(),
Err(e) => match e {
Error::NoSchedule => ErrorCode::NoSchedule.into_response(),
ErrorCode::NoSchedule => ErrorCode::NoSchedule.into_response(),
},
}
}
mod schema {
use crate::routes::schedule::schema::ScheduleView;
use actix_macros::{IntoResponseErrorNamed, StatusCode};
use derive_more::Display;
use serde::Serialize;
use utoipa::ToSchema;
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
pub type Response = crate::routes::schema::Response<ScheduleView, ErrorCode>;
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = ScheduleView::ErrorCode)]
pub enum ErrorCode {
#[display("Schedule not parsed yet")]
NoSchedule,
}
pub type ServiceResponse = crate::routes::schema::Response<ScheduleView, ErrorCode>;
}

View File

@@ -0,0 +1,97 @@
use self::schema::*;
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use crate::AppState;
use actix_web::{get, web};
#[utoipa::path(responses(
(status = OK, body = Response),
(
status = SERVICE_UNAVAILABLE,
body = ResponseError<ErrorCode>,
example = json!({
"code": "NO_SCHEDULE",
"message": "Schedule not parsed yet."
})
),
(
status = NOT_FOUND,
body = ResponseError<ErrorCode>,
example = json!({
"code": "NOT_FOUND",
"message": "Required teacher not found."
})
),
))]
#[get("/teacher/{name}")]
pub async fn get_teacher(
name: web::Path<String>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
// Prevent thread lock
let schedule_lock = app_state.schedule.lock().unwrap();
match schedule_lock.as_ref() {
None => ErrorCode::NoSchedule.into_response(),
Some(schedule) => match schedule.data.teachers.get(&name.into_inner()) {
None => ErrorCode::NotFound.into_response(),
Some(entry) => Ok(entry.clone().into()).into(),
},
}
}
mod schema {
use crate::parser::schema::ScheduleEntry;
use actix_macros::{IntoResponseErrorNamed, StatusCode};
use chrono::{DateTime, NaiveDateTime, Utc};
use derive_more::Display;
use serde::Serialize;
use utoipa::ToSchema;
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
#[derive(Serialize, ToSchema)]
#[schema(as = GetTeacher::Response)]
#[serde(rename_all = "camelCase")]
pub struct Response {
/// Расписание преподавателя
pub teacher: ScheduleEntry,
/// Устаревшая переменная
///
/// По умолчанию возвращается пустой список
#[deprecated = "Will be removed in future versions"]
pub updated: Vec<i32>,
/// Устаревшая переменная
///
/// По умолчанию начальная дата по Unix
#[deprecated = "Will be removed in future versions"]
pub updated_at: DateTime<Utc>,
}
#[allow(deprecated)]
impl From<ScheduleEntry> for Response {
fn from(teacher: ScheduleEntry) -> Self {
Self {
teacher,
updated: Vec::new(),
updated_at: NaiveDateTime::default().and_utc(),
}
}
}
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = TeacherSchedule::ErrorCode)]
pub enum ErrorCode {
/// Расписания ещё не получены
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
#[display("Schedule not parsed yet.")]
NoSchedule,
/// Преподаватель не найден
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
#[display("Required teacher not found.")]
NotFound,
}
}

View File

@@ -0,0 +1,48 @@
use self::schema::*;
use crate::AppState;
use crate::routes::schedule::schema::ErrorCode;
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use actix_web::{get, web};
#[utoipa::path(responses(
(status = OK, body = Response),
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>),
))]
#[get("/teacher-names")]
pub async fn get_teacher_names(app_state: web::Data<AppState>) -> ServiceResponse {
// Prevent thread lock
let schedule_lock = app_state.schedule.lock().unwrap();
match schedule_lock.as_ref() {
None => ErrorCode::NoSchedule.into_response(),
Some(schedule) => {
let mut names: Vec<String> = schedule.data.teachers.keys().cloned().collect();
names.sort();
Ok(names.into()).into()
}
}
.into()
}
mod schema {
use crate::routes::schedule::schema::ErrorCode;
use serde::Serialize;
use utoipa::ToSchema;
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
#[derive(Serialize, ToSchema)]
#[schema(as = GetTeacherNames::Response)]
pub struct Response {
/// Список имён преподавателей отсортированный в алфавитном порядке
#[schema(examples(json!(["Хомченко Н.Е."])))]
pub names: Vec<String>,
}
impl From<Vec<String>> for Response {
fn from(names: Vec<String>) -> Self {
Self { names }
}
}
}

View File

@@ -1,2 +1,8 @@
pub mod get_cache_status;
pub mod get_schedule;
mod schema;
pub mod get_group;
pub mod get_group_names;
pub mod get_teacher;
pub mod get_teacher_names;
mod schema;
pub mod update_download_url;

View File

@@ -1,47 +1,107 @@
use crate::app_state::AppState;
use crate::app_state::{AppState, Schedule};
use crate::parser::schema::ScheduleEntry;
use chrono::{DateTime, Utc};
use serde::Serialize;
use actix_macros::{IntoResponseErrorNamed, ResponderJson, StatusCode};
use actix_web::web;
use chrono::{DateTime, Duration, Utc};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use utoipa::ToSchema;
/// Ответ от сервера с расписаниями
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleView {
/// ETag расписания на сервере политехникума
etag: String,
replacer_id: Option<String>,
/// Дата обновления расписания на сайте политехникума
uploaded_at: DateTime<Utc>,
/// Дата последнего скачивания расписания с сервера политехникума
downloaded_at: DateTime<Utc>,
/// Расписание групп
groups: HashMap<String, ScheduleEntry>,
/// Расписание преподавателей
teachers: HashMap<String, ScheduleEntry>,
updated_groups: Vec<Vec<i32>>,
updated_teachers: Vec<Vec<i32>>,
}
pub enum Error {
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = ScheduleShared::ErrorCode)]
pub enum ErrorCode {
/// Расписания ещё не получены
#[display("Schedule not parsed yet.")]
NoSchedule,
}
impl TryFrom<&AppState> for ScheduleView {
type Error = Error;
fn try_from(app_state: &AppState) -> Result<Self, Self::Error> {
let schedule_lock = app_state.schedule.lock().unwrap();
if let Some(schedule_ref) = schedule_lock.as_ref() {
let schedule = schedule_ref.clone();
impl TryFrom<&web::Data<AppState>> for ScheduleView {
type Error = ErrorCode;
fn try_from(app_state: &web::Data<AppState>) -> Result<Self, Self::Error> {
if let Some(schedule) = app_state.schedule.lock().unwrap().clone() {
Ok(Self {
etag: schedule.etag,
replacer_id: None,
uploaded_at: schedule.updated_at,
downloaded_at: schedule.parsed_at,
groups: schedule.data.groups,
teachers: schedule.data.teachers,
updated_groups: vec![],
updated_teachers: vec![],
})
} else {
Err(Error::NoSchedule)
Err(ErrorCode::NoSchedule)
}
}
}
/// Статус кешированного расписаний
#[derive(Serialize, Deserialize, ToSchema, ResponderJson)]
#[serde(rename_all = "camelCase")]
pub struct CacheStatus {
/// Хеш расписаний
pub cache_hash: String,
/// Требуется ли обновить ссылку на расписание
pub cache_update_required: bool,
/// Дата последнего обновления кеша
pub last_cache_update: i64,
/// Дата обновления кешированного расписания
///
/// Определяется сервером политехникума
pub last_schedule_update: i64,
}
impl CacheStatus {
pub fn default() -> Self {
CacheStatus {
cache_hash: "0000000000000000000000000000000000000000".to_string(),
cache_update_required: true,
last_cache_update: 0,
last_schedule_update: 0,
}
}
}
impl From<&web::Data<AppState>> for CacheStatus {
fn from(value: &web::Data<AppState>) -> Self {
let schedule_lock = value.schedule.lock().unwrap();
let schedule = schedule_lock.as_ref().unwrap();
CacheStatus::from(schedule)
}
}
impl From<&Schedule> for CacheStatus {
fn from(value: &Schedule) -> Self {
Self {
cache_hash: value.hash(),
cache_update_required: (value.fetched_at - Utc::now()) > Duration::minutes(5),
last_cache_update: value.fetched_at.timestamp(),
last_schedule_update: value.updated_at.timestamp(),
}
}
}

View File

@@ -0,0 +1,132 @@
use self::schema::*;
use crate::AppState;
use crate::app_state::Schedule;
use crate::parser::parse_xls;
use crate::routes::schedule::schema::CacheStatus;
use crate::routes::schema::{IntoResponseAsError, ResponseError};
use crate::xls_downloader::interface::XLSDownloader;
use actix_web::web::Json;
use actix_web::{patch, web};
use chrono::Utc;
#[utoipa::path(responses(
(status = OK, body = CacheStatus),
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>),
))]
#[patch("/update-download-url")]
pub async fn update_download_url(
data: Json<Request>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
if !data.url.starts_with("https://politehnikum-eng.ru/") {
return ErrorCode::NonWhitelistedHost.into_response();
}
let mut downloader = app_state.downloader.lock().unwrap();
if let Some(url) = &downloader.url {
if url.eq(&data.url) {
return Ok(CacheStatus::from(&app_state)).into();
}
}
match downloader.set_url(data.url.clone()).await {
Ok(fetch_result) => {
let mut schedule = app_state.schedule.lock().unwrap();
if schedule.is_some()
&& fetch_result.uploaded_at < schedule.as_ref().unwrap().updated_at
{
return ErrorCode::OutdatedSchedule.into_response();
}
match downloader.fetch(false).await {
Ok(download_result) => match parse_xls(download_result.data.as_ref().unwrap()) {
Ok(data) => {
*schedule = Some(Schedule {
etag: download_result.etag,
fetched_at: download_result.requested_at,
updated_at: download_result.uploaded_at,
parsed_at: Utc::now(),
data,
});
Ok(CacheStatus::from(schedule.as_ref().unwrap())).into()
}
Err(error) => ErrorCode::InvalidSchedule(error).into_response(),
},
Err(error) => {
eprintln!("Unknown url provided {}", data.url);
eprintln!("{:?}", error);
ErrorCode::DownloadFailed.into_response()
}
}
}
Err(error) => {
eprintln!("Unknown url provided {}", data.url);
eprintln!("{:?}", error);
ErrorCode::FetchFailed.into_response()
}
}
}
mod schema {
use crate::parser::schema::ParseError;
use crate::routes::schedule::schema::CacheStatus;
use actix_macros::{IntoResponseErrorNamed, StatusCode};
use derive_more::Display;
use serde::{Deserialize, Serialize, Serializer};
use utoipa::ToSchema;
pub type ServiceResponse = crate::routes::schema::Response<CacheStatus, ErrorCode>;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Request {
/// Ссылка на расписание
pub url: String,
}
#[derive(Clone, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
#[schema(as = SetDownloadUrl::ErrorCode)]
pub enum ErrorCode {
/// Передана ссылка с хостом отличающимся от politehnikum-eng.ru
#[display("URL with unknown host provided. Provide url with politehnikum-eng.ru host.")]
NonWhitelistedHost,
/// Не удалось получить мета-данные файла
#[display("Unable to retrieve metadata from the specified URL.")]
FetchFailed,
/// Не удалось скачать файл
#[display("Unable to retrieve data from the specified URL.")]
DownloadFailed,
/// Ссылка ведёт на устаревшее расписание
///
/// Под устаревшим расписанием подразумевается расписание, которое было опубликовано раньше, чем уже имеется на данный момент
#[display("The schedule is older than it already is.")]
OutdatedSchedule,
/// Не удалось преобразовать расписание
#[display("{}", "_0.display()")]
InvalidSchedule(ParseError),
}
impl Serialize for ErrorCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ErrorCode::NonWhitelistedHost => serializer.serialize_str("NON_WHITELISTED_HOST"),
ErrorCode::FetchFailed => serializer.serialize_str("FETCH_FAILED"),
ErrorCode::DownloadFailed => serializer.serialize_str("DOWNLOAD_FAILED"),
ErrorCode::OutdatedSchedule => serializer.serialize_str("OUTDATED_SCHEDULE"),
ErrorCode::InvalidSchedule(_) => serializer.serialize_str("INVALID_SCHEDULE"),
}
}
}
}

View File

@@ -112,27 +112,31 @@ pub mod user {
use crate::database::models::{User, UserRole};
use actix_macros::ResponderJson;
use serde::Serialize;
/// UserResponse
///
/// Uses for stripping sensitive fields (password, fcm, etc.) from response.
/// Используется для скрытия чувствительных полей, таких как хеш пароля или FCM
#[derive(Serialize, utoipa::ToSchema, ResponderJson)]
#[serde(rename_all = "camelCase")]
pub struct UserResponse {
/// UUID
#[schema(examples("67dcc9a9507b0000772744a2"))]
id: String,
/// Имя пользователя
#[schema(examples("n08i40k"))]
username: String,
/// Группа
#[schema(examples("ИС-214/23"))]
group: String,
/// Роль
role: UserRole,
/// Идентификатор прявязанного аккаунта VK
#[schema(examples(498094647, json!(null)))]
vk_id: Option<i32>,
/// JWT токен доступа
#[schema(examples(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjE3NDMxMDgwOTkiLCJleHAiOiIxODY5MjUyMDk5In0.rMgXRb3JbT9AvLK4eiY9HMB5LxgUudkpQyoWKOypZFY"
))]