diff --git a/.changes/optimize.md b/.changes/optimize.md new file mode 100644 index 0000000..136b027 --- /dev/null +++ b/.changes/optimize.md @@ -0,0 +1,7 @@ +--- +"algohub-server": patch:feat +--- + +Optimize data structures and code for better performance. + +Also, make the codes more readable and easier to maintain. diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 15fea36..f9d5f67 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,4 +21,7 @@ jobs: --user root --pass root - name: Rust Cache uses: Swatinem/rust-cache@v2 - - run: cargo test --verbose --all-features + - name: Test + run: cargo test --verbose --all-features + - name: Lint + run: cargo fmt --all -- --check && cargo clippy --all-features -- -D warnings diff --git a/src/models/account.rs b/src/models/account.rs index ae3bf55..6dd64bf 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -86,3 +86,11 @@ pub struct Session { pub account_id: Thing, pub token: String, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(crate = "rocket::serde")] +pub struct MergeProfile<'r> { + pub id: &'r str, + pub token: &'r str, + pub profile: Profile, +} diff --git a/src/models/asset.rs b/src/models/asset.rs index c3d82f9..0f8deaa 100644 --- a/src/models/asset.rs +++ b/src/models/asset.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; -use rocket::fs::TempFile; +use rocket::{ + fs::{NamedFile, TempFile}, + response::Responder, +}; use serde::{Deserialize, Serialize}; use surrealdb::sql::Thing; @@ -25,3 +28,13 @@ pub struct CreateAsset<'a> { pub struct UserContent { pub id: String, } + +pub struct AssetFile(pub(crate) NamedFile); + +impl<'r, 'o: 'r> Responder<'r, 'o> for AssetFile { + fn respond_to(self, req: &rocket::Request) -> rocket::response::Result<'o> { + rocket::Response::build_from(self.0.respond_to(req)?) + .raw_header("Cache-control", "max-age=86400") // 24h (24*60*60) + .ok() + } +} diff --git a/src/models/category.rs b/src/models/category.rs index bcda9b2..ecfc977 100644 --- a/src/models/category.rs +++ b/src/models/category.rs @@ -14,7 +14,21 @@ pub struct Category { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(crate = "rocket::serde")] -pub struct CreateCategory { - pub name: String, +pub struct CategoryData<'c> { + pub name: &'c str, + pub owner: UserRecordId, +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CreateCategory<'r> { + pub id: &'r str, + pub token: &'r str, + + pub data: CategoryData<'r>, +} + +#[derive(Serialize, Deserialize)] +pub struct ListCategories { pub owner: UserRecordId, } diff --git a/src/models/organization.rs b/src/models/organization.rs index eeadd4f..deee650 100644 --- a/src/models/organization.rs +++ b/src/models/organization.rs @@ -9,8 +9,8 @@ pub struct Organization { pub description: Option, - pub owner: Vec, - pub member: Vec, + pub owners: Vec, + pub members: Vec, pub creator: String, @@ -20,8 +20,17 @@ pub struct Organization { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(crate = "rocket::serde")] -pub struct CreateOrganization { - pub name: String, +pub struct OrganizationData<'c> { + pub name: &'c str, pub display_name: Option, pub description: Option, } + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CreateOrganization<'r> { + pub id: &'r str, + pub token: &'r str, + + pub org: OrganizationData<'r>, +} diff --git a/src/models/shared.rs b/src/models/shared.rs index c8e40f9..5beec38 100644 --- a/src/models/shared.rs +++ b/src/models/shared.rs @@ -69,6 +69,12 @@ pub struct OwnedCredentials { pub token: String, } +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct OwnedId { + pub id: String, +} + #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] pub struct Token<'r> { diff --git a/src/routes/account.rs b/src/routes/account.rs index 59b5575..a2097b2 100644 --- a/src/routes/account.rs +++ b/src/routes/account.rs @@ -1,12 +1,11 @@ use std::path::Path; use rocket::{get, post, serde::json::Json, tokio::fs::remove_dir_all, State}; -use serde::{Deserialize, Serialize}; use surrealdb::{engine::remote::ws::Client, Surreal}; use crate::{ models::{ - account::{Login, Profile, Register}, + account::{Login, MergeProfile, Profile, Register}, error::{Error, ErrorResponse}, response::{Empty, Response}, OwnedCredentials, Record, Token, @@ -50,14 +49,6 @@ pub async fn register( } } -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(crate = "rocket::serde")] -pub struct MergeProfile<'r> { - pub id: &'r str, - pub token: &'r str, - pub profile: Profile, -} - #[post("/profile", data = "")] pub async fn profile( db: &State>, @@ -122,14 +113,11 @@ pub async fn delete(db: &State>, id: &str, auth: Json> .into()) } -#[derive(Serialize, Deserialize)] -pub struct LoginResponse { - pub id: String, - pub token: String, -} - #[post("/login", data = "")] -pub async fn login(db: &State>, login: Json>) -> Result { +pub async fn login( + db: &State>, + login: Json>, +) -> Result { let session = session::authenticate(db, login.identity, login.password) .await .map_err(|e| Error::ServerError(Json(e.to_string().into())))? @@ -137,7 +125,7 @@ pub async fn login(db: &State>, login: Json>) -> Resul Ok(Response { success: true, message: "Login successful".into(), - data: Some(LoginResponse { + data: Some(OwnedCredentials { id: session.account_id.id.to_string(), token: session.token.clone(), }), diff --git a/src/routes/asset.rs b/src/routes/asset.rs index 172baaa..c596498 100644 --- a/src/routes/asset.rs +++ b/src/routes/asset.rs @@ -1,7 +1,6 @@ use rocket::{ form::Form, fs::NamedFile, - response::Responder, serde::json::Json, tokio::fs::{create_dir_all, File}, State, @@ -10,7 +9,7 @@ use surrealdb::{engine::remote::ws::Client, Surreal}; use crate::{ models::{ - asset::{CreateAsset, UserContent}, + asset::{AssetFile, CreateAsset, UserContent}, error::Error, response::{Empty, Response}, Credentials, @@ -82,16 +81,6 @@ pub async fn upload( })) } -pub struct AssetFile(NamedFile); - -impl<'r, 'o: 'r> Responder<'r, 'o> for AssetFile { - fn respond_to(self, req: &rocket::Request) -> rocket::response::Result<'o> { - rocket::Response::build_from(self.0.respond_to(req)?) - .raw_header("Cache-control", "max-age=86400") // 24h (24*60*60) - .ok() - } -} - #[get("/")] pub async fn get(db: &State>, id: &str) -> Option { let asset = asset::get_by_id(db, id).await.ok()??; @@ -113,7 +102,7 @@ pub async fn delete( let asset = asset::get_by_id(db, id) .await - .map_err(|e| Error::from(e))? + .map_err(Error::from)? .ok_or(Error::NotFound(Json("Asset not found".into())))?; rocket::tokio::fs::remove_file(&asset.path) diff --git a/src/routes/category.rs b/src/routes/category.rs index 0d8e2b2..2fae7df 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -1,36 +1,21 @@ use crate::{ models::{ - category::{Category, CreateCategory}, + category::{Category, CreateCategory, ListCategories}, error::Error, response::{Empty, Response}, - UserRecordId, + OwnedId, }, utils::{category, session}, Result, }; use rocket::{post, serde::json::Json, State}; -use serde::{Deserialize, Serialize}; use surrealdb::{engine::remote::ws::Client, Surreal}; -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct CategoryData<'r> { - pub id: &'r str, - pub token: &'r str, - - pub data: CreateCategory, -} - -#[derive(Serialize, Deserialize)] -pub struct CreateCatResponse { - pub id: String, -} - #[post("/create", data = "")] pub async fn create( db: &State>, - category: Json>, -) -> Result { + category: Json>, +) -> Result { if !session::verify(db, category.id, category.token).await { return Err(Error::Unauthorized(Json( "Failed to grant permission".into(), @@ -45,7 +30,7 @@ pub async fn create( Ok(Json(Response { success: true, message: "Category created successfully".into(), - data: Some(CreateCatResponse { + data: Some(OwnedId { id: data.id.unwrap().id.to_string(), }), })) @@ -55,7 +40,7 @@ pub async fn create( pub async fn delete( db: &State>, id: &str, - category: Json>, + category: Json>, ) -> Result { if !session::verify(db, category.id, category.token).await { return Err(Error::Unauthorized(Json( @@ -75,11 +60,6 @@ pub async fn delete( .into()) } -#[derive(Serialize, Deserialize)] -pub struct ListCategories { - pub owner: UserRecordId, -} - #[post("/list", data = "")] pub async fn list( db: &State>, diff --git a/src/routes/contest.rs b/src/routes/contest.rs index 1924265..706f0bc 100644 --- a/src/routes/contest.rs +++ b/src/routes/contest.rs @@ -1,5 +1,4 @@ use rocket::{serde::json::Json, State}; -use serde::{Deserialize, Serialize}; use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; use crate::{ @@ -7,21 +6,14 @@ use crate::{ contest::{AddProblems, CreateContest}, error::Error, response::{Empty, Response}, + OwnedId, }, utils::{contest, session}, Result, }; -#[derive(Serialize, Deserialize)] -pub struct CreateResponse { - pub id: String, -} - #[post("/create", data = "")] -pub async fn create( - db: &State>, - contest: Json, -) -> Result { +pub async fn create(db: &State>, contest: Json) -> Result { if !session::verify(db, &contest.auth.id, &contest.auth.token).await { return Err(Error::Unauthorized(Json("Invalid session".into()))); } @@ -35,7 +27,7 @@ pub async fn create( Ok(Json(Response { success: true, message: "Contest created successfully".into(), - data: Some(CreateResponse { + data: Some(OwnedId { id: contest.id.unwrap().id.to_string(), }), })) diff --git a/src/routes/index.rs b/src/routes/index.rs index 7b62b13..e1b8622 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -1,15 +1,16 @@ use std::path::{Path, PathBuf}; use super::asset; +use super::category; use super::contest; use super::organization; use super::problem; use super::submission; -use super::category; use crate::{cors::CORS, routes::account}; use anyhow::Result; use rocket::fs::NamedFile; use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; + #[get("/")] async fn index() -> Result { NamedFile::open("dist/index.html").await @@ -43,7 +44,6 @@ pub async fn rocket() -> rocket::Rocket { .mount("/problem", problem::routes()) .mount("/org", organization::routes()) .mount("/category", category::routes()) - .mount("/contest", contest::routes()) .mount("/code", submission::routes()) .manage(db) diff --git a/src/routes/organization.rs b/src/routes/organization.rs index 96721b1..cbe3920 100644 --- a/src/routes/organization.rs +++ b/src/routes/organization.rs @@ -1,5 +1,4 @@ use rocket::{post, serde::json::Json, tokio::fs::remove_dir_all, State}; -use serde::{Deserialize, Serialize}; use std::path::Path; use surrealdb::{engine::remote::ws::Client, Surreal}; @@ -8,37 +7,24 @@ use crate::{ error::Error, organization::CreateOrganization, response::{Empty, Response}, + Credentials, OwnedId, }, utils::{organization, session}, Result, }; -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct OrgData<'r> { - pub id: &'r str, - pub token: &'r str, - - pub org: CreateOrganization, -} - -#[derive(Serialize, Deserialize)] -pub struct CreateOrgResponse { - pub id: String, -} - -#[post("/create", data = "")] +#[post("/create", data = "")] pub async fn create( db: &State>, - organization: Json>, -) -> Result { - if !session::verify(db, organization.id, organization.token).await { + org: Json>, +) -> Result { + if !session::verify(db, org.id, org.token).await { return Err(Error::Unauthorized(Json( "Failed to grant permission".into(), ))); } - let org = organization::create(db, organization.id, organization.into_inner().org) + let org = organization::create(db, org.id, org.into_inner().org) .await .map_err(|e| Error::ServerError(Json(e.to_string().into())))? .ok_or(Error::ServerError(Json( @@ -48,19 +34,19 @@ pub async fn create( Ok(Json(Response { success: true, message: "Organization created successfully".to_string(), - data: Some(CreateOrgResponse { + data: Some(OwnedId { id: org.id.unwrap().id.to_string(), }), })) } -#[post("/delete/", data = "")] +#[post("/delete/", data = "")] pub async fn delete( db: &State>, id: &str, - organization: Json>, + org: Json>, ) -> Result { - if !session::verify(db, organization.id, organization.token).await { + if !session::verify(db, org.id, org.token).await { return Err(Error::Unauthorized(Json( "Failed to grant permission".into(), ))); diff --git a/src/routes/problem.rs b/src/routes/problem.rs index 5b07221..d9e64ea 100644 --- a/src/routes/problem.rs +++ b/src/routes/problem.rs @@ -8,7 +8,7 @@ use crate::{ error::Error, problem::{Problem, ProblemDetail, Sample}, response::Response, - OwnedCredentials, UserRecordId, + Credentials, OwnedCredentials, UserRecordId, }, utils::{account, problem, session}, Result, @@ -72,18 +72,11 @@ pub async fn create( })) } -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct Authenticate<'r> { - pub id: Option<&'r str>, - pub token: Option<&'r str>, -} - #[post("/get/", data = "")] pub async fn get( db: &State>, id: &str, - auth: Json>, + auth: Json>>, ) -> Result { let problem = problem::get::(db, id) .await @@ -93,13 +86,14 @@ pub async fn get( )))?; let has_permission = if problem.private { - if auth.id.is_none() - || auth.token.is_none() - || !session::verify(db, auth.id.unwrap(), auth.token.unwrap()).await - { - false + if let Some(auth) = auth.as_ref() { + if !session::verify(db, auth.id, auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); + } else { + auth.id == problem.owner.id.to_string() + } } else { - auth.id.unwrap() == problem.owner.id.to_string() + false } } else { true diff --git a/src/routes/submission.rs b/src/routes/submission.rs index c128549..ee25efb 100644 --- a/src/routes/submission.rs +++ b/src/routes/submission.rs @@ -64,7 +64,7 @@ pub async fn get( Ok(Json(Response { success: true, message: "Submission fetched successfully".to_string(), - data: Some(submission.into()), + data: Some(submission), })) } diff --git a/src/utils/category.rs b/src/utils/category.rs index 486e296..4114154 100644 --- a/src/utils/category.rs +++ b/src/utils/category.rs @@ -1,14 +1,14 @@ use anyhow::{Ok, Result}; use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; -use crate::models::category::{Category, CreateCategory}; +use crate::models::category::{Category, CategoryData}; -pub async fn create(db: &Surreal, data: CreateCategory) -> Result> { +pub async fn create(db: &Surreal, data: CategoryData<'_>) -> Result> { Ok(db .create("category") .content(Category { id: None, - name: data.name, + name: data.name.to_string(), owner: data.owner.into(), created_at: chrono::Local::now().naive_local(), updated_at: chrono::Local::now().naive_local(), diff --git a/src/utils/organization.rs b/src/utils/organization.rs index 2f040a6..89a6079 100644 --- a/src/utils/organization.rs +++ b/src/utils/organization.rs @@ -2,22 +2,22 @@ use anyhow::Result; use serde::Deserialize; use surrealdb::{engine::remote::ws::Client, Surreal}; -use crate::models::organization::{CreateOrganization, Organization}; +use crate::models::organization::{Organization, OrganizationData}; pub async fn create( db: &Surreal, id: &str, - org: CreateOrganization, + org: OrganizationData<'_>, ) -> Result> { Ok(db .create("organization") .content(Organization { id: None, - name: org.name, + name: org.name.to_string(), display_name: org.display_name, description: org.description, - owner: vec![("account", id).into()], - member: vec![], + owners: vec![("account", id).into()], + members: vec![], creator: id.to_string(), created_at: chrono::Local::now().naive_local(), updated_at: chrono::Local::now().naive_local(), diff --git a/tests/account.rs b/tests/account.rs index 36a53e9..3f095a5 100644 --- a/tests/account.rs +++ b/tests/account.rs @@ -1,73 +1,16 @@ mod utils; -use std::{fs::File, io::Read, path::Path}; - -use algohub_server::{ - models::{ - account::{Profile, Register}, - asset::UserContent, - response::{Empty, Response}, - Credentials, OwnedCredentials, Token, - }, - routes::account::MergeProfile, +use std::{fs::File, path::Path}; + +use algohub_server::models::{ + account::{MergeProfile, Profile, Register}, + asset::UserContent, + response::{Empty, Response}, + Credentials, OwnedCredentials, Token, }; use anyhow::Result; use rocket::{http::ContentType, local::asynchronous::Client}; - -pub struct Upload<'a> { - pub auth: Credentials<'a>, - pub owner_id: &'a str, - pub file: File, -} - -impl<'a> AsRef<[u8]> for Upload<'a> { - fn as_ref(&self) -> &[u8] { - let boundary = "boundary"; - let mut body = Vec::new(); - - body.extend( - format!( - "--{boundary}\r\nContent-Disposition: form-data; name=\"auth[id]\"\r\n\r\n{}\r\n", - self.auth.id - ) - .as_bytes(), - ); - - body.extend( - format!( - "--{boundary}\r\nContent-Disposition: form-data; name=\"auth[token]\"\r\n\r\n{}\r\n", - self.auth.token - ) - .as_bytes(), - ); - - body.extend( - format!( - "--{boundary}\r\nContent-Disposition: form-data; name=\"owner\"\r\n\r\naccount:{}\r\n", - self.owner_id - ) - .as_bytes(), - ); - - body.extend( - format!( - "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test.png\"\r\nContent-Type: image/png\r\n\r\n", - ) - .as_bytes(), - ); - - let mut file_content = Vec::new(); - let mut file_clone = self.file.try_clone().expect("Failed to clone file"); - file_clone - .read_to_end(&mut file_content) - .expect("Failed to read file"); - body.extend(file_content); - - body.extend(format!("\r\n--{boundary}--\r\n").as_bytes()); - - body.leak() - } -} +use utils::Upload; #[rocket::async_test] async fn test_register() -> Result<()> { diff --git a/tests/category.rs b/tests/category.rs index 1f7dc28..1e57144 100644 --- a/tests/category.rs +++ b/tests/category.rs @@ -1,11 +1,8 @@ -use algohub_server::{ - models::{ - account::Register, - category::CreateCategory, - response::{Empty, Response}, - OwnedCredentials, Token, UserRecordId, - }, - routes::category::{CategoryData, CreateCatResponse}, +use algohub_server::models::{ + account::Register, + category::{CategoryData, CreateCategory}, + response::{Empty, Response}, + OwnedCredentials, OwnedId, Token, UserRecordId, }; use anyhow::Result; use rocket::local::asynchronous::Client; @@ -42,11 +39,11 @@ async fn test_category() -> Result<()> { let response = client .post("/category/create") - .json(&CategoryData { + .json(&CreateCategory { id: &id, token: &token, - data: CreateCategory { - name: "test_category".to_string(), + data: CategoryData { + name: "test_category", owner: UserRecordId { tb: "account".to_string(), id: id.clone(), @@ -63,18 +60,18 @@ async fn test_category() -> Result<()> { message: _, data, } = response.into_json().await.unwrap(); - let data: CreateCatResponse = data.unwrap(); + let data: OwnedId = data.unwrap(); assert!(success); println!("Created category: {}", data.id); let response = client .post(format!("/category/delete/{}", data.id)) - .json(&CategoryData { + .json(&CreateCategory { id: &id, token: &token, - data: CreateCategory { - name: "test_category".to_string(), + data: CategoryData { + name: "test_category", owner: UserRecordId { tb: "account".to_string(), id: id.clone(), diff --git a/tests/organization.rs b/tests/organization.rs index 1a5bdd8..85a2bac 100644 --- a/tests/organization.rs +++ b/tests/organization.rs @@ -1,13 +1,10 @@ use std::path::Path; -use algohub_server::{ - models::{ - account::Register, - organization::CreateOrganization, - response::{Empty, Response}, - OwnedCredentials, Token, - }, - routes::organization::{CreateOrgResponse, OrgData}, +use algohub_server::models::{ + account::Register, + organization::{CreateOrganization, OrganizationData}, + response::{Empty, Response}, + Credentials, OwnedCredentials, OwnedId, Token, }; use anyhow::Result; use rocket::local::asynchronous::Client; @@ -45,11 +42,11 @@ async fn test_organization() -> Result<()> { let response = client .post("/org/create") - .json(&OrgData { + .json(&CreateOrganization { id: &id, token: &token, - org: CreateOrganization { - name: "test_organization".to_string(), + org: OrganizationData { + name: "test_organization", display_name: None, description: None, }, @@ -64,21 +61,16 @@ async fn test_organization() -> Result<()> { message: _, data, } = response.into_json().await.unwrap(); - let data: CreateOrgResponse = data.unwrap(); + let data: OwnedId = data.unwrap(); assert!(success); println!("Created organization: {}", data.id); let response = client .post(format!("/org/delete/{}", id)) - .json(&OrgData { + .json(&Credentials { id: &id, token: &token, - org: CreateOrganization { - name: "test_organization".to_string(), - display_name: None, - description: None, - }, }) .dispatch() .await; diff --git a/tests/utils.rs b/tests/utils.rs index e69de29..fa15409 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -0,0 +1,58 @@ +use std::{fs::File, io::Read}; + +use algohub_server::models::Credentials; + +pub struct Upload<'a> { + pub auth: Credentials<'a>, + pub owner_id: &'a str, + pub file: File, +} + +impl AsRef<[u8]> for Upload<'_> { + fn as_ref(&self) -> &[u8] { + let boundary = "boundary"; + let mut body = Vec::new(); + + body.extend( + format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"auth[id]\"\r\n\r\n{}\r\n", + self.auth.id + ) + .as_bytes(), + ); + + body.extend( + format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"auth[token]\"\r\n\r\n{}\r\n", + self.auth.token + ) + .as_bytes(), + ); + + body.extend( + format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"owner\"\r\n\r\naccount:{}\r\n", + self.owner_id + ) + .as_bytes(), + ); + + body.extend( + format!( + "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test.png\"\r\nContent-Type: image/png\r\n\r\n", + ) + .as_bytes(), + ); + + let mut file_content = Vec::new(); + let mut file_clone = self.file.try_clone().expect("Failed to clone file"); + file_clone + .read_to_end(&mut file_content) + .expect("Failed to read file"); + body.extend(file_content); + + body.extend(format!("\r\n--{boundary}--\r\n").as_bytes()); + + body.leak() + } +}