From e69414601dedd0d9bc1c0a5cad4011b79c0a2cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sun, 24 Nov 2024 01:52:11 +0800 Subject: [PATCH 1/3] feat(problem): support problem --- .cspell.json | 3 ++ .github/workflows/test.yaml | 2 +- src/lib.rs | 5 ++ src/models/problem.rs | 59 +++++++++++++++++--- src/routes/account.rs | 23 ++++---- src/routes/index.rs | 3 ++ src/routes/problem.rs | 105 ++++++++++++++++++++++++++++++++++++ src/utils/problem.rs | 9 ++-- 8 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/routes/problem.rs diff --git a/.cspell.json b/.cspell.json index ead73e7..21d0fe4 100644 --- a/.cspell.json +++ b/.cspell.json @@ -1,7 +1,10 @@ { "words": [ "chrono", + "covector", "farmfe", + "ICPC", + "serde", "signin", "surrealdb" ] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6d99e5a..5a68b4c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@nightly - name: Setup Surrealdb run: | docker run --rm --pull always -d \ diff --git a/src/lib.rs b/src/lib.rs index 069f121..ea279e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,13 @@ pub mod utils { pub mod routes { pub mod account; pub mod index; + pub mod problem; } pub mod cors; +use models::response::Response; +use rocket::serde::json::Json; +pub type Result = std::result::Result>, models::error::Error>; + pub use crate::routes::index::rocket; diff --git a/src/models/problem.rs b/src/models/problem.rs index 2e8f205..5387580 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -1,25 +1,72 @@ use serde::{Deserialize, Serialize}; use surrealdb::sql::Thing; +use crate::routes::problem::ProblemData; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Sample { + pub input: String, + pub output: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub enum Mode { + #[default] + ICPC, + OI, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Problem { pub id: Option, - pub sequence: String, pub title: String, - pub content: String, - pub input: String, - pub output: String, - pub samples: Vec<(String, String)>, + 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 time_limit: i32, pub memory_limit: i32, - pub test_cases: Vec<(String, String)>, + pub test_cases: Vec, pub creator: Thing, pub categories: Vec, pub tags: Vec, + pub mode: Mode, + pub private: bool, + + #[serde(skip)] pub created_at: chrono::NaiveDateTime, + #[serde(skip)] pub updated_at: chrono::NaiveDateTime, } + +impl From> for Problem { + fn from(val: ProblemData<'_>) -> Self { + Problem { + id: None, + title: val.title.to_string(), + description: val.description.to_string(), + input: val.input, + output: val.output, + samples: val.samples, + hint: val.hint, + time_limit: val.time_limit, + memory_limit: val.memory_limit, + test_cases: val.test_cases, + creator: ("account", val.id).into(), + categories: val.categories, + tags: val.tags, + mode: val.mode, + private: val.private, + created_at: chrono::Local::now().naive_local(), + updated_at: chrono::Local::now().naive_local(), + } + } +} diff --git a/src/routes/account.rs b/src/routes/account.rs index 4e88612..dc5ecc0 100644 --- a/src/routes/account.rs +++ b/src/routes/account.rs @@ -21,6 +21,7 @@ use crate::{ response::{Empty, Response}, }, utils::{account, session}, + Result, }; #[derive(Serialize, Deserialize)] @@ -41,7 +42,7 @@ pub struct RegisterResponse { pub async fn register( db: &State>, register: Json, -) -> Result>, Error> { +) -> Result { match account::create(db, register.into_inner()).await { Ok(Some(account)) => { let token = match session::create(db, account.id.clone().unwrap()).await { @@ -81,10 +82,7 @@ pub struct ProfileData<'r> { } #[post("/profile", data = "")] -pub async fn profile( - db: &State>, - profile: Json>, -) -> Result>, Error> { +pub async fn profile(db: &State>, profile: Json>) -> Result { account::get_by_id::(db, profile.id) .await .map_err(|e| Error::ServerError(Json(e.to_string().into())))? @@ -116,7 +114,7 @@ pub async fn get_profile( db: &State>, id: &str, auth: Json>, -) -> Result>, Error> { +) -> Result { if !session::verify(db, id, auth.token).await { return Err(Error::Unauthorized(Json( "Failed to grant access permission".into(), @@ -153,13 +151,14 @@ pub struct Upload<'r> { #[derive(Serialize, Deserialize)] pub struct UploadResponse { pub uri: String, + pub path: String, } #[put("/content/upload", data = "")] pub async fn upload_content( db: &State>, data: Form>, -) -> Result>, Error> { +) -> Result { if !session::verify(db, data.id, data.token).await { return Err(Error::Unauthorized(Json( "Failed to grant access permission".into(), @@ -182,7 +181,7 @@ pub async fn upload_content( .await .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; } - let file_name = format!("avatar.{}", file_extension); + let file_name = format!("{}.{}", uuid::Uuid::new_v4(), file_extension); let file_path = user_path.join(&file_name); let mut file = data @@ -203,6 +202,7 @@ pub async fn upload_content( message: "Content updated successfully".into(), data: Some(UploadResponse { uri: format!("/account/content/{}/{}", data.id, file_name), + path: format!("content/{}/{}", data.id, file_name), }), })) } @@ -212,7 +212,7 @@ pub async fn delete( db: &State>, id: &str, auth: Json>, -) -> Result>, Error> { +) -> Result { if !session::verify(db, id, auth.token).await { return Err(Error::Unauthorized(Json( "Failed to grant access permission".into(), @@ -248,10 +248,7 @@ pub struct LoginResponse { } #[post("/login", data = "")] -pub async fn login( - db: &State>, - login: Json>, -) -> Result>, Error> { +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())))? diff --git a/src/routes/index.rs b/src/routes/index.rs index 9b59959..94708a5 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -5,6 +5,8 @@ use anyhow::Result; use rocket::fs::NamedFile; use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; +use super::problem; + #[get("/")] async fn index() -> Result { NamedFile::open("dist/index.html").await @@ -34,5 +36,6 @@ pub async fn rocket() -> rocket::Rocket { .attach(CORS) .mount("/", routes![index, files]) .mount("/account", account::routes()) + .mount("/problem", problem::routes()) .manage(db) } diff --git a/src/routes/problem.rs b/src/routes/problem.rs new file mode 100644 index 0000000..3ecf50d --- /dev/null +++ b/src/routes/problem.rs @@ -0,0 +1,105 @@ +use rocket::{serde::json::Json, State}; +use serde::{Deserialize, Serialize}; +use surrealdb::{engine::remote::ws::Client, Surreal}; + +use crate::{ + models::{ + error::Error, + problem::{Mode, Problem, Sample}, + response::Response, + }, + utils::{problem, session}, + Result, +}; + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct ProblemData<'r> { + pub id: &'r str, + pub token: &'r str, + + pub title: &'r str, + pub description: &'r str, + #[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 time_limit: i32, + pub memory_limit: i32, + pub test_cases: Vec, + + pub categories: Vec, + pub tags: Vec, + + pub mode: Mode, + pub private: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct ProblemResponse { + pub id: String, +} + +#[post("/create", data = "")] +pub async fn create( + db: &State>, + problem: Json>, +) -> Result { + if !session::verify(db, problem.id, problem.token).await { + return Err(Error::Unauthorized(Json("Invalid token".into()))); + } + + let problem = problem::create(db, problem.into_inner()) + .await + .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .ok_or(Error::ServerError(Json( + "Failed to create problem, please try again later.".into(), + )))?; + + Ok(Json(Response { + success: true, + message: "Problem created successfully".to_string(), + data: Some(ProblemResponse { + id: problem.id.unwrap().id.to_string(), + }), + })) +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Authenticate<'r> { + pub id: &'r str, + pub token: &'r str, +} + +#[post("/get", data = "")] +pub async fn get(db: &State>, auth: Json>) -> Result { + let problem = problem::get(db, auth.id) + .await + .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .ok_or(Error::NotFound(Json( + "Problem with specified id not found".into(), + )))?; + + if problem.private && !session::verify(db, auth.id, auth.token).await { + return Err(Error::Unauthorized(Json( + "You have no permission to access this problem".into(), + ))); + } + + Ok(Json(Response { + success: true, + message: "Problem found".to_string(), + data: Some(problem), + })) +} + +pub fn routes() -> Vec { + use rocket::routes; + routes![create, get] +} diff --git a/src/utils/problem.rs b/src/utils/problem.rs index 968f9fe..1ea63f0 100644 --- a/src/utils/problem.rs +++ b/src/utils/problem.rs @@ -1,10 +1,13 @@ use anyhow::Result; use surrealdb::{engine::remote::ws::Client, Surreal}; -use crate::models::problem::Problem; +use crate::{models::problem::Problem, routes::problem::ProblemData}; -pub async fn create(db: &Surreal, problem: Problem) -> Result> { - Ok(db.create("problem").content(problem).await?) +pub async fn create(db: &Surreal, problem: ProblemData<'_>) -> Result> { + Ok(db + .create("problem") + .content(Into::::into(problem)) + .await?) } pub async fn update(db: &Surreal, problem: Problem) -> Result> { From 7a2b2bed3393869a7ab4deca155237bad7bd3074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Wed, 27 Nov 2024 13:57:45 +0800 Subject: [PATCH 2/3] chore: bump version --- .changes/problem.md | 5 ++ .gitignore | 3 +- src/models/problem.rs | 39 +++++++++-- src/routes/problem.rs | 157 ++++++++++++++++++++++++++++++++++++++---- src/utils/problem.rs | 10 ++- 5 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 .changes/problem.md diff --git a/.changes/problem.md b/.changes/problem.md new file mode 100644 index 0000000..d71eb7c --- /dev/null +++ b/.changes/problem.md @@ -0,0 +1,5 @@ +--- +"algohub": patch:feat +--- + +Support for problems creation and initial submission. diff --git a/.gitignore b/.gitignore index 35dd3bd..045c110 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ *.sln *.sw? -# Datas +# Data Files /content +/database # Node.js /node_modules diff --git a/src/models/problem.rs b/src/models/problem.rs index 5387580..5d9fa2d 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -22,28 +22,24 @@ pub struct Problem { pub title: String, 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 time_limit: i32, - pub memory_limit: i32, + pub time_limit: u64, + pub memory_limit: u64, pub test_cases: Vec, pub creator: Thing, + pub owner: Thing, pub categories: Vec, pub tags: Vec, pub mode: Mode, pub private: bool, - #[serde(skip)] pub created_at: chrono::NaiveDateTime, - #[serde(skip)] pub updated_at: chrono::NaiveDateTime, } @@ -61,6 +57,8 @@ impl From> for Problem { memory_limit: val.memory_limit, test_cases: val.test_cases, creator: ("account", val.id).into(), + // owner: val.owner, + owner: ("account", val.id).into(), categories: val.categories, tags: val.tags, mode: val.mode, @@ -70,3 +68,30 @@ impl From> for Problem { } } } + +#[derive(Serialize, Deserialize)] +pub struct ProblemDetail { + pub id: Thing, + + pub title: String, + pub description: String, + pub input: Option, + pub output: Option, + pub samples: Vec, + pub hint: Option, + + pub time_limit: i32, + pub memory_limit: i32, + pub test_cases: Vec, + + pub creator: Thing, + pub owner: Thing, + pub categories: Vec, + pub tags: Vec, + + pub mode: Mode, + pub private: bool, + + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} diff --git a/src/routes/problem.rs b/src/routes/problem.rs index 3ecf50d..735f4bb 100644 --- a/src/routes/problem.rs +++ b/src/routes/problem.rs @@ -1,11 +1,21 @@ -use rocket::{serde::json::Json, State}; +use std::time::Duration; + +use eval_stack::{compile::Language, config::JudgeOptions, judge::JudgeStatus}; +use rocket::{ + serde::json::Json, + tokio::{ + fs::{create_dir_all, File}, + io::AsyncWriteExt, + }, + State, +}; use serde::{Deserialize, Serialize}; use surrealdb::{engine::remote::ws::Client, Surreal}; use crate::{ models::{ error::Error, - problem::{Mode, Problem, Sample}, + problem::{Mode, Problem, ProblemDetail, Sample}, response::Response, }, utils::{problem, session}, @@ -19,7 +29,7 @@ pub struct ProblemData<'r> { pub token: &'r str, pub title: &'r str, - pub description: &'r str, + pub description: String, #[serde(skip_serializing_if = "Option::is_none")] pub input: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -28,8 +38,9 @@ pub struct ProblemData<'r> { #[serde(skip_serializing_if = "Option::is_none")] pub hint: Option, - pub time_limit: i32, - pub memory_limit: i32, + // pub owner: Thing, + pub time_limit: u64, + pub memory_limit: u64, pub test_cases: Vec, pub categories: Vec, @@ -73,20 +84,38 @@ pub async fn create( #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] pub struct Authenticate<'r> { - pub id: &'r str, - pub token: &'r str, + pub id: Option<&'r str>, + pub token: Option<&'r str>, } -#[post("/get", data = "")] -pub async fn get(db: &State>, auth: Json>) -> Result { - let problem = problem::get(db, auth.id) +#[post("/get/", data = "")] +pub async fn get( + db: &State>, + id: &str, + auth: Json>, +) -> Result { + let problem = problem::get::(db, id) .await .map_err(|e| Error::ServerError(Json(e.to_string().into())))? .ok_or(Error::NotFound(Json( "Problem with specified id not found".into(), )))?; - if problem.private && !session::verify(db, auth.id, auth.token).await { + let has_permission = if problem.private { + if auth.id.is_none() || auth.token.is_none() { + false + } else { + if !session::verify(db, auth.id.unwrap(), auth.token.unwrap()).await { + false + } else { + auth.id.unwrap() == problem.owner.id.to_string() + } + } + } else { + true + }; + + if !has_permission { return Err(Error::Unauthorized(Json( "You have no permission to access this problem".into(), ))); @@ -99,7 +128,111 @@ pub async fn get(db: &State>, auth: Json>) -> R })) } +#[derive(Serialize, Deserialize)] +pub struct SubmitData<'r> { + pub id: &'r str, + pub token: &'r str, + pub language: &'r str, + pub code: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SubmitResponse { + pub status: String, +} + +#[post("/submit/", data = "")] +pub async fn submit( + db: &State>, + id: &str, + data: Json>, +) -> Result { + if !session::verify(db, data.id, data.token).await { + return Err(Error::Unauthorized(Json("Invalid token".into()))); + } + + let problem = problem::get::(db, id) + .await + .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .ok_or(Error::NotFound(Json( + "Problem with specified id not found".into(), + )))?; + + let options = JudgeOptions { + time_limit: Duration::from_secs(problem.time_limit), + memory_limit: problem.memory_limit, + }; + + let workspace = std::env::current_dir() + .unwrap() + .join("content") + .join(data.id) + .join("workspace") + .join(uuid::Uuid::new_v4().to_string()); + + if !workspace.exists() { + create_dir_all(&workspace) + .await + .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; + } + + let language = match data.language { + "rust" => Language::Rust, + "c" => Language::C, + "cpp" => Language::CPP, + "python" => Language::Python, + _ => Language::Rust, + }; + + let source_file_path = workspace.join("source.code"); + File::create(&source_file_path) + .await + .map_err(|e| Error::ServerError(Json(e.to_string().into())))? + .write_all(data.code.as_bytes()) + .await + .unwrap(); + + println!("{:?}", problem.test_cases); + let result = eval_stack::case::run_test_cases( + language, + workspace, + source_file_path, + options, + problem + .test_cases + .iter() + .map(|s| (s.input.clone(), s.output.clone())) + .collect(), + true, + ) + .await + .map_err(|e| Error::ServerError(Json(e.to_string().into())))?; + println!("{:?}", result); + + let err = result + .iter() + .find(|&r| !matches!(r.status, JudgeStatus::Accepted)); + + if let Some(_e) = err { + Ok(Json(Response { + success: true, + message: "Unaccepted".to_string(), + data: Some(SubmitResponse { + status: "Wrong Answer".to_string(), + }), + })) + } else { + Ok(Json(Response { + success: true, + message: "Accepted".to_string(), + data: Some(SubmitResponse { + status: "Accepted".to_string(), + }), + })) + } +} + pub fn routes() -> Vec { use rocket::routes; - routes![create, get] + routes![create, get, submit] } diff --git a/src/utils/problem.rs b/src/utils/problem.rs index 1ea63f0..9a0ac0d 100644 --- a/src/utils/problem.rs +++ b/src/utils/problem.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use serde::Deserialize; use surrealdb::{engine::remote::ws::Client, Surreal}; use crate::{models::problem::Problem, routes::problem::ProblemData}; @@ -21,9 +22,12 @@ pub async fn update(db: &Surreal, problem: Problem) -> Result, id: &str) -> Result> { - Ok(db.delete(("problem", id.to_string())).await?) + Ok(db.delete(("problem", id)).await?) } -pub async fn get(db: &Surreal, id: &str) -> Result> { - Ok(db.select(("problem", id.to_string())).await?) +pub async fn get(db: &Surreal, id: &str) -> Result> +where + for<'de> M: Deserialize<'de>, +{ + Ok(db.select(("problem", id)).await?) } From 2be8153a82eee5f8027a9d7414c83f7e7e7c426b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Wed, 27 Nov 2024 13:59:31 +0800 Subject: [PATCH 3/3] fix: version bump --- .changes/problem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/problem.md b/.changes/problem.md index d71eb7c..b00145c 100644 --- a/.changes/problem.md +++ b/.changes/problem.md @@ -1,5 +1,5 @@ --- -"algohub": patch:feat +"algohub-server": patch:feat --- Support for problems creation and initial submission.