Обновление документации.

This commit is contained in:
2025-04-15 22:09:10 +04:00
parent 2fd6d787a0
commit 5068fe3069
26 changed files with 370 additions and 235 deletions

View File

@@ -1,4 +1,4 @@
-- This file was automatically created by Diesel to setup helper functions
-- This file was automatically created by Diesel to set up helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.

View File

@@ -1,4 +1,4 @@
-- This file was automatically created by Diesel to setup helper functions
-- This file was automatically created by Diesel to set up helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.

View File

@@ -50,7 +50,7 @@ impl Schedule {
}
}
/// Общие данные передаваемые в эндпоинты
/// Common data provided to endpoints.
pub struct AppState {
pub downloader: Mutex<BasicXlsDownloader>,
pub schedule: Mutex<Option<Schedule>>,
@@ -76,7 +76,7 @@ impl AppState {
}
}
/// Создание нового объекта web::Data<AppState>
/// Create a new object web::Data<AppState>.
pub async fn app_state() -> web::Data<AppState> {
web::Data::new(AppState::new().await)
}

View File

@@ -31,28 +31,28 @@ pub enum UserRole {
#[diesel(table_name = crate::database::schema::users)]
#[diesel(treat_none_as_null = true)]
pub struct User {
/// UUID аккаунта
/// Account UUID.
pub id: String,
/// Имя пользователя
/// User name.
pub username: String,
/// BCrypt хеш пароля
/// BCrypt password hash.
pub password: String,
/// Идентификатор привязанного аккаунта VK
/// ID of the linked VK account.
pub vk_id: Option<i32>,
/// JWT токен доступа
/// JWT access token.
pub access_token: String,
/// Группа
/// Group.
pub group: String,
/// Роль
/// Role.
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
/// Version of the installed Polytechnic+ application.
pub version: String,
}
@@ -73,12 +73,12 @@ pub struct User {
#[diesel(table_name = crate::database::schema::fcm)]
#[diesel(primary_key(user_id))]
pub struct FCM {
/// UUID аккаунта.
/// Account UUID.
pub user_id: String,
/// FCM токен.
/// FCM token.
pub token: String,
/// Список топиков, на которые подписан пользователь.
/// List of topics subscribed to by the user.
pub topics: Vec<Option<String>>,
}

View File

@@ -16,19 +16,19 @@ use std::fmt::Debug;
#[status_code = "actix_web::http::StatusCode::UNAUTHORIZED"]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Error {
/// В запросе отсутствует заголовок Authorization
/// There is no Authorization header in the request.
#[display("No Authorization header found")]
NoHeader,
/// Неизвестный тип авторизации, отличающийся от Bearer
/// Unknown authorization type other than Bearer.
#[display("Bearer token is required")]
UnknownAuthorizationType,
/// Токен не действителен
/// Invalid or expired access token.
#[display("Invalid or expired access token")]
InvalidAccessToken,
/// Пользователь привязанный к токену не найден в базе данных
/// The user bound to the token is not found in the database.
#[display("No user associated with access token")]
NoUser,
}
@@ -39,7 +39,7 @@ impl Error {
}
}
/// Экстрактор пользователя из запроса с токеном
/// User extractor from request with Bearer access token.
impl FromRequestSync for User {
type Error = actix_web::Error;
@@ -87,7 +87,7 @@ impl<const FCM: bool> UserExtractor<{ FCM }> {
}
}
/// Экстрактор пользователя и дополнительных параметров из запроса с токеном
/// Extractor of user and additional parameters from request with Bearer token.
impl<const FCM: bool> FromRequestSync for UserExtractor<{ FCM }> {
type Error = actix_web::Error;

View File

@@ -2,26 +2,64 @@ use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use futures_util::future::LocalBoxFuture;
use std::future::{Ready, ready};
use std::ops;
/// Асинхронный экстрактор объектов из запроса
/// # Async extractor.
/// Asynchronous object extractor from a query.
pub struct AsyncExtractor<T>(T);
impl<T> AsyncExtractor<T> {
#[allow(dead_code)]
/// Получение объекта, извлечённого с помощью экстрактора
/// Retrieve the object extracted with the extractor.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> ops::Deref for AsyncExtractor<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> ops::DerefMut for AsyncExtractor<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub trait FromRequestAsync: Sized {
type Error: Into<actix_web::Error>;
/// Асинхронная функция для извлечения данных из запроса
/// Asynchronous function for extracting data from a query.
///
/// returns: Result<Self, Self::Error>
///
/// # Examples
///
/// ```
/// struct User {
/// pub id: String,
/// pub username: String,
/// }
///
/// // TODO: Я вообще этот экстрактор не использую, нахуя мне тогда писать пример, если я не ебу как его использовать. Я забыл.
///
/// #[get("/")]
/// fn get_user_async(
/// user: web::AsyncExtractor<User>,
/// ) -> web::Json<User> {
/// let user = user.into_inner();
///
/// web::Json(user)
/// }
/// ```
async fn from_request_async(req: HttpRequest, payload: Payload) -> Result<Self, Self::Error>;
}
/// Реализация треита FromRequest для всех асинхронных экстракторов
impl<T: FromRequestAsync> FromRequest for AsyncExtractor<T> {
type Error = T::Error;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
@@ -37,24 +75,72 @@ impl<T: FromRequestAsync> FromRequest for AsyncExtractor<T> {
}
}
/// Синхронный экстрактор объектов из запроса
/// # Sync extractor.
/// Synchronous object extractor from a query.
pub struct SyncExtractor<T>(T);
impl<T> SyncExtractor<T> {
/// Получение объекта, извлечённого с помощью экстрактора
/// Retrieving an object extracted with the extractor.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> ops::Deref for SyncExtractor<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> ops::DerefMut for SyncExtractor<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub trait FromRequestSync: Sized {
type Error: Into<actix_web::Error>;
/// Синхронная функция для извлечения данных из запроса
/// Synchronous function for extracting data from a query.
///
/// returns: Result<Self, Self::Error>
///
/// # Examples
///
/// ```
/// struct User {
/// pub id: String,
/// pub username: String,
/// }
///
/// impl FromRequestSync for User {
/// type Error = actix_web::Error;
///
/// fn from_request_sync(req: &HttpRequest, _: &mut Payload) -> Result<Self, Self::Error> {
/// // do magic here.
///
/// Ok(User {
/// id: "qwerty".to_string(),
/// username: "n08i40k".to_string()
/// })
/// }
/// }
///
/// #[get("/")]
/// fn get_user_sync(
/// user: web::SyncExtractor<User>,
/// ) -> web::Json<User> {
/// let user = user.into_inner();
///
/// web::Json(user)
/// }
/// ```
fn from_request_sync(req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error>;
}
/// Реализация треита FromRequest для всех синхронных экстракторов
impl<T: FromRequestSync> FromRequest for SyncExtractor<T> {
type Error = T::Error;
type Future = Ready<Result<Self, Self::Error>>;

View File

@@ -7,7 +7,7 @@ use actix_web::{Error, HttpRequest, ResponseError};
use futures_util::future::LocalBoxFuture;
use std::future::{Ready, ready};
/// Middleware guard работающий с токенами JWT
/// Middleware guard working with JWT tokens.
pub struct JWTAuthorization;
impl<S, B> Transform<S, ServiceRequest> for JWTAuthorization
@@ -31,13 +31,13 @@ pub struct JWTAuthorizationMiddleware<S> {
service: S,
}
/// Функция для проверки наличия и действительности токена в запросе, а так же существования пользователя к которому он привязан
impl<S, B> JWTAuthorizationMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
/// Checking the validity of the token.
pub fn check_authorization(
&self,
req: &HttpRequest,

View File

@@ -14,39 +14,37 @@ use std::sync::LazyLock;
pub mod schema;
/// Данные ячейке хранящей строку
/// Data cell storing the line.
struct InternalId {
/// Индекс строки
/// Line index.
row: u32,
/// Индекс столбца
/// Column index.
column: u32,
/**
* Текст в ячейке
*/
/// Text in the cell.
name: String,
}
/// Данные о времени проведения пар из второй колонки расписания
/// Data on the time of lessons from the second column of the schedule.
struct InternalTime {
/// Временной отрезок проведения пары
/// Temporary segment of the lesson.
time_range: LessonTime,
/// Тип пары
/// Type of lesson.
lesson_type: LessonType,
/// Индекс пары
/// The lesson index.
default_index: Option<u32>,
/// Рамка ячейки
/// The frame of the cell.
xls_range: ((u32, u32), (u32, u32)),
}
/// Сокращение типа рабочего листа
/// Working sheet type alias.
type WorkSheet = calamine::Range<calamine::Data>;
/// Получение строки из требуемой ячейки
/// Getting a line from the required cell.
fn get_string_from_cell(worksheet: &WorkSheet, row: u32, col: u32) -> Option<String> {
let cell_data = if let Some(data) = worksheet.get((row as usize, col as usize)) {
data.to_string()
@@ -74,7 +72,7 @@ fn get_string_from_cell(worksheet: &WorkSheet, row: u32, col: u32) -> Option<Str
}
}
/// Получение границ ячейки по её верхней левой координате
/// Obtaining the boundaries of the cell along its upper left coordinate.
fn get_merge_from_start(worksheet: &WorkSheet, row: u32, column: u32) -> ((u32, u32), (u32, u32)) {
let worksheet_end = worksheet.end().unwrap();
@@ -109,7 +107,7 @@ fn get_merge_from_start(worksheet: &WorkSheet, row: u32, column: u32) -> ((u32,
((row, column), (row_end, column_end))
}
/// Получение "скелета" расписания из рабочего листа
/// Obtaining a "skeleton" schedule from the working sheet.
fn parse_skeleton(worksheet: &WorkSheet) -> Result<(Vec<InternalId>, Vec<InternalId>), ParseError> {
let range = &worksheet;
@@ -167,19 +165,20 @@ fn parse_skeleton(worksheet: &WorkSheet) -> Result<(Vec<InternalId>, Vec<Interna
Ok((days, groups))
}
/// Результат получения пары из ячейки
/// The result of obtaining a lesson from the cell.
enum LessonParseResult {
/// Список пар длинной от одного до двух
/// List of lessons long from one to two.
///
/// Количество пар будет равно одному, если пара первая за день, иначе будет возвращен список из шаблона перемены и самой пары
/// The number of lessons will be equal to one if the couple is the first in the day,
/// otherwise the list from the change template and the lesson itself will be returned.
Lessons(Vec<Lesson>),
/// Улица на которой находится корпус политехникума
/// Street on which the Polytechnic Corps is located.
Street(String),
}
trait StringInnerSlice {
/// Получения отрезка строки из строки по начальному и конечному индексу
/// Obtaining a line from the line on the initial and final index.
fn inner_slice(&self, from: usize, to: usize) -> Self;
}
@@ -192,7 +191,8 @@ impl StringInnerSlice for String {
}
}
/// Получение нестандартного типа пары по названию
// noinspection GrazieInspection
/// Obtaining a non-standard type of lesson by name.
fn guess_lesson_type(name: &String) -> Option<(String, LessonType)> {
let map: HashMap<String, LessonType> = HashMap::from([
("(консультация)".to_string(), LessonType::Consultation),
@@ -234,7 +234,7 @@ fn guess_lesson_type(name: &String) -> Option<(String, LessonType)> {
}
}
/// Получение пары или улицы из ячейки
/// Getting a pair or street from a cell.
fn parse_lesson(
worksheet: &WorkSheet,
day: &mut Day,
@@ -369,7 +369,7 @@ fn parse_lesson(
])))
}
/// Получение списка кабинетов справа от ячейки пары
/// Obtaining a list of cabinets to the right of the lesson cell.
fn parse_cabinets(worksheet: &WorkSheet, row: u32, column: u32) -> Vec<String> {
let mut cabinets: Vec<String> = Vec::new();
@@ -387,7 +387,7 @@ fn parse_cabinets(worksheet: &WorkSheet, row: u32, column: u32) -> Vec<String> {
cabinets
}
/// Получение "чистого" названия пары и списка преподавателей из текста ячейки пары
/// Getting the "pure" name of the lesson and list of teachers from the text of the lesson cell.
fn parse_name_and_subgroups(name: &String) -> Result<(String, Vec<LessonSubGroup>), ParseError> {
static LESSON_RE: LazyLock<Regex, fn() -> Regex> =
LazyLock::new(|| Regex::new(r"(?:[А-Я][а-я]+[А-Я]{2}(?:\([0-9][а-я]+\))?)+$").unwrap());
@@ -479,7 +479,7 @@ fn parse_name_and_subgroups(name: &String) -> Result<(String, Vec<LessonSubGroup
Ok((lesson_name, subgroups))
}
/// Конвертация списка пар групп в список пар преподавателей
/// Conversion of the list of couples of groups in the list of lessons of teachers.
fn convert_groups_to_teachers(
groups: &HashMap<String, ScheduleEntry>,
) -> HashMap<String, ScheduleEntry> {
@@ -556,7 +556,24 @@ fn convert_groups_to_teachers(
teachers
}
/// Чтение XLS документа из буфера и преобразование его в готовые к использованию расписания
/// Reading XLS Document from the buffer and converting it into the schedule ready to use.
///
/// # Arguments
///
/// * `buffer`: XLS data containing schedule.
///
/// returns: Result<ParseResult, ParseError>
///
/// # Examples
///
/// ```
/// let result = parse_xls(&include_bytes!("../../schedule.xls").to_vec());
///
/// assert!(result.is_ok());
///
/// assert_ne!(result.as_ref().unwrap().groups.len(), 0);
/// assert_ne!(result.as_ref().unwrap().teachers.len(), 0);
/// ```
pub fn parse_xls(buffer: &Vec<u8>) -> Result<ParseResult, ParseError> {
let cursor = Cursor::new(&buffer);
let mut workbook: Xls<_> =

View File

@@ -6,137 +6,139 @@ use std::collections::HashMap;
use std::sync::Arc;
use utoipa::ToSchema;
/// The beginning and end of the lesson.
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
pub struct LessonTime {
/// Начало пары
/// 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,
}
#[derive(Clone, Hash, Debug, Serialize, Deserialize, ToSchema)]
pub struct LessonSubGroup {
/// Номер подгруппы
/// Index of subgroup.
pub number: u8,
/// Кабинет, если присутствует
/// Cabinet, if present.
pub cabinet: Option<String>,
/// Фио преподавателя
/// Full name of the teacher.
pub teacher: 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 default_range: Option<[u8; 2]>,
/// Название занятия
/// Name.
pub name: Option<String>,
/// Начало и конец занятия
/// The beginning and end.
pub time: LessonTime,
/// Список подгрупп
/// List of subgroups.
#[serde(rename = "subGroups")]
pub subgroups: Option<Vec<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 ParseResult {
/// Список групп
/// List of groups.
pub groups: HashMap<String, ScheduleEntry>,
/// Список преподавателей
/// List of teachers.
pub teachers: HashMap<String, ScheduleEntry>,
}
#[derive(Debug, Display, Clone, ToSchema)]
pub enum ParseError {
/// Ошибки связанные с чтением XLS файла.
/// Errors related to reading XLS file.
#[display("{}: Failed to read XLS file.", "_0")]
#[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 line
#[display("Failed to read lesson start and end times from string.")]
GlobalTime,
/// Не найдены начало и конец соответствующее паре
/// Not found the beginning and the end corresponding to the lesson.
#[display("No start and end times matching the lesson was found.")]
LessonTimeNotFound,
/// Не удалось прочитать индекс подгруппы
/// Failed to read the subgroup index.
#[display("Failed to read subgroup index.")]
SubgroupIndexParsingFailed,
}

View File

@@ -86,11 +86,11 @@ mod schema {
#[derive(Deserialize, Serialize, ToSchema)]
#[schema(as = SignIn::Request)]
pub struct Request {
/// Имя пользователя
/// User name.
#[schema(examples("n08i40k"))]
pub username: String,
/// Пароль
/// Password.
pub password: String,
}
@@ -102,7 +102,7 @@ mod schema {
#[serde(rename_all = "camelCase")]
#[schema(as = SignInVk::Request)]
pub struct Request {
/// Токен VK ID
/// VK ID token.
pub access_token: String,
}
}
@@ -114,21 +114,21 @@ mod schema {
#[schema(as = SignIn::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
pub enum ErrorCode {
/// Некорректное имя пользователя или пароль
/// Incorrect username or password.
IncorrectCredentials,
/// Недействительный токен VK ID
/// Invalid VK ID token.
InvalidVkAccessToken,
}
/// Internal
/// Тип авторизации
/// Type of authorization.
pub enum SignInData {
/// Имя пользователя и пароль
/// User and password name and password.
Default(Request),
/// Идентификатор привязанного аккаунта VK
/// Identifier of the attached account VK.
Vk(i32),
}
}

View File

@@ -50,10 +50,7 @@ async fn sign_up_combined(
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>)
))]
#[post("/sign-up")]
pub async fn sign_up(
data_json: Json<Request>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
pub async fn sign_up(data_json: Json<Request>, app_state: web::Data<AppState>) -> ServiceResponse {
let data = data_json.into_inner();
sign_up_combined(
@@ -124,21 +121,21 @@ mod schema {
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = SignUp::Request)]
pub struct Request {
/// Имя пользователя
/// User name.
#[schema(examples("n08i40k"))]
pub username: String,
/// Пароль
/// Password.
pub password: String,
/// Группа
/// Group.
#[schema(examples("ИС-214/23"))]
pub group: String,
/// Роль
/// Role.
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
/// Version of the installed Polytechnic+ application.
#[schema(examples("3.0.0"))]
pub version: String,
}
@@ -151,21 +148,21 @@ mod schema {
#[serde(rename_all = "camelCase")]
#[schema(as = SignUpVk::Request)]
pub struct Request {
/// Токен VK ID
/// VK ID token.
pub access_token: String,
/// Имя пользователя
/// User name.
#[schema(examples("n08i40k"))]
pub username: String,
/// Группа
/// Group.
#[schema(examples("ИС-214/23"))]
pub group: String,
/// Роль
/// Role.
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
/// Version of the installed Polytechnic+ application.
#[schema(examples("3.0.0"))]
pub version: String,
}
@@ -178,44 +175,44 @@ mod schema {
#[schema(as = SignUp::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
pub enum ErrorCode {
/// Передана роль ADMIN
/// Conveyed the role of Admin.
DisallowedRole,
/// Неизвестное название группы
/// Unknown name of the group.
InvalidGroupName,
/// Пользователь с таким именем уже зарегистрирован
/// User with this name is already registered.
UsernameAlreadyExists,
/// Недействительный токен VK ID
/// Invalid VK ID token.
InvalidVkAccessToken,
/// Пользователь с таким аккаунтом VK уже зарегистрирован
/// User with such an account VK is already registered.
VkAlreadyExists,
}
/// Internal
/// Данные для регистрации
/// Data for registration.
pub struct SignUpData {
/// Имя пользователя
/// User name.
pub username: String,
/// Пароль
/// Password.
///
/// Должен присутствовать даже если регистрация происходит с помощью токена VK ID
/// Should be present even if registration occurs using the VK ID token.
pub password: String,
/// Идентификатор аккаунта VK
/// Account identifier VK.
pub vk_id: Option<i32>,
/// Группа
/// Group.
pub group: String,
/// Роль
/// Role.
pub role: UserRole,
/// Версия установленного приложения Polytechnic+
/// Version of the installed Polytechnic+ application.
pub version: String,
}
@@ -302,10 +299,7 @@ mod tests {
test_env();
let app_state = static_app_state().await;
driver::users::delete_by_username(
&app_state,
&"test::sign_up_multiple".to_string(),
);
driver::users::delete_by_username(&app_state, &"test::sign_up_multiple".to_string());
let create = sign_up_client(SignUpPartial {
username: "test::sign_up_multiple".to_string(),

View File

@@ -25,10 +25,7 @@ use actix_web::{get, web};
),
))]
#[get("/group")]
pub async fn group(
user: SyncExtractor<User>,
app_state: web::Data<AppState>,
) -> ServiceResponse {
pub async fn group(user: SyncExtractor<User>, app_state: web::Data<AppState>) -> ServiceResponse {
// Prevent thread lock
let schedule_lock = app_state.schedule.lock().unwrap();
@@ -55,18 +52,18 @@ mod schema {
#[schema(as = GetGroup::Response)]
#[serde(rename_all = "camelCase")]
pub struct Response {
/// Расписание группы
/// Group schedule.
pub group: ScheduleEntry,
/// Устаревшая переменная
/// ## Outdated variable.
///
/// По умолчанию возвращается пустой список
/// By default, an empty list is returned.
#[deprecated = "Will be removed in future versions"]
pub updated: Vec<i32>,
/// Устаревшая переменная
/// ## Outdated variable.
///
/// По умолчанию начальная дата по Unix
/// By default, the initial date for unix.
#[deprecated = "Will be removed in future versions"]
pub updated_at: DateTime<Utc>,
}
@@ -86,12 +83,12 @@ mod schema {
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = GroupSchedule::ErrorCode)]
pub enum ErrorCode {
/// Расписания ещё не получены
/// Schedules have not yet been parsed.
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
#[display("Schedule not parsed yet.")]
NoSchedule,
/// Группа не найдена
/// Group not found.
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
#[display("Required group not found.")]
NotFound,

View File

@@ -35,7 +35,7 @@ mod schema {
#[derive(Serialize, ToSchema)]
#[schema(as = GetGroupNames::Response)]
pub struct Response {
/// Список названий групп отсортированный в алфавитном порядке
/// List of group names sorted in alphabetical order.
#[schema(examples(json!(["ИС-214/23"])))]
pub names: Vec<String>,
}

View File

@@ -8,23 +8,23 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use utoipa::ToSchema;
/// Ответ от сервера с расписаниями
/// Response from schedule server.
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleView {
/// ETag расписания на сервере политехникума
/// ETag schedules on polytechnic server.
etag: String,
/// Дата обновления расписания на сайте политехникума
/// Schedule update date on polytechnic website.
uploaded_at: DateTime<Utc>,
/// Дата последнего скачивания расписания с сервера политехникума
/// Date last downloaded from the Polytechnic server.
downloaded_at: DateTime<Utc>,
/// Расписание групп
/// Groups schedule.
groups: HashMap<String, ScheduleEntry>,
/// Расписание преподавателей
/// Teachers schedule.
teachers: HashMap<String, ScheduleEntry>,
}
@@ -33,7 +33,7 @@ pub struct ScheduleView {
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = ScheduleShared::ErrorCode)]
pub enum ErrorCode {
/// Расписания ещё не получены
/// Schedules not yet parsed.
#[display("Schedule not parsed yet.")]
NoSchedule,
}
@@ -56,22 +56,22 @@ impl TryFrom<&web::Data<AppState>> for ScheduleView {
}
}
/// Статус кешированного расписаний
/// Cached schedule status.
#[derive(Serialize, Deserialize, ToSchema, ResponderJson)]
#[serde(rename_all = "camelCase")]
pub struct CacheStatus {
/// Хеш расписаний
/// Schedule hash.
pub cache_hash: String,
/// Требуется ли обновить ссылку на расписание
/// Whether the schedule reference needs to be updated.
pub cache_update_required: bool,
/// Дата последнего обновления кеша
/// Last cache update date.
pub last_cache_update: i64,
/// Дата обновления кешированного расписания
/// Cached schedule update date.
///
/// Определяется сервером политехникума
/// Determined by the polytechnic's server.
pub last_schedule_update: i64,
}

View File

@@ -53,18 +53,18 @@ mod schema {
#[schema(as = GetTeacher::Response)]
#[serde(rename_all = "camelCase")]
pub struct Response {
/// Расписание преподавателя
/// Teacher's schedule.
pub teacher: ScheduleEntry,
/// Устаревшая переменная
/// ## Deprecated variable.
///
/// По умолчанию возвращается пустой список
/// By default, an empty list is returned.
#[deprecated = "Will be removed in future versions"]
pub updated: Vec<i32>,
/// Устаревшая переменная
/// ## Deprecated variable.
///
/// По умолчанию начальная дата по Unix
/// Defaults to the Unix start date.
#[deprecated = "Will be removed in future versions"]
pub updated_at: DateTime<Utc>,
}
@@ -84,12 +84,12 @@ mod schema {
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[schema(as = TeacherSchedule::ErrorCode)]
pub enum ErrorCode {
/// Расписания ещё не получены
/// Schedules have not yet been parsed.
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
#[display("Schedule not parsed yet.")]
NoSchedule,
/// Преподаватель не найден
/// Teacher not found.
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
#[display("Required teacher not found.")]
NotFound,

View File

@@ -35,7 +35,7 @@ mod schema {
#[derive(Serialize, ToSchema)]
#[schema(as = GetTeacherNames::Response)]
pub struct Response {
/// Список имён преподавателей отсортированный в алфавитном порядке
/// List of teacher names sorted alphabetically.
#[schema(examples(json!(["Хомченко Н.Е."])))]
pub names: Vec<String>,
}

View File

@@ -84,7 +84,7 @@ mod schema {
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Request {
/// Ссылка на расписание
/// Schedule link.
pub url: String,
}
@@ -92,25 +92,26 @@ mod schema {
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
#[schema(as = SetDownloadUrl::ErrorCode)]
pub enum ErrorCode {
/// Передана ссылка с хостом отличающимся от politehnikum-eng.ru
/// Transferred link with host different from politehnikum-eng.ru.
#[display("URL with unknown host provided. Provide url with politehnikum-eng.ru host.")]
NonWhitelistedHost,
/// Не удалось получить мета-данные файла
/// Failed to retrieve file metadata.
#[display("Unable to retrieve metadata from the specified URL.")]
FetchFailed,
/// Не удалось скачать файл
/// Failed to download the file.
#[display("Unable to retrieve data from the specified URL.")]
DownloadFailed,
/// Ссылка ведёт на устаревшее расписание
/// The link leads to an outdated schedule.
///
/// Под устаревшим расписанием подразумевается расписание, которое было опубликовано раньше, чем уже имеется на данный момент
/// An outdated schedule refers to a schedule that was published earlier
/// than is currently available.
#[display("The schedule is older than it already is.")]
OutdatedSchedule,
/// Не удалось преобразовать расписание
/// Failed to parse the schedule.
#[display("{}", "_0.display()")]
InvalidSchedule(ParseError),
}

View File

@@ -113,6 +113,7 @@ pub mod user {
use actix_macros::ResponderJson;
use serde::Serialize;
//noinspection SpellCheckingInspection
/// Используется для скрытия чувствительных полей, таких как хеш пароля или FCM
#[derive(Serialize, utoipa::ToSchema, ResponderJson)]
#[serde(rename_all = "camelCase")]
@@ -132,7 +133,7 @@ pub mod user {
/// Роль
role: UserRole,
/// Идентификатор прявязанного аккаунта VK
/// Идентификатор привязанного аккаунта VK
#[schema(examples(498094647, json!(null)))]
vk_id: Option<i32>,

View File

@@ -54,7 +54,7 @@ mod schema {
#[derive(Serialize, Deserialize, ToSchema)]
#[schema(as = ChangeGroup::Request)]
pub struct Request {
/// Название группы.
/// Group name.
pub group: String,
}
@@ -63,21 +63,21 @@ mod schema {
#[schema(as = ChangeGroup::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::CONFLICT"]
pub enum ErrorCode {
/// Расписания ещё не получены.
/// Schedules have not yet been received.
#[display("Schedule not parsed yet.")]
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
NoSchedule,
/// Передано то же название группы, что есть на данный момент.
/// Passed the same group name that is currently there.
#[display("Passed the same group name as it is at the moment.")]
SameGroup,
/// Требуемая группа не существует.
/// The required group does not exist.
#[display("The required group does not exist.")]
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
NotFound,
/// Ошибка на стороне сервера.
/// Server-side error.
#[display("Internal server error.")]
#[status_code = "actix_web::http::StatusCode::INTERNAL_SERVER_ERROR"]
InternalServerError,

View File

@@ -45,7 +45,7 @@ mod schema {
#[derive(Serialize, Deserialize, ToSchema)]
#[schema(as = ChangeUsername::Request)]
pub struct Request {
/// Новое имя.
/// User name.
pub username: String,
}
@@ -54,15 +54,15 @@ mod schema {
#[schema(as = ChangeUsername::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::CONFLICT"]
pub enum ErrorCode {
/// Передано то же имя, что есть на данный момент.
/// The same name that is currently present is passed.
#[display("Passed the same name as it is at the moment.")]
SameUsername,
/// Пользователь с таким именем уже существует.
/// A user with this name already exists.
#[display("A user with this name already exists.")]
AlreadyExists,
/// Ошибка на стороне сервера.
/// Server-side error.
#[display("Internal server error.")]
#[status_code = "actix_web::http::StatusCode::INTERNAL_SERVER_ERROR"]
InternalServerError,

View File

@@ -84,13 +84,13 @@ mod schema {
#[serde(rename_all = "camelCase")]
#[schema(as = VkIdOAuth::Request)]
pub struct Request {
/// Код подтверждения authorization_code
/// Код подтверждения authorization_code.
pub code: String,
/// Параметр для защиты передаваемых данных
/// Parameter to protect transmitted data.
pub code_verifier: String,
/// Идентификатор устройства
/// Device ID.
pub device_id: String,
}
@@ -98,7 +98,7 @@ mod schema {
#[serde(rename_all = "camelCase")]
#[schema(as = VkIdOAuth::Response)]
pub struct Response {
/// ID токен
/// ID token.
pub access_token: String,
}
@@ -107,7 +107,7 @@ mod schema {
#[schema(as = VkIdOAuth::ErrorCode)]
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
pub enum ErrorCode {
/// Сервер VK вернул ошибку
/// VK server returned an error.
#[display("VK server returned an error")]
VkIdError,
}

View File

@@ -2,7 +2,7 @@ use std::fmt::{Write};
use std::fmt::Display;
use serde::{Deserialize, Serialize};
/// Ответ от сервера при ошибках внутри Middleware
/// Server response to errors within Middleware.
#[derive(Serialize, Deserialize)]
pub struct ResponseErrorMessage<T: Display> {
code: T,

View File

@@ -1,7 +1,7 @@
use sha1::Digest;
use std::hash::Hasher;
/// Хешер возвращающий хеш из алгоритма реализующего Digest
/// Hesher returning hash from the algorithm implementing Digest
pub struct DigestHasher<D: Digest> {
digest: D,
}
@@ -10,7 +10,7 @@ impl<D> DigestHasher<D>
where
D: Digest,
{
/// Получение хеша
/// Obtain hash.
pub fn finalize(self) -> String {
hex::encode(self.digest.finalize().0)
}
@@ -20,14 +20,14 @@ impl<D> From<D> for DigestHasher<D>
where
D: Digest,
{
/// Создания хешера из алгоритма реализующего Digest
/// Creating a hash from an algorithm implementing Digest.
fn from(digest: D) -> Self {
DigestHasher { digest }
}
}
impl<D: Digest> Hasher for DigestHasher<D> {
/// Заглушка для предотвращения вызова стандартного результата Hasher
/// Stopper to prevent calling the standard Hasher result.
fn finish(&self) -> u64 {
unimplemented!("Do not call finish()");
}

View File

@@ -9,31 +9,31 @@ use std::env;
use std::mem::discriminant;
use std::sync::LazyLock;
/// Ключ для верификации токена
/// Key for token verification.
static DECODING_KEY: LazyLock<DecodingKey> = LazyLock::new(|| {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
DecodingKey::from_secret(secret.as_bytes())
});
/// Ключ для создания подписанного токена
/// Key for creating a signed token.
static ENCODING_KEY: LazyLock<EncodingKey> = LazyLock::new(|| {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
EncodingKey::from_secret(secret.as_bytes())
});
/// Ошибки верификации токена
/// Token verification errors.
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error {
/// Токен имеет другую подпись
/// The token has a different signature.
InvalidSignature,
/// Ошибка чтения токена
/// Token reading error.
InvalidToken(ErrorKind),
/// Токен просрочен
/// Token expired.
Expired,
}
@@ -43,26 +43,26 @@ impl PartialEq for Error {
}
}
/// Данные, которые хранит в себе токен
/// The data the token holds.
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
/// UUID аккаунта пользователя
/// User account UUID.
id: String,
/// Дата создания токена
/// Token creation date.
#[serde_as(as = "DisplayFromStr")]
iat: u64,
/// Дата окончания действия токена
/// Token expiry date.
#[serde_as(as = "DisplayFromStr")]
exp: u64,
}
/// Алгоритм подписи токенов
/// Token signing algorithm.
pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
/// Проверка токена и извлечение из него UUID аккаунта пользователя
/// Checking the token and extracting the UUID of the user account from it.
pub fn verify_and_decode(token: &String) -> Result<String, Error> {
let mut validation = Validation::new(DEFAULT_ALGORITHM);
@@ -87,7 +87,7 @@ pub fn verify_and_decode(token: &String) -> Result<String, Error> {
}
}
/// Создание токена пользователя
/// Creating a user token.
pub fn encode(id: &String) -> String {
let header = Header {
typ: Some(String::from("JWT")),
@@ -132,6 +132,7 @@ mod tests {
);
}
//noinspection SpellCheckingInspection
#[test]
fn test_decode_invalid_signature() {
test_env();
@@ -143,6 +144,7 @@ mod tests {
assert_eq!(result.err().unwrap(), Error::InvalidSignature);
}
//noinspection SpellCheckingInspection
#[test]
fn test_decode_expired() {
test_env();
@@ -154,6 +156,7 @@ mod tests {
assert_eq!(result.err().unwrap(), Error::Expired);
}
//noinspection SpellCheckingInspection
#[test]
fn test_decode_ok() {
test_env();

View File

@@ -5,6 +5,22 @@ pub trait MutexScope<T, ScopeFn, ScopeFnOutput>
where
ScopeFn: FnOnce(&mut T) -> ScopeFnOutput,
{
/// Replaces manually creating a mutex lock to perform operations on the data it manages.
///
/// # Arguments
///
/// * `f`: Function (mostly lambda) to which a reference to the mutable object stored in the mutex will be passed.
///
/// returns: Return value of `f` function.
///
/// # Examples
///
/// ```
/// let mtx: Mutex<i32> = Mutex::new(10);
///
/// let res = mtx.scope(|x| { *x = *x * 2; *x });
/// assert_eq!(res, *mtx.lock().unwrap());
/// ```
fn scope(&self, f: ScopeFn) -> ScopeFnOutput;
}
@@ -21,6 +37,24 @@ where
}
pub trait MutexScopeAsync<T> {
/// ## Asynchronous variant of [MutexScope::scope][MutexScope::scope].
///
/// Replaces manually creating a mutex lock to perform operations on the data it manages.
///
/// # Arguments
///
/// * `f`: Asynchronous function (mostly lambda) to which a reference to the mutable object stored in the mutex will be passed.
///
/// returns: Return value of `f` function.
///
/// # Examples
///
/// ```
/// let mtx: Mutex<i32> = Mutex::new(10);
///
/// let res = mtx.async_scope(async |x| { *x = *x * 2; *x }).await;
/// assert_eq!(res, *mtx.lock().unwrap());
/// ```
async fn async_scope<'a, F, FnFut, FnOut>(&'a self, f: F) -> FnOut
where
FnFut: Future<Output = FnOut>,

View File

@@ -1,41 +1,41 @@
use chrono::{DateTime, Utc};
/// Ошибки получения данных XLS
/// XLS data retrieval errors.
#[derive(PartialEq, Debug)]
pub enum FetchError {
/// Не установлена ссылка на файл
/// File url is not set.
NoUrlProvided,
/// Неизвестная ошибка
/// Unknown error.
Unknown,
/// Сервер вернул статус код отличающийся от 200
/// Server returned a status code different from 200.
BadStatusCode,
/// Ссылка ведёт на файл другого типа
/// The url leads to a file of a different type.
BadContentType,
/// Сервер не вернул ожидаемые заголовки
/// Server doesn't return expected headers.
BadHeaders,
}
/// Результат получения данных XLS
/// Result of XLS data retrieval.
pub struct FetchOk {
/// ETag объекта
/// ETag object.
pub etag: String,
/// Дата загрузки файла
/// File upload date.
pub uploaded_at: DateTime<Utc>,
/// Дата получения данных
/// Date data received.
pub requested_at: DateTime<Utc>,
/// Данные файла
/// File data.
pub data: Option<Vec<u8>>,
}
impl FetchOk {
/// Результат без контента файла
/// Result without file content.
pub fn head(etag: String, uploaded_at: DateTime<Utc>) -> Self {
FetchOk {
etag,
@@ -45,7 +45,7 @@ impl FetchOk {
}
}
/// Полный результат
/// Full result.
pub fn get(etag: String, uploaded_at: DateTime<Utc>, data: Vec<u8>) -> Self {
FetchOk {
etag,
@@ -59,9 +59,9 @@ impl FetchOk {
pub type FetchResult = Result<FetchOk, FetchError>;
pub trait XLSDownloader {
/// Получение данных о файле, и, опционально, его контент
/// Get data about the file, and optionally its content.
async fn fetch(&self, head: bool) -> FetchResult;
/// Установка ссылки на файл
/// Setting the file link.
async fn set_url(&mut self, url: String) -> FetchResult;
}