From 70a7480ea3adc03dccfc9bb1ad5fec49e855b7ef Mon Sep 17 00:00:00 2001 From: N08I40K Date: Fri, 28 Mar 2025 01:21:49 +0400 Subject: [PATCH] 0.7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена OpenAPI документация эндпоинтов и структур с интерфейсом RapiDoc. Добавлены derive макросы для преобразования структуры в HttpResponse с помощью ResponderJson и IResponse с помощью IntoIResponse. Ревью кода эндпоинтов связанных с авторизацией. Эндпоинт users/me теперь объект пользователя в требуемом виде. --- Cargo.lock | 52 ++++++++++- Cargo.toml | 5 +- actix-macros/src/lib.rs | 183 +++++++++++++++++++++++++------------ src/database/models.rs | 26 +++++- src/main.rs | 28 ++++-- src/routes/auth/sign_in.rs | 65 ++++++------- src/routes/auth/sign_up.rs | 97 ++++++++++---------- src/routes/schema.rs | 101 +++++++++++++++++--- src/routes/users/me.rs | 11 +-- 9 files changed, 398 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3521f61..c50be0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2351,7 +2351,7 @@ dependencies = [ [[package]] name = "schedule-parser-rusted" -version = "0.6.0" +version = "0.7.0" dependencies = [ "actix-macros 0.1.0", "actix-test", @@ -2379,6 +2379,9 @@ dependencies = [ "serde_with", "sha2", "tokio", + "utoipa", + "utoipa-actix-web", + "utoipa-rapidoc", ] [[package]] @@ -2930,6 +2933,53 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" +dependencies = [ + "indexmap 2.8.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-actix-web" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7eda9c23c05af0fb812f6a177514047331dac4851a2c8e9c4b895d6d826967f" +dependencies = [ + "actix-service", + "actix-web", + "utoipa", +] + +[[package]] +name = "utoipa-gen" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "utoipa-rapidoc" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f8f5abd341cce16bb4f09a8bafc087d4884a004f25fb980e538d51d6501dab" +dependencies = [ + "actix-web", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index ac8c6d8..35a1639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["actix-macros", "actix-test"] [package] name = "schedule-parser-rusted" -version = "0.6.0" +version = "0.7.0" edition = "2024" publish = false @@ -32,6 +32,9 @@ serde_repr = "0.1.20" sha2 = "0.11.0-pre.5" tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } rand = "0.9.0" +utoipa = { version = "5", features = ["actix_extras"] } +utoipa-rapidoc = { version = "6.0.0", features = ["actix-web"] } +utoipa-actix-web = "0.1" [dev-dependencies] actix-test = { path = "actix-test" } diff --git a/actix-macros/src/lib.rs b/actix-macros/src/lib.rs index 954ded2..fcc2c79 100644 --- a/actix-macros/src/lib.rs +++ b/actix-macros/src/lib.rs @@ -1,79 +1,148 @@ extern crate proc_macro; use proc_macro::TokenStream; -use quote::{ToTokens, quote}; -use syn::Attribute; -fn find_status_code(attrs: &Vec) -> Option { - attrs - .iter() - .find_map(|attr| -> Option { - if !attr.path().is_ident("status_code") { - return None; - } +mod response_error_message { + use proc_macro::TokenStream; + use quote::{ToTokens, quote}; + use syn::Attribute; - let meta = attr.meta.require_name_value().ok()?; + pub fn find_status_code(attrs: &Vec) -> Option { + attrs + .iter() + .find_map(|attr| -> Option { + if !attr.path().is_ident("status_code") { + return None; + } - let code = meta.value.to_token_stream().to_string(); - let trimmed_code = code.trim_matches('"'); + let meta = attr.meta.require_name_value().ok()?; - if let Ok(numeric_code) = trimmed_code.parse::() { - Some(quote! { actix_web::http::StatusCode::from_u16(#numeric_code).unwrap() }) - } else { - let string_code: proc_macro2::TokenStream = - trimmed_code.to_string().parse().unwrap(); - - Some(quote! { #string_code }) - } - }) -} + let code = meta.value.to_token_stream().to_string(); + let trimmed_code = code.trim_matches('"'); -fn impl_rem(ast: &syn::DeriveInput) -> TokenStream { - let name = &ast.ident; + if let Ok(numeric_code) = trimmed_code.parse::() { + Some(quote! { actix_web::http::StatusCode::from_u16(#numeric_code).unwrap() }) + } else { + let string_code: proc_macro2::TokenStream = + trimmed_code.to_string().parse().unwrap(); - let variants = if let syn::Data::Enum(data) = &ast.data { - &data.variants - } else { - panic!("Only enums are supported"); - }; - - let mut status_code_arms: Vec = variants - .iter() - .map(|v| -> Option { - let status_code = find_status_code(&v.attrs)?; - let variant_name = &v.ident; - - Some(quote! { #name::#variant_name => #status_code, }) - }) - .filter(|v| v.is_some()) - .map(|v| v.unwrap()) - .collect(); - - if status_code_arms.len() < variants.len() { - let status_code = find_status_code(&ast.attrs) - .unwrap_or_else(|| quote! { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR }); - - status_code_arms.push(quote! { _ => #status_code }); + Some(quote! { #string_code }) + } + }) } - TokenStream::from(quote! { - impl actix_web::ResponseError for #name { - fn status_code(&self) -> actix_web::http::StatusCode { - match self { - #(#status_code_arms)* + pub fn fmt(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + + let variants = if let syn::Data::Enum(data) = &ast.data { + &data.variants + } else { + panic!("Only enums are supported"); + }; + + let mut status_code_arms: Vec = variants + .iter() + .map(|v| -> Option { + let status_code = find_status_code(&v.attrs)?; + let variant_name = &v.ident; + + Some(quote! { #name::#variant_name => #status_code, }) + }) + .filter(|v| v.is_some()) + .map(|v| v.unwrap()) + .collect(); + + if status_code_arms.len() < variants.len() { + let status_code = find_status_code(&ast.attrs) + .unwrap_or_else(|| quote! { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR }); + + status_code_arms.push(quote! { _ => #status_code }); + } + + TokenStream::from(quote! { + impl actix_web::ResponseError for #name { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + #(#status_code_arms)* + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + actix_web::HttpResponse::build(self.status_code()).json(crate::utility::error::ResponseErrorMessage::new(self.clone())) } } + }) + } +} - fn error_response(&self) -> actix_web::HttpResponse { - actix_web::HttpResponse::build(self.status_code()).json(crate::utility::error::ResponseErrorMessage::new(self.clone())) +mod responder_json { + use proc_macro::TokenStream; + use quote::quote; + + pub fn fmt(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + + TokenStream::from(quote! { + impl ::actix_web::Responder for #name { + type Body = ::actix_web::body::EitherBody<::actix_web::body::BoxBody>; + + fn respond_to(self, _: &::actix_web::HttpRequest) -> ::actix_web::HttpResponse { + match ::serde_json::to_string(&self) { + Ok(body) => ::actix_web::HttpResponse::Ok() + .json(body) + .map_into_left_body(), + + Err(err) => ::actix_web::HttpResponse::from_error( + ::actix_web::error::JsonPayloadError::Serialize(err), + ) + .map_into_right_body(), + } + } } - } - }) + }) + } +} + +mod into_iresponse { + use proc_macro::TokenStream; + use quote::quote; + + pub fn fmt(ast: &syn::DeriveInput) -> TokenStream { + let name = &ast.ident; + + TokenStream::from(quote! { + impl ::core::convert::Into> for #name + where + E: ::serde::ser::Serialize + + ::utoipa::PartialSchema + + ::core::clone::Clone + + crate::routes::schema::HttpStatusCode, + { + fn into(self) -> crate::routes::schema::IResponse<#name, E> { + crate::routes::schema::IResponse(Ok(self)) + } + } + }) + } } #[proc_macro_derive(ResponseErrorMessage, attributes(status_code))] pub fn rem_derive(input: TokenStream) -> TokenStream { let ast = syn::parse(input).unwrap(); - impl_rem(&ast) + response_error_message::fmt(&ast) +} + +#[proc_macro_derive(ResponderJson)] +pub fn responser_json_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + + responder_json::fmt(&ast) +} + +#[proc_macro_derive(IntoIResponse)] +pub fn into_iresponse_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + + into_iresponse::fmt(&ast) } diff --git a/src/database/models.rs b/src/database/models.rs index 2c0cd6b..2771f22 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,7 +1,17 @@ +use actix_macros::ResponderJson; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(diesel_derive_enum::DbEnum, Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +#[derive( + diesel_derive_enum::DbEnum, + Serialize, + Deserialize, + Debug, + Clone, + Copy, + PartialEq, + utoipa::ToSchema, +)] #[ExistingTypePath = "crate::database::schema::sql_types::UserRole"] #[DbValueStyle = "UPPERCASE"] #[serde(rename_all = "UPPERCASE")] @@ -11,7 +21,17 @@ pub enum UserRole { Admin, } -#[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize, Insertable, Debug)] +#[derive( + Identifiable, + AsChangeset, + Queryable, + Selectable, + Serialize, + Insertable, + Debug, + utoipa::ToSchema, + ResponderJson, +)] #[diesel(table_name = crate::database::schema::users)] #[diesel(treat_none_as_null = true)] pub struct User { @@ -23,4 +43,4 @@ pub struct User { pub group: String, pub role: UserRole, pub version: String, -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index a5796bb..acfc11e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,10 @@ use crate::middlewares::authorization::Authorization; use crate::routes::auth::sign_in::{sign_in_default, sign_in_vk}; use crate::routes::auth::sign_up::{sign_up_default, sign_up_vk}; use crate::routes::users::me::me; -use actix_web::{web, App, HttpServer}; +use actix_web::{App, HttpServer}; use dotenvy::dotenv; +use utoipa_actix_web::AppExt; +use utoipa_rapidoc::RapiDoc; mod app_state; @@ -29,23 +31,37 @@ async fn main() { env_logger::init(); HttpServer::new(move || { - let auth_scope = web::scope("/auth") + let auth_scope = utoipa_actix_web::scope("/auth") .service(sign_in_default) .service(sign_in_vk) .service(sign_up_default) .service(sign_up_vk); - let users_scope = web::scope("/users") + let users_scope = utoipa_actix_web::scope("/users") .wrap(Authorization) .service(me); - let api_scope = web::scope("/api/v1") + let api_scope = utoipa_actix_web::scope("/api/v1") .service(auth_scope) .service(users_scope); - App::new().app_data(app_state()).service(api_scope) + let (app, api) = App::new() + .into_utoipa_app() + .app_data(app_state()) + .service(api_scope) + .split_for_parts(); + + let rapidoc_service = RapiDoc::with_openapi("/api-docs-json", api).path("/api-docs"); + + // Because CORS error on non-localhost + let patched_rapidoc_html = rapidoc_service.to_html().replace( + "https://unpkg.com/rapidoc/dist/rapidoc-min.js", + "https://cdn.jsdelivr.net/npm/rapidoc/dist/rapidoc-min.min.js", + ); + + app.service(rapidoc_service.custom_html(patched_rapidoc_html)) }) - .bind(("127.0.0.1", 8080)) + .bind(("0.0.0.0", 8080)) .unwrap() .run() .await diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index 4096f22..95812e0 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -2,8 +2,9 @@ use self::schema::*; use crate::database::driver; use crate::database::models::User; use crate::routes::auth::shared::parse_vk_id; -use crate::routes::auth::sign_in::schema::ErrorCode; use crate::routes::auth::sign_in::schema::SignInData::{Default, Vk}; +use crate::routes::schema::user::UserResponse; +use crate::routes::schema::ResponseError; use crate::{utility, AppState}; use actix_web::{post, web}; use diesel::SaveChangesDsl; @@ -22,11 +23,11 @@ async fn sign_in(data: SignInData, app_state: &web::Data) -> Response match bcrypt::verify(&data.password, &user.password) { Ok(result) => { if !result { - return Response::err(ErrorCode::IncorrectCredentials); + return ErrorCode::IncorrectCredentials.into(); } } Err(_) => { - return Response::err(ErrorCode::IncorrectCredentials); + return ErrorCode::IncorrectCredentials.into(); } } } @@ -39,36 +40,46 @@ async fn sign_in(data: SignInData, app_state: &web::Data) -> Response user.save_changes::(conn) .expect("Failed to update user"); - Response::ok(&user) + UserResponse::from(&user).into() } - Err(_) => Response::err(ErrorCode::IncorrectCredentials), + Err(_) => ErrorCode::IncorrectCredentials.into(), } } +#[utoipa::path(responses( + (status = OK, body = UserResponse), + (status = NOT_ACCEPTABLE, body = ResponseError) +))] #[post("/sign-in")] pub async fn sign_in_default(data: Json, app_state: web::Data) -> Response { sign_in(Default(data.into_inner()), &app_state).await } +#[utoipa::path(responses( + (status = OK, body = UserResponse), + (status = NOT_ACCEPTABLE, body = ResponseError) +))] #[post("/sign-in-vk")] pub async fn sign_in_vk(data_json: Json, app_state: web::Data) -> Response { let data = data_json.into_inner(); match parse_vk_id(&data.access_token) { Ok(id) => sign_in(Vk(id), &app_state).await, - Err(_) => Response::err(ErrorCode::InvalidVkAccessToken), + Err(_) => ErrorCode::InvalidVkAccessToken.into(), } } mod schema { - use crate::database::models::User; - use crate::routes::schema::{user, ErrorToHttpCode, IResponse}; + use crate::routes::schema::user::UserResponse; + use crate::routes::schema::{HttpStatusCode, IResponse}; use actix_web::http::StatusCode; use serde::{Deserialize, Serialize}; - #[derive(Deserialize, Serialize)] + #[derive(Deserialize, Serialize, utoipa::ToSchema)] + #[schema(as = SignIn::Request)] pub struct Request { + #[schema(examples("n08i40k"))] pub username: String, pub password: String, } @@ -76,44 +87,26 @@ mod schema { pub mod vk { use serde::{Deserialize, Serialize}; - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] + #[schema(as = SignInVk::Request)] pub struct Request { pub access_token: String, } } - pub type Response = IResponse; + pub type Response = IResponse; - #[derive(Serialize)] - pub struct ResponseErr { - code: ErrorCode, - } - - #[derive(Serialize)] + #[derive(Serialize, utoipa::ToSchema, Clone)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + #[schema(as = SignIn::ErrorCode)] pub enum ErrorCode { IncorrectCredentials, InvalidVkAccessToken, } - pub trait ResponseExt { - fn ok(user: &User) -> Self; - fn err(code: ErrorCode) -> Response; - } - - impl ResponseExt for Response { - fn ok(user: &User) -> Self { - IResponse(Ok(user::ResponseOk::from_user(&user))) - } - - fn err(code: ErrorCode) -> Response { - IResponse(Err(ResponseErr { code })) - } - } - - impl ErrorToHttpCode for ResponseErr { - fn to_http_status_code(&self) -> StatusCode { + impl HttpStatusCode for ErrorCode { + fn status_code(&self) -> StatusCode { StatusCode::NOT_ACCEPTABLE } } @@ -134,13 +127,13 @@ mod tests { use crate::routes::auth::sign_in::sign_in_default; use crate::test_env::tests::{static_app_state, test_app_state, test_env}; use crate::utility; - use actix_web::http::StatusCode; + use actix_test::test_app; use actix_web::dev::ServiceResponse; use actix_web::http::Method; + use actix_web::http::StatusCode; use actix_web::test; use sha2::{Digest, Sha256}; use std::fmt::Write; - use actix_test::test_app; async fn sign_in_client(data: Request) -> ServiceResponse { let app = test_app(test_app_state(), sign_in_default).await; diff --git a/src/routes/auth/sign_up.rs b/src/routes/auth/sign_up.rs index 3cb014d..2c4baaf 100644 --- a/src/routes/auth/sign_up.rs +++ b/src/routes/auth/sign_up.rs @@ -3,6 +3,8 @@ use crate::AppState; use crate::database::driver; use crate::database::models::UserRole; use crate::routes::auth::shared::{Error, parse_vk_id}; +use crate::routes::schema::ResponseError; +use crate::routes::schema::user::UserResponse; use actix_web::{post, web}; use rand::{Rng, rng}; use web::Json; @@ -10,7 +12,7 @@ use web::Json; async fn sign_up(data: SignUpData, app_state: &web::Data) -> Response { // If user selected forbidden role. if data.role == UserRole::Admin { - return Response::err(ErrorCode::DisallowedRole); + return ErrorCode::DisallowedRole.into(); } // If specified group doesn't exist in schedule. @@ -18,28 +20,32 @@ async fn sign_up(data: SignUpData, app_state: &web::Data) -> Response if let Some(schedule) = &*schedule_opt { if !schedule.data.groups.contains_key(&data.group) { - return Response::err(ErrorCode::InvalidGroupName); + return ErrorCode::InvalidGroupName.into(); } } // If user with specified username already exists. if driver::users::contains_by_username(&app_state.database, &data.username) { - return Response::err(ErrorCode::UsernameAlreadyExists); + return ErrorCode::UsernameAlreadyExists.into(); } // If user with specified VKID already exists. if let Some(id) = data.vk_id { if driver::users::contains_by_vk_id(&app_state.database, id) { - return Response::err(ErrorCode::VkAlreadyExists); + return ErrorCode::VkAlreadyExists.into(); } } - let user = data.to_user(); + let user = data.into(); driver::users::insert(&app_state.database, &user).unwrap(); - Response::ok(&user) + UserResponse::from(&user).into() } +#[utoipa::path(responses( + (status = OK, body = UserResponse), + (status = NOT_ACCEPTABLE, body = ResponseError) +))] #[post("/sign-up")] pub async fn sign_up_default(data_json: Json, app_state: web::Data) -> Response { let data = data_json.into_inner(); @@ -58,8 +64,15 @@ pub async fn sign_up_default(data_json: Json, app_state: web::Data) +))] #[post("/sign-up-vk")] -pub async fn sign_up_vk(data_json: Json, app_state: web::Data) -> Response { +pub async fn sign_up_vk( + data_json: Json, + app_state: web::Data, +) -> Response { let data = data_json.into_inner(); match parse_vk_id(&data.access_token) { @@ -87,14 +100,15 @@ pub async fn sign_up_vk(data_json: Json, app_state: web::Data; + pub type Response = IResponse; - #[derive(Serialize)] - #[serde(rename_all = "camelCase")] - pub struct ResponseOk { - id: String, - access_token: String, - group: String, - } - - #[derive(Serialize)] - pub struct ResponseErr { - code: ErrorCode, - } - - #[derive(Serialize)] + #[derive(Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] + #[schema(as = SignUp::ErrorCode)] pub enum ErrorCode { DisallowedRole, InvalidGroupName, @@ -151,23 +169,8 @@ mod schema { VkAlreadyExists, } - pub trait ResponseExt { - fn ok(user: &User) -> Self; - fn err(code: ErrorCode) -> Self; - } - - impl ResponseExt for Response { - fn ok(user: &User) -> Self { - IResponse(Ok(user::ResponseOk::from_user(&user))) - } - - fn err(code: ErrorCode) -> Response { - Self(Err(ResponseErr { code })) - } - } - - impl ErrorToHttpCode for ResponseErr { - fn to_http_status_code(&self) -> StatusCode { + impl HttpStatusCode for ErrorCode { + fn status_code(&self) -> StatusCode { StatusCode::NOT_ACCEPTABLE } } @@ -183,8 +186,8 @@ mod schema { pub version: String, } - impl SignUpData { - pub fn to_user(self) -> User { + impl Into for SignUpData { + fn into(self) -> User { let id = ObjectId::new().unwrap().to_string(); let access_token = utility::jwt::encode(&id); @@ -209,11 +212,11 @@ mod tests { use crate::routes::auth::sign_up::schema::Request; use crate::routes::auth::sign_up::sign_up_default; use crate::test_env::tests::{static_app_state, test_app_state, test_env}; - use actix_web::http::StatusCode; + use actix_test::test_app; use actix_web::dev::ServiceResponse; use actix_web::http::Method; + use actix_web::http::StatusCode; use actix_web::test; - use actix_test::test_app; struct SignUpPartial { username: String, diff --git a/src/routes/schema.rs b/src/routes/schema.rs index 87d33f1..74124e3 100644 --- a/src/routes/schema.rs +++ b/src/routes/schema.rs @@ -3,32 +3,68 @@ use actix_web::error::JsonPayloadError; use actix_web::http::StatusCode; use actix_web::{HttpRequest, HttpResponse, Responder}; use serde::{Serialize, Serializer}; +use utoipa::PartialSchema; -pub struct IResponse(pub Result); +pub struct IResponse(pub Result) +where + T: Serialize + PartialSchema, + E: Serialize + PartialSchema + Clone + HttpStatusCode; -pub trait ErrorToHttpCode { - fn to_http_status_code(&self) -> StatusCode; +impl Into> for IResponse +where + T: Serialize + PartialSchema, + E: Serialize + PartialSchema + Clone + HttpStatusCode, +{ + fn into(self) -> Result { + self.0 + } } -impl IResponse { +impl From for IResponse +where + T: Serialize + PartialSchema, + E: Serialize + PartialSchema + Clone + HttpStatusCode, +{ + fn from(value: E) -> Self { + IResponse(Err(value)) + } +} + +pub trait HttpStatusCode { + fn status_code(&self) -> StatusCode; +} + +impl IResponse +where + T: Serialize + PartialSchema, + E: Serialize + PartialSchema + Clone + HttpStatusCode, +{ pub fn new(result: Result) -> Self { IResponse(result) } } -impl Serialize for IResponse { +impl Serialize for IResponse +where + T: Serialize + PartialSchema, + E: Serialize + PartialSchema + Clone + HttpStatusCode, +{ fn serialize(&self, serializer: S) -> Result where S: Serializer, { match &self.0 { Ok(ok) => serializer.serialize_some::(&ok), - Err(err) => serializer.serialize_some::(&err), + Err(err) => serializer.serialize_some::>(&ResponseError::new(err)), } } } -impl Responder for IResponse { +impl Responder for IResponse +where + T: Serialize + PartialSchema, + E: Serialize + PartialSchema + Clone + HttpStatusCode, +{ type Body = EitherBody; fn respond_to(self, _: &HttpRequest) -> HttpResponse { @@ -36,7 +72,7 @@ impl Responder for IResponse Ok(body) => { let code = match &self.0 { Ok(_) => StatusCode::OK, - Err(e) => e.to_http_status_code(), + Err(e) => e.status_code(), }; match HttpResponse::build(code) @@ -55,24 +91,50 @@ impl Responder for IResponse } } +#[derive(Serialize, utoipa::ToSchema)] +pub struct ResponseError { + code: T, +} + +impl ResponseError { + fn new(status_code: &T) -> Self { + ResponseError { + code: status_code.clone(), + } + } +} + pub mod user { use crate::database::models::{User, UserRole}; + use actix_macros::{IntoIResponse, ResponderJson}; use serde::Serialize; - #[derive(Serialize)] + #[derive(Serialize, utoipa::ToSchema, IntoIResponse, ResponderJson)] #[serde(rename_all = "camelCase")] - pub struct ResponseOk { + pub struct UserResponse { + #[schema(examples("67dcc9a9507b0000772744a2"))] id: String, + + #[schema(examples("n08i40k"))] username: String, + + #[schema(examples("ИС-214/23"))] group: String, + role: UserRole, + + #[schema(examples(498094647, json!(null)))] vk_id: Option, + + #[schema(examples( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjE3NDMxMDgwOTkiLCJleHAiOiIxODY5MjUyMDk5In0.rMgXRb3JbT9AvLK4eiY9HMB5LxgUudkpQyoWKOypZFY" + ))] access_token: String, } - impl ResponseOk { - pub fn from_user(user: &User) -> Self { - ResponseOk { + impl From<&User> for UserResponse { + fn from(user: &User) -> Self { + UserResponse { id: user.id.clone(), username: user.username.clone(), group: user.group.clone(), @@ -82,4 +144,17 @@ pub mod user { } } } + + impl From for UserResponse { + fn from(user: User) -> Self { + UserResponse { + id: user.id, + username: user.username, + group: user.group, + role: user.role, + vk_id: user.vk_id, + access_token: user.access_token, + } + } + } } diff --git a/src/routes/users/me.rs b/src/routes/users/me.rs index 6b0ab40..179702e 100644 --- a/src/routes/users/me.rs +++ b/src/routes/users/me.rs @@ -1,11 +1,10 @@ -use crate::app_state::AppState; use crate::database::models::User; use crate::extractors::base::SyncExtractor; -use actix_web::{HttpResponse, Responder, get, web}; +use actix_web::get; +use crate::routes::schema::user::UserResponse; +#[utoipa::path(responses((status = OK, body = UserResponse)))] #[get("/me")] -pub async fn me(user: SyncExtractor, app_state: web::Data) -> impl Responder { - HttpResponse::Ok().json(user.into_inner()) +pub async fn me(user: SyncExtractor) -> UserResponse { + user.into_inner().into() } - -mod schema {}