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 1761915..189ccb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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..0b6d30b 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(Debug, Clone, Serialize, Deserialize)] +#[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, PartialEq)] +#[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..da62197 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,31 @@ 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 = 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 => problem.owner.id.to_string() == authed_id.unwrap(), + ProblemVisibility::Internal => { + // Check for internal access + todo!() + } + } }; if !has_permission { @@ -124,7 +110,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/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/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, 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();