diff --git a/.changes/solution.md b/.changes/solution.md new file mode 100644 index 0000000..858c394 --- /dev/null +++ b/.changes/solution.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support create and delete solutions \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 77daefd..becf7f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "algohub-server" -version = "0.1.15" +version = "0.1.16" dependencies = [ "anyhow", "chrono", diff --git a/src/lib.rs b/src/lib.rs index f30524a..98fb5ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod utils { pub mod organization; pub mod problem; pub mod session; + pub mod solution; pub mod submission; } @@ -22,6 +23,7 @@ pub mod routes { pub mod organization; pub mod problem; + pub mod solution; pub mod submission; } diff --git a/src/models/mod.rs b/src/models/mod.rs index b7feed6..e09a29f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -7,6 +7,6 @@ pub mod organization; pub mod problem; pub mod response; pub mod shared; +pub mod solution; pub mod submission; - pub use shared::*; diff --git a/src/models/solution.rs b/src/models/solution.rs new file mode 100644 index 0000000..33c10ff --- /dev/null +++ b/src/models/solution.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Solution { + pub id: Option, + + pub problem_id: Thing, + pub creator: Thing, + pub title: String, + pub content: String, + + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct SolutionData { + pub title: String, + pub content: String, + pub problem_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CreateSolution<'r> { + pub id: &'r str, + pub token: &'r str, + pub data: SolutionData, +} diff --git a/src/routes/index.rs b/src/routes/index.rs index ee7638b..a6ea222 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -5,6 +5,7 @@ use super::category; use super::contest; use super::organization; use super::problem; +use super::solution; use super::submission; use crate::{cors::CORS, routes::account}; use anyhow::Result; @@ -52,5 +53,6 @@ pub async fn rocket(db: Surreal) -> rocket::Rocket { .mount("/category", category::routes()) .mount("/contest", contest::routes()) .mount("/code", submission::routes()) + .mount("/solution", solution::routes()) .manage(db) } diff --git a/src/routes/solution.rs b/src/routes/solution.rs new file mode 100644 index 0000000..df7e2e1 --- /dev/null +++ b/src/routes/solution.rs @@ -0,0 +1,63 @@ +use crate::{ + models::{ + error::Error, + response::{Empty, Response}, + solution::CreateSolution, + Credentials, OwnedId, + }, + utils::{session, solution}, + Result, +}; +use rocket::{post, serde::json::Json, tokio::fs::remove_dir_all, State}; +use std::path::Path; +use surrealdb::{engine::remote::ws::Client, Surreal}; + +#[post("/create", data = "")] +pub async fn create(db: &State>, sol: Json>) -> Result { + if !session::verify(db, sol.id, sol.token).await { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + + let solution = solution::create(db, sol.id, sol.into_inner().data) + .await? + .ok_or(Error::ServerError(Json("Failed to create solution".into())))?; + + Ok(Json(Response { + success: true, + message: "Solution created successfully".to_string(), + data: Some(OwnedId { + id: solution.id.unwrap().to_string(), + }), + })) +} + +#[post("/delete/", data = "")] +pub async fn delete( + db: &State>, + id: &str, + sol: Json>, +) -> Result { + if !session::verify(db, sol.id, sol.token).await { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + + solution::delete(db, id).await?; + + remove_dir_all(Path::new("content/").join(id)).await?; + + Ok(Response { + success: true, + message: "Solution deleted successfully".to_string(), + data: None, + } + .into()) +} + +pub fn routes() -> Vec { + use rocket::routes; + routes![create, delete] +} diff --git a/src/utils/solution.rs b/src/utils/solution.rs new file mode 100644 index 0000000..df71780 --- /dev/null +++ b/src/utils/solution.rs @@ -0,0 +1,37 @@ +use anyhow::Result; +use serde::Deserialize; +use surrealdb::{engine::remote::ws::Client, Surreal}; + +use crate::models::solution::{Solution, SolutionData}; + +pub async fn create( + db: &Surreal, + id: &str, + data: SolutionData, +) -> Result> { + Ok(db + .create("solution") + .content(Solution { + id: None, + + title: data.title, + creator: ("account", id).into(), + problem_id: ("problem".to_string(), data.problem_id).into(), + content: data.content, + + created_at: chrono::Local::now().naive_local(), + updated_at: chrono::Local::now().naive_local(), + }) + .await?) +} + +pub async fn delete(db: &Surreal, id: &str) -> Result> { + Ok(db.delete(("solution", id)).await?) +} + +pub async fn get_by_id(db: &Surreal, id: &str) -> Result> +where + for<'de> M: Deserialize<'de>, +{ + Ok(db.select(("solution", id)).await?) +} diff --git a/tests/solution.rs b/tests/solution.rs new file mode 100644 index 0000000..94322c3 --- /dev/null +++ b/tests/solution.rs @@ -0,0 +1,135 @@ +mod utils; + +use std::path::Path; + +use algohub_server::{ + models::{ + account::Register, + problem::{CreateProblem, ProblemVisibility}, + response::{Empty, Response}, + solution::{CreateSolution, SolutionData}, + Credentials, OwnedCredentials, OwnedId, Token, UserRecordId, + }, + routes::problem::ProblemResponse, +}; +use anyhow::Result; +use rocket::local::asynchronous::Client; +use utils::rocket; + +#[rocket::async_test] +async fn test_solution() -> Result<()> { + let rocket = rocket().await; + let client = Client::tracked(rocket).await?; + + println!("Testing solution"); + + let response = client + .post("/account/create") + .json(&Register { + username: "fu050409".to_string(), + password: "password".to_string(), + email: "email@example.com".to_string(), + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: OwnedCredentials = data.unwrap(); + + let id = data.id.clone(); + let token = data.token.clone(); + + assert!(success); + + let response = client + .post("/problem/create") + .json(&CreateProblem { + id: &id, + token: &token, + title: "Test Problem", + description: "Test Description".to_string(), + input: Some("Test Input".to_string()), + output: Some("Test Output".to_string()), + samples: vec![], + hint: None, + owner: UserRecordId { + tb: "account".to_string(), + id: id.clone(), + }, + time_limit: 1000, + memory_limit: 128, + test_cases: vec![], + categories: vec![], + tags: vec![], + visibility: ProblemVisibility::Public, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let problem_data: ProblemResponse = data.unwrap(); + + assert!(success); + + let response = client + .post("/solution/create") + .json(&CreateSolution { + id: &id, + token: &token, + data: SolutionData { + title: "test".to_string(), + content: "test".to_string(), + problem_id: problem_data.id, + }, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: OwnedId = data.unwrap(); + + assert!(success); + println!("Created organization: {}", data.id); + + let response = client + .post(format!("/solution/delete/{}", data.id)) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + response.into_json::>().await.unwrap(); + + assert!(!Path::new("content").join(data.id.clone()).exists()); + + client + .post(format!("/account/delete/{}", id)) + .json(&Token { token: &token }) + .dispatch() + .await + .into_json::>() + .await + .unwrap(); + + Ok(()) +}