Skip to content

Commit

Permalink
feat(problem): support problem
Browse files Browse the repository at this point in the history
  • Loading branch information
fu050409 committed Nov 23, 2024
1 parent 90a64e7 commit e694146
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"words": [
"chrono",
"covector",
"farmfe",
"ICPC",
"serde",
"signin",
"surrealdb"
]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = std::result::Result<Json<Response<T>>, models::error::Error>;

pub use crate::routes::index::rocket;
59 changes: 53 additions & 6 deletions src/models/problem.rs
Original file line number Diff line number Diff line change
@@ -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<Thing>,
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
pub samples: Vec<Sample>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,

pub time_limit: i32,
pub memory_limit: i32,
pub test_cases: Vec<(String, String)>,
pub test_cases: Vec<Sample>,

pub creator: Thing,
pub categories: Vec<String>,
pub tags: Vec<String>,

pub mode: Mode,
pub private: bool,

#[serde(skip)]
pub created_at: chrono::NaiveDateTime,
#[serde(skip)]
pub updated_at: chrono::NaiveDateTime,
}

impl From<ProblemData<'_>> 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(),
}
}
}
23 changes: 10 additions & 13 deletions src/routes/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::{
response::{Empty, Response},
},
utils::{account, session},
Result,
};

#[derive(Serialize, Deserialize)]
Expand All @@ -41,7 +42,7 @@ pub struct RegisterResponse {
pub async fn register(
db: &State<Surreal<Client>>,
register: Json<RegisterData>,
) -> Result<Json<Response<RegisterResponse>>, Error> {
) -> Result<RegisterResponse> {
match account::create(db, register.into_inner()).await {
Ok(Some(account)) => {
let token = match session::create(db, account.id.clone().unwrap()).await {
Expand Down Expand Up @@ -81,10 +82,7 @@ pub struct ProfileData<'r> {
}

#[post("/profile", data = "<profile>")]
pub async fn profile(
db: &State<Surreal<Client>>,
profile: Json<ProfileData<'_>>,
) -> Result<Json<Response<Empty>>, Error> {
pub async fn profile(db: &State<Surreal<Client>>, profile: Json<ProfileData<'_>>) -> Result<Empty> {
account::get_by_id::<Record>(db, profile.id)
.await
.map_err(|e| Error::ServerError(Json(e.to_string().into())))?
Expand Down Expand Up @@ -116,7 +114,7 @@ pub async fn get_profile(
db: &State<Surreal<Client>>,
id: &str,
auth: Json<Authenticate<'_>>,
) -> Result<Json<Response<Profile>>, Error> {
) -> Result<Profile> {
if !session::verify(db, id, auth.token).await {
return Err(Error::Unauthorized(Json(
"Failed to grant access permission".into(),
Expand Down Expand Up @@ -153,13 +151,14 @@ pub struct Upload<'r> {
#[derive(Serialize, Deserialize)]
pub struct UploadResponse {
pub uri: String,
pub path: String,
}

#[put("/content/upload", data = "<data>")]
pub async fn upload_content(
db: &State<Surreal<Client>>,
data: Form<Upload<'_>>,
) -> Result<Json<Response<UploadResponse>>, Error> {
) -> Result<UploadResponse> {
if !session::verify(db, data.id, data.token).await {
return Err(Error::Unauthorized(Json(
"Failed to grant access permission".into(),
Expand All @@ -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
Expand All @@ -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),
}),
}))
}
Expand All @@ -212,7 +212,7 @@ pub async fn delete(
db: &State<Surreal<Client>>,
id: &str,
auth: Json<Authenticate<'_>>,
) -> Result<Json<Response<Empty>>, Error> {
) -> Result<Empty> {
if !session::verify(db, id, auth.token).await {
return Err(Error::Unauthorized(Json(
"Failed to grant access permission".into(),
Expand Down Expand Up @@ -248,10 +248,7 @@ pub struct LoginResponse {
}

#[post("/login", data = "<login>")]
pub async fn login(
db: &State<Surreal<Client>>,
login: Json<Login<'_>>,
) -> Result<Json<Response<LoginResponse>>, Error> {
pub async fn login(db: &State<Surreal<Client>>, login: Json<Login<'_>>) -> Result<LoginResponse> {
let session = session::authenticate(db, login.identity, login.password)
.await
.map_err(|e| Error::ServerError(Json(e.to_string().into())))?
Expand Down
3 changes: 3 additions & 0 deletions src/routes/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, std::io::Error> {
NamedFile::open("dist/index.html").await
Expand Down Expand Up @@ -34,5 +36,6 @@ pub async fn rocket() -> rocket::Rocket<rocket::Build> {
.attach(CORS)
.mount("/", routes![index, files])
.mount("/account", account::routes())
.mount("/problem", problem::routes())
.manage(db)
}
105 changes: 105 additions & 0 deletions src/routes/problem.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
pub samples: Vec<Sample>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,

pub time_limit: i32,
pub memory_limit: i32,
pub test_cases: Vec<Sample>,

pub categories: Vec<String>,
pub tags: Vec<String>,

pub mode: Mode,
pub private: bool,
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct ProblemResponse {
pub id: String,
}

#[post("/create", data = "<problem>")]
pub async fn create(
db: &State<Surreal<Client>>,
problem: Json<ProblemData<'_>>,
) -> Result<ProblemResponse> {
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 = "<auth>")]
pub async fn get(db: &State<Surreal<Client>>, auth: Json<Authenticate<'_>>) -> Result<Problem> {
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<rocket::Route> {
use rocket::routes;
routes![create, get]
}
9 changes: 6 additions & 3 deletions src/utils/problem.rs
Original file line number Diff line number Diff line change
@@ -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<Client>, problem: Problem) -> Result<Option<Problem>> {
Ok(db.create("problem").content(problem).await?)
pub async fn create(db: &Surreal<Client>, problem: ProblemData<'_>) -> Result<Option<Problem>> {
Ok(db
.create("problem")
.content(Into::<Problem>::into(problem))
.await?)
}

pub async fn update(db: &Surreal<Client>, problem: Problem) -> Result<Option<Problem>> {
Expand Down

0 comments on commit e694146

Please sign in to comment.