From 01d6113c1e0fc2d65a0a3d997f2f18221931f951 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Tue, 29 Jul 2025 21:09:18 +0200 Subject: [PATCH] todo shhit. figure it out. do post checking later --- src/handler.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 77 ++++++++++++++++-------- src/model.rs | 35 +++++++++++ src/response.rs | 31 ++++++++++ 4 files changed, 273 insertions(+), 25 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e69de29..c73d850 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -0,0 +1,155 @@ +use crate::{ + model::{QueryOptions, Todo, UpdateTodoSchema}, + response::{GenericResponse, SingleTodoResponse, TodoData, TodoListResponse}, + WebResult, DB, +}; +use chrono::prelude::*; +use uuid::Uuid; +use warp::{http::StatusCode, reply::json, reply::with_status, Reply}; + +pub async fn health_checker_handler() -> WebResult { + const MESSAGE: &str = "Build Simple CRUD API with Rust"; + + let response_json = &GenericResponse { + status: "success".to_string(), + message: MESSAGE.to_string(), + }; + Ok(json(response_json)) +} + +pub async fn todos_list_handler(opts: QueryOptions, db: DB) -> WebResult { + let todos = db.lock().await; + + let limit = opts.limit.unwrap_or(10); + let offset = (opts.page.unwrap_or(1) - 1) * limit; + + let todos: Vec = todos.clone().into_iter().skip(offset).take(limit).collect(); + + let json_response = TodoListResponse { + status: "success".to_string(), + results: todos.len(), + todos, + }; + Ok(json(&json_response)) +} + +pub async fn create_todo_handler(mut body: Todo, db: DB) -> WebResult { + let mut vec = db.lock().await; + + for todo in vec.iter() { + if todo.title == body.title { + let error_response = GenericResponse { + status: "fail".to_string(), + message: format!("Todo with title: '{}' already exists", todo.title), + }; + return Ok(with_status(json(&error_response), StatusCode::CONFLICT)); + } + } + + let uuid_id = Uuid::new_v4(); + let datetime = Utc::now(); + + body.id = Some(uuid_id.to_string()); + body.completed = Some(false); + body.createdAt = Some(datetime); + body.updatedAt = Some(datetime); + + let todo = body.to_owned(); + + vec.push(body); + + let json_response = SingleTodoResponse { + status: "success".to_string(), + data: TodoData { todo }, + }; + + Ok(with_status(json(&json_response), StatusCode::CREATED)) +} + +pub async fn get_todo_handler(id: String, db: DB) -> WebResult { + let vec = db.lock().await; + + for todo in vec.iter() { + if todo.id == Some(id.to_owned()) { + let json_response = SingleTodoResponse { + status: "success".to_string(), + data: TodoData { todo: todo.clone() }, + }; + + return Ok(with_status(json(&json_response), StatusCode::OK)); + } + } + + let error_response = GenericResponse { + status: "fail".to_string(), + message: format!("Todo with ID: {} not found", id), + }; + return Ok(with_status(json(&error_response), StatusCode::NOT_FOUND)); +} + +pub async fn edit_todo_handler( + id: String, + body: UpdateTodoSchema, + db: DB, +) -> WebResult { + let mut vec = db.lock().await; + + for todo in vec.iter_mut() { + if todo.id == Some(id.clone()) { + let datetime = Utc::now(); + let title = body.title.to_owned().unwrap_or(todo.title.to_owned()); + let content = body.content.to_owned().unwrap_or(todo.content.to_owned()); + let payload = Todo { + id: todo.id.to_owned(), + title: if !title.is_empty() { + title + } else { + todo.title.to_owned() + }, + content: if !content.is_empty() { + content + } else { + todo.content.to_owned() + }, + completed: if body.completed.is_some() { + body.completed + } else { + todo.completed + }, + createdAt: todo.createdAt, + updatedAt: Some(datetime), + }; + *todo = payload; + + let json_response = SingleTodoResponse { + status: "success".to_string(), + data: TodoData { todo: todo.clone() }, + }; + return Ok(with_status(json(&json_response), StatusCode::OK)); + } + } + + let error_response = GenericResponse { + status: "fail".to_string(), + message: format!("Todo with ID: {} not found", id), + }; + + Ok(with_status(json(&error_response), StatusCode::NOT_FOUND)) +} + +pub async fn delete_todo_handler(id: String, db: DB) -> WebResult { + let mut vec = db.lock().await; + + for todo in vec.iter_mut() { + if todo.id == Some(id.clone()) { + vec.retain(|todo| todo.id != Some(id.to_owned())); + return Ok(with_status(json(&""), StatusCode::NO_CONTENT)); + } + } + + let error_response = GenericResponse { + status: "fail".to_string(), + message: format!("Todo with ID: {} not found", id), + }; + Ok(with_status(json(&error_response), StatusCode::NOT_FOUND)) +} diff --git a/src/main.rs b/src/main.rs index 85b79bc..73cee1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,39 +1,66 @@ -use std::env; +mod handler; +mod model; +mod response; -use serde::Serialize; -use warp::{reply::json, Filter, Rejection, Reply}; +use model::{QueryOptions, DB}; +use warp::{http::Method, Filter, Rejection}; type WebResult = std::result::Result; -#[derive(Serialize)] -pub struct GenericResponse { - pub status: String, - pub message: String, -} - -pub async fn health_checker_handler() -> WebResult { - const MESSAGE: &str = "This is a Simple CRUD API"; - - let jsonresponse = GenericResponse { - status: "success".to_string(), - message: MESSAGE.to_string(), - }; - - Ok(json(&jsonresponse)) -} - -#[tokio::main] // needed for async main +#[tokio::main] async fn main() { pretty_env_logger::init(); + let db = model::todo_db(); + + let todo_router = warp::path!("api" / "todos"); + let todo_router_id = warp::path!("api" / "todos" / String); + let health_checker = warp::path!("api" / "healthchecker") .and(warp::get()) - .and_then(health_checker_handler); // don't call the function here! + .and_then(handler::health_checker_handler); - let routes = health_checker.with(warp::log("api")); + let cors = warp::cors() + .allow_methods(&[Method::GET, Method::POST, Method::PATCH, Method::DELETE]) + .allow_origins(vec!["http://localhost:3000/", "http://localhost:8000/"]) + .allow_headers(vec!["content-type"]) + .allow_credentials(true); - println!("[LOG]: Server started successfully."); + let todo_routes = todo_router + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db.clone())) + .and_then(handler::create_todo_handler) + .or(todo_router + .and(warp::get()) + .and(warp::query::()) + .and(with_db(db.clone())) + .and_then(handler::todos_list_handler)); - // serve the server on localhost:8080 + let todo_routes_id = todo_router_id + .and(warp::patch()) + .and(warp::body::json()) + .and(with_db(db.clone())) + .and_then(handler::edit_todo_handler) + .or(todo_router_id + .and(warp::get()) + .and(with_db(db.clone())) + .and_then(handler::get_todo_handler)) + .or(todo_router_id + .and(warp::delete()) + .and(with_db(db.clone())) + .and_then(handler::delete_todo_handler)); + + let routes = todo_routes + .with(cors) + .with(warp::log("api")) + .or(todo_routes_id) + .or(health_checker); + + println!("🚀 Server started successfully"); warp::serve(routes).run(([0, 0, 0, 0], 8080)).await; } + +fn with_db(db: DB) -> impl Filter + Clone { + warp::any().map(move || db.clone()) +} diff --git a/src/model.rs b/src/model.rs index e69de29..33edcf2 100644 --- a/src/model.rs +++ b/src/model.rs @@ -0,0 +1,35 @@ +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Todo { + pub id: Option, + pub title: String, + pub content: String, + pub completed: Option, + pub createdAt: Option>, + pub updatedAt: Option>, +} + +pub type DB = Arc>>; + +pub fn todo_db() -> DB { + // We return a Thread-safe, shared, mutable empty vector for concurrent access + // This is so fucking gross to explain + Arc::new(Mutex::new(Vec::new())) +} + +#[derive(Debug,Deserialize)] +pub struct QueryOptions { + pub page: Option, + pub limit: Option, +} + +#[derive(Debug,Deserialize,Serialize,Clone)] +pub struct UpdateTodoSchema { + pub title: Option, + pub content: Option, + pub completed: Option, +} \ No newline at end of file diff --git a/src/response.rs b/src/response.rs index e69de29..16d2328 100644 --- a/src/response.rs +++ b/src/response.rs @@ -0,0 +1,31 @@ +use crate::model::Todo; +use serde::Serialize; + +#[derive(Serialize)] +pub struct GenericResponse { + pub status: String, + pub message: String, +} + +#[derive(Serialize, Debug)] +pub struct TodoData { + pub todo: Todo, +} + +#[derive(Serialize, Debug)] +pub struct SingleTodoResponse { + pub status: String, + pub data: TodoData, +} + +#[derive(Serialize, Debug)] +pub struct TodoListResponse { + pub status: String, + pub results: usize, + pub todos: Vec, +} + +/* +Fuck this is so much fucking boilerplate. java is nothing compared to this shit +rust is good but overkill . could've done this shit in fucking javascript or python ffs +*/ \ No newline at end of file