From 5adbd81dfa883dc266ab6bd475ede964db97e266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Tue, 3 Dec 2024 14:45:09 +0800 Subject: [PATCH 1/4] refactor(problem): support safely filter assets --- .changes/refactor-problem.md | 5 +++ Cargo.lock | 6 +-- src/models/problem.rs | 83 +++++++++++++++++++++++++++++------- src/routes/problem.rs | 80 +++++++++++++++++----------------- src/utils/contest.rs | 4 ++ src/utils/problem.rs | 2 +- tests/problem.rs | 10 ++--- 7 files changed, 124 insertions(+), 66 deletions(-) create mode 100644 .changes/refactor-problem.md diff --git a/.changes/refactor-problem.md b/.changes/refactor-problem.md new file mode 100644 index 0000000..8d88545 --- /dev/null +++ b/.changes/refactor-problem.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Refactor problem strucutre to support multiple test cases and safely handle input/output files. diff --git a/Cargo.lock b/Cargo.lock index cd33a58..189ccb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "algohub-server" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", "chrono", @@ -1000,9 +1000,9 @@ dependencies = [ [[package]] name = "eval-stack" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6051d0c8dba3eefbe04a05cf9e81e75402511f2db70e7afc6e14bc9d1fe2665d" +checksum = "6da0079f339d7a393172c036cd9d9b3b2e3df2e9e8fdde7ad35a82b128601f74" dependencies = [ "anyhow", "libc", diff --git a/src/models/problem.rs b/src/models/problem.rs index 83a2636..d16cbcd 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -1,8 +1,6 @@ use serde::{Deserialize, Serialize}; use surrealdb::sql::Thing; -use crate::routes::problem::CreateProblem; - use super::UserRecordId; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -11,7 +9,31 @@ pub struct Sample { pub output: String, } +#[derive(Serialize, Deserialize, Clone)] +pub struct TestCase { + pub input: Thing, + pub output: Thing, +} + +impl From> for TestCase { + fn from(value: UserTestCase<'_>) -> Self { + TestCase { + input: Thing::from(("asset", value.input)), + output: Thing::from(("asset", value.output)), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProblemVisibility { + ContestOnly, + Public, + Private, + Internal, +} + +#[derive(Clone, Serialize, Deserialize)] pub struct Problem { pub id: Option, @@ -24,19 +46,52 @@ pub struct Problem { pub time_limit: u64, pub memory_limit: u64, - pub test_cases: Vec, + pub test_cases: Vec, pub creator: Thing, pub owner: Thing, pub categories: Vec, pub tags: Vec, - pub private: bool, + pub visibility: ProblemVisibility, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserTestCase<'r> { + pub input: &'r str, + pub output: &'r str, +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CreateProblem<'r> { + pub id: &'r str, + pub token: &'r str, + + pub title: &'r str, + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, + pub samples: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub hint: Option, + + pub owner: UserRecordId, + pub time_limit: u64, + pub memory_limit: u64, + pub test_cases: Vec>, + + pub categories: Vec, + pub tags: Vec, + + pub visibility: ProblemVisibility, +} + impl From> for Problem { fn from(val: CreateProblem<'_>) -> Self { Problem { @@ -49,12 +104,12 @@ impl From> for Problem { hint: val.hint, time_limit: val.time_limit, memory_limit: val.memory_limit, - test_cases: val.test_cases, + test_cases: val.test_cases.into_iter().map(Into::into).collect(), creator: ("account", val.id).into(), owner: val.owner.into(), categories: val.categories, tags: val.tags, - private: val.private, + visibility: val.visibility, created_at: chrono::Local::now().naive_local(), updated_at: chrono::Local::now().naive_local(), } @@ -62,7 +117,7 @@ impl From> for Problem { } #[derive(Debug, Deserialize, Serialize)] -pub struct ProblemDetail { +pub struct UserProblem { pub id: String, pub title: String, @@ -74,22 +129,21 @@ pub struct ProblemDetail { pub time_limit: u64, pub memory_limit: u64, - pub test_cases: Vec, - pub creator: UserRecordId, + pub creator: String, pub owner: UserRecordId, pub categories: Vec, pub tags: Vec, - pub private: bool, + pub visibility: ProblemVisibility, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, } -impl From for ProblemDetail { +impl From for UserProblem { fn from(value: Problem) -> Self { - ProblemDetail { + UserProblem { id: value.id.unwrap().id.to_string(), title: value.title, description: value.description, @@ -99,12 +153,11 @@ impl From for ProblemDetail { hint: value.hint, time_limit: value.time_limit, memory_limit: value.memory_limit, - test_cases: value.test_cases, - creator: value.creator.into(), + creator: value.creator.id.to_string(), owner: value.owner.into(), categories: value.categories, tags: value.tags, - private: value.private, + visibility: value.visibility, created_at: value.created_at, updated_at: value.updated_at, } diff --git a/src/routes/problem.rs b/src/routes/problem.rs index d9e64ea..f2baac5 100644 --- a/src/routes/problem.rs +++ b/src/routes/problem.rs @@ -6,41 +6,14 @@ use crate::{ models::{ account::Account, error::Error, - problem::{Problem, ProblemDetail, Sample}, + problem::{CreateProblem, Problem, ProblemVisibility, UserProblem}, response::Response, - Credentials, OwnedCredentials, UserRecordId, + Credentials, OwnedCredentials, OwnedId, }, utils::{account, problem, session}, Result, }; -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct CreateProblem<'r> { - pub id: &'r str, - pub token: &'r str, - - pub title: &'r str, - pub description: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub input: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub output: Option, - pub samples: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub hint: Option, - - pub owner: UserRecordId, - pub time_limit: u64, - pub memory_limit: u64, - pub test_cases: Vec, - - pub categories: Vec, - pub tags: Vec, - - pub private: bool, -} - #[derive(Serialize, Deserialize, Debug)] #[serde(crate = "rocket::serde")] pub struct ProblemResponse { @@ -51,7 +24,7 @@ pub struct ProblemResponse { pub async fn create( db: &State>, problem: Json>, -) -> Result { +) -> Result { if !session::verify(db, problem.id, problem.token).await { return Err(Error::Unauthorized(Json("Invalid token".into()))); } @@ -66,7 +39,7 @@ pub async fn create( Ok(Json(Response { success: true, message: "Problem created successfully".to_string(), - data: Some(ProblemResponse { + data: Some(OwnedId { id: problem.id.unwrap().id.to_string(), }), })) @@ -77,7 +50,7 @@ pub async fn get( db: &State>, id: &str, auth: Json>>, -) -> Result { +) -> Result { let problem = problem::get::(db, id) .await .map_err(|e| Error::ServerError(Json(e.to_string().into())))? @@ -85,18 +58,41 @@ pub async fn get( "Problem with specified id not found".into(), )))?; - let has_permission = if problem.private { - 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() - } + let authed_id = if let Some(auth) = auth.into_inner() { + if !session::verify(db, auth.id, auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); } else { - false + Some(auth.id) } } else { - true + None + }; + + let has_permission = match problem.visibility { + ProblemVisibility::ContestOnly => { + if !authed_id.is_some() { + false + } else { + // Check for contest access + todo!() + } + } + ProblemVisibility::Public => true, + ProblemVisibility::Private => { + if !authed_id.is_some() { + false + } else { + problem.owner.id.to_string() == authed_id.unwrap() + } + } + ProblemVisibility::Internal => { + if !authed_id.is_some() { + false + } else { + // Check for internal access + todo!() + } + } }; if !has_permission { @@ -124,7 +120,7 @@ pub struct ListProblem { pub async fn list( db: &State>, data: Json, -) -> Result> { +) -> Result> { let authed_id = if let Some(auth) = &data.auth { if !session::verify(db, &auth.id, &auth.token).await { return Err(Error::Unauthorized(Json("Invalid token".into()))); diff --git a/src/utils/contest.rs b/src/utils/contest.rs index 749c61e..3fc00e2 100644 --- a/src/utils/contest.rs +++ b/src/utils/contest.rs @@ -67,3 +67,7 @@ pub async fn remove_problem( .await? .take(0)?) } + +// pub async fn participant(db: &Surreal, id: &str, user_id: &str) -> Result> { +// Ok(db.query("SELECT participants FROM contest WHERE participants = $user_id AND record::id(id) = $id")) +// } \ No newline at end of file diff --git a/src/utils/problem.rs b/src/utils/problem.rs index fd6d341..76d9796 100644 --- a/src/utils/problem.rs +++ b/src/utils/problem.rs @@ -2,7 +2,7 @@ use anyhow::Result; use serde::Deserialize; use surrealdb::{engine::remote::ws::Client, Surreal}; -use crate::{models::problem::Problem, routes::problem::CreateProblem}; +use crate::models::problem::{CreateProblem, Problem}; pub async fn create(db: &Surreal, problem: CreateProblem<'_>) -> Result> { Ok(db diff --git a/tests/problem.rs b/tests/problem.rs index 4379623..95d38e9 100644 --- a/tests/problem.rs +++ b/tests/problem.rs @@ -1,11 +1,11 @@ use algohub_server::{ models::{ account::Register, - problem::ProblemDetail, + problem::{CreateProblem, ProblemVisibility, UserProblem}, response::{Empty, Response}, OwnedCredentials, Token, UserRecordId, }, - routes::problem::{CreateProblem, ListProblem, ProblemResponse}, + routes::problem::{ListProblem, ProblemResponse}, }; use anyhow::Result; use rocket::local::asynchronous::Client; @@ -62,7 +62,7 @@ async fn test_problem() -> Result<()> { test_cases: vec![], categories: vec![], tags: vec![], - private: true, + visibility: ProblemVisibility::Public, }) .dispatch() .await; @@ -98,7 +98,7 @@ async fn test_problem() -> Result<()> { message: _, data, } = response - .into_json::>>() + .into_json::>>() .await .unwrap(); let data = data.unwrap(); @@ -123,7 +123,7 @@ async fn test_problem() -> Result<()> { message: _, data, } = response - .into_json::>>() + .into_json::>>() .await .unwrap(); let data = data.unwrap(); From e2e9344a2a75a68d6987233fe53b245110b0450f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Tue, 3 Dec 2024 14:51:36 +0800 Subject: [PATCH 2/4] chore: fix code lint --- src/utils/contest.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/utils/contest.rs b/src/utils/contest.rs index 3fc00e2..749c61e 100644 --- a/src/utils/contest.rs +++ b/src/utils/contest.rs @@ -67,7 +67,3 @@ pub async fn remove_problem( .await? .take(0)?) } - -// pub async fn participant(db: &Surreal, id: &str, user_id: &str) -> Result> { -// Ok(db.query("SELECT participants FROM contest WHERE participants = $user_id AND record::id(id) = $id")) -// } \ No newline at end of file From 5405eb89eab1738aab5b2370cb3a5a16577e5412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Tue, 3 Dec 2024 15:03:44 +0800 Subject: [PATCH 3/4] fix(lint): fix code lint --- src/models/problem.rs | 2 +- src/routes/problem.rs | 26 ++++++++------------------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/models/problem.rs b/src/models/problem.rs index d16cbcd..0b6d30b 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -24,7 +24,7 @@ impl From> for TestCase { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ProblemVisibility { ContestOnly, diff --git a/src/routes/problem.rs b/src/routes/problem.rs index f2baac5..da62197 100644 --- a/src/routes/problem.rs +++ b/src/routes/problem.rs @@ -68,27 +68,17 @@ pub async fn get( None }; - let has_permission = match problem.visibility { - ProblemVisibility::ContestOnly => { - if !authed_id.is_some() { - false - } else { + let has_permission = if authed_id.is_none() && problem.visibility != ProblemVisibility::Public { + false + } else { + match problem.visibility { + ProblemVisibility::ContestOnly => { // Check for contest access todo!() } - } - ProblemVisibility::Public => true, - ProblemVisibility::Private => { - if !authed_id.is_some() { - false - } else { - problem.owner.id.to_string() == authed_id.unwrap() - } - } - ProblemVisibility::Internal => { - if !authed_id.is_some() { - false - } else { + ProblemVisibility::Public => true, + ProblemVisibility::Private => problem.owner.id.to_string() == authed_id.unwrap(), + ProblemVisibility::Internal => { // Check for internal access todo!() } From 562e2bed2188f84519d47b6984f97b4e7bc25d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Tue, 3 Dec 2024 15:05:28 +0800 Subject: [PATCH 4/4] chore: fix test code lint --- tests/category.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/category.rs b/tests/category.rs index cb80556..6497d4f 100644 --- a/tests/category.rs +++ b/tests/category.rs @@ -37,7 +37,7 @@ async fn test_category() -> Result<()> { assert!(success); - let mut new_category_id: Vec = Vec::new(); + let mut new_category_ids: Vec = Vec::new(); for i in 0..10 { let response = client @@ -67,7 +67,7 @@ async fn test_category() -> Result<()> { assert!(success); println!("Created category: {}", data.id); - new_category_id.push(data.id); + new_category_ids.push(data.id); } let response = client @@ -93,9 +93,9 @@ async fn test_category() -> Result<()> { assert!(success); println!("Listed categories: {:#?}", data); - for i in 0..10 { + for new_category_id in new_category_ids.iter().take(10) { let response = client - .post(format!("/category/delete/{}", new_category_id[i])) + .post(format!("/category/delete/{}", new_category_id)) .json(&CreateCategory { id: &id, token: &token,