From 54db7db3bcf5a3d8809970223af63b123b7ffafd Mon Sep 17 00:00:00 2001 From: Jaken Herman Date: Mon, 14 Oct 2024 02:24:00 -0500 Subject: [PATCH] Add initial database for dooly --- .gitignore | 3 +- Cargo.lock | 177 +++++++++++++++ Cargo.toml | 1 + diesel.toml | 9 + migrations/.keep | 0 .../2024-10-14-063139_create_todos/down.sql | 1 + .../2024-10-14-063139_create_todos/up.sql | 5 + src/db.rs | 24 ++ src/main.rs | 210 +++--------------- src/schema.rs | 11 + 10 files changed, 267 insertions(+), 174 deletions(-) create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/2024-10-14-063139_create_todos/down.sql create mode 100644 migrations/2024-10-14-063139_create_todos/up.sql create mode 100644 src/db.rs create mode 100644 src/schema.rs diff --git a/.gitignore b/.gitignore index 9f97022..09c8b81 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +*.sqlite \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0848585..5366080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,41 @@ dependencies = [ "version_check", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -193,15 +228,76 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" +dependencies = [ + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_migrations" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn", +] + [[package]] name = "dooly" version = "0.1.0" dependencies = [ + "diesel", + "diesel_migrations", "rocket", "serde", "serde_json", ] +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" @@ -385,6 +481,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -466,6 +568,12 @@ dependencies = [ "want", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.6.0" @@ -512,6 +620,16 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -564,6 +682,27 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "migrations_internals" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.17" @@ -721,6 +860,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -767,6 +912,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -983,6 +1139,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1109,6 +1274,12 @@ dependencies = [ "loom", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.79" @@ -1370,6 +1541,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 5198797..a8ac656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" [dependencies] rocket = { version = "0.5.1", features = ["json"] } +diesel = { version = "2.0", features = ["r2d2", "sqlite"] } serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..83d15a9 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "./migrations" diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2024-10-14-063139_create_todos/down.sql b/migrations/2024-10-14-063139_create_todos/down.sql new file mode 100644 index 0000000..41a188c --- /dev/null +++ b/migrations/2024-10-14-063139_create_todos/down.sql @@ -0,0 +1 @@ +DROP TABLE todos; \ No newline at end of file diff --git a/migrations/2024-10-14-063139_create_todos/up.sql b/migrations/2024-10-14-063139_create_todos/up.sql new file mode 100644 index 0000000..8b41672 --- /dev/null +++ b/migrations/2024-10-14-063139_create_todos/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT 0 +); \ No newline at end of file diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..f1e8e5f --- /dev/null +++ b/src/db.rs @@ -0,0 +1,24 @@ +use diesel::prelude::*; +use diesel::SqliteConnection; +use rocket::serde::{Serialize, Deserialize}; + +use crate::schema::todos; + +#[derive(Queryable, Serialize, Deserialize)] +pub struct TodoItem { + pub id: i32, + pub title: String, + pub completed: bool, +} + +#[derive(Insertable, Deserialize)] +#[diesel(table_name = todos)] +pub struct NewTodoItem<'a> { + pub title: &'a str, + pub completed: bool, +} + +pub fn establish_connection() -> SqliteConnection { + let database_url = "db.sqlite"; + SqliteConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url)) +} diff --git a/src/main.rs b/src/main.rs index d47af60..af82431 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,193 +1,57 @@ #[macro_use] extern crate rocket; -use rocket::serde::{Deserialize, Serialize, json::Json}; -use rocket::http::Status; -use rocket::State; -use std::sync::{Mutex, PoisonError}; -// Define the TodoItem struct with Serialize and Deserialize -#[derive(Serialize, Deserialize, Clone)] -struct TodoItem { - id: u32, - title: String, - completed: bool, -} +mod db; +mod schema; -// Global mutable list of to-do items -struct AppState { - todos: Mutex>, -} +use db::{establish_connection, TodoItem, NewTodoItem}; +use rocket::serde::{json::Json}; +use rocket::http::Status; +use crate::schema::todos; +use diesel::prelude::*; -// Initialize with some default to-dos -#[rocket::launch] +#[launch] fn rocket() -> _ { - let state = AppState { - todos: Mutex::new(vec![ - TodoItem { id: 1, title: "Learn Rust".to_string(), completed: false }, - TodoItem { id: 2, title: "Build a REST API".to_string(), completed: false }, - ]) - }; rocket::build() - .manage(state) .mount("/", routes![get_todos, add_todo, update_todo, delete_todo]) } -// A helper function to convert Mutex lock errors into HTTP 500 responses. -fn handle_mutex_error(_: PoisonError) -> (Status, &'static str) { - (Status::InternalServerError, "Failed to acquire lock on state") -} - -// GET route for fetching to-do items +// Fetch all to-do items from the database #[get("/todos")] -fn get_todos(state: &State) -> Result>, (Status, &'static str)> { - let todos = state.todos.lock().map_err(handle_mutex_error)?; - Ok(Json(todos.clone())) +fn get_todos() -> Result>, (Status, &'static str)> { + let mut connection = establish_connection(); + let todos: Vec = todos::table.load(&mut connection).map_err(|_| (Status::InternalServerError, "Failed to fetch todos"))?; + Ok(Json(todos)) } -// POST route for adding a new to-do item +// Add a new to-do item to the database #[post("/todos", format = "json", data = "")] -fn add_todo(state: &State, new_todo: Json) -> Result<&'static str, (Status, &'static str)> { - let mut todos = state.todos.lock().map_err(handle_mutex_error)?; - todos.push(new_todo.into_inner()); +fn add_todo(new_todo: Json) -> Result<&'static str, (Status, &'static str)> { + let mut connection = establish_connection(); + let new_todo = NewTodoItem { title: &new_todo.title, completed: new_todo.completed }; + diesel::insert_into(todos::table) + .values(&new_todo) + .execute(&mut connection) + .map_err(|_| (Status::InternalServerError, "Failed to add todo"))?; Ok("Todo added successfully!") } +// Update an existing to-do item #[put("/todos/", format = "json", data = "")] -fn update_todo( - state: &rocket::State, - id: u32, - updated_todo: Json -) -> Result<&'static str, (Status, &'static str)> { - let mut todos = state.todos.lock().map_err(handle_mutex_error)?; - - // Find and update the matching to-do item - if let Some(todo) = todos.iter_mut().find(|todo| todo.id == id) { - *todo = updated_todo.into_inner(); - return Ok("Todo updated successfully!"); - } - - // Return a `NotFound` error if no matching todo was found - Err((Status::NotFound, "Todo not found!")) +fn update_todo(id: i32, updated_todo: Json) -> Result<&'static str, (Status, &'static str)> { + let mut connection = establish_connection(); + diesel::update(todos::table.find(id)) + .set((todos::title.eq(&updated_todo.title), todos::completed.eq(updated_todo.completed))) + .execute(&mut connection) + .map_err(|_| (Status::InternalServerError, "Failed to update todo"))?; + Ok("Todo updated successfully!") } +// Delete a to-do item #[delete("/todos/")] -fn delete_todo( - state: &rocket::State, - id: u32 -) -> Result<&'static str, (Status, &'static str)> { - let mut todos = state.todos.lock().map_err(handle_mutex_error)?; - - // Find the position of the to-do item with the given ID - if let Some(pos) = todos.iter().position(|todo| todo.id == id) { - todos.remove(pos); - return Ok("Todo deleted successfully!"); - } - - // Return a `NotFound` error if no matching todo was found - Err((Status::NotFound, "Todo not found!")) -} - -#[cfg(test)] -mod tests { - use super::*; - use rocket::local::blocking::Client; - use rocket::http::Status; - - #[test] - fn test_get_todos() { - let client = Client::tracked(rocket()).expect("valid rocket instance"); - let response = client.get("/todos").dispatch(); - assert_eq!(response.status(), Status::Ok); - - let todos: Option> = response.into_json().expect("valid json response"); - assert!(todos.is_some(), "Expected some todos"); - let todos = todos.unwrap(); // Unwrap here since we checked is_some - assert_eq!(todos.len(), 2); - } - - #[test] - fn test_add_todo() { - let client = Client::tracked(rocket()).expect("valid rocket instance"); - let new_todo = TodoItem { id: 3, title: "Write tests".to_string(), completed: false }; - - let response = client.post("/todos") - .json(&new_todo) - .dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string().unwrap(), "Todo added successfully!"); - - // Verify the todo was added - let response = client.get("/todos").dispatch(); - let todos: Option> = response.into_json().expect("valid json response"); - assert!(todos.is_some(), "Expected some todos"); - let todos = todos.unwrap(); // Unwrap here since we checked is_some - assert_eq!(todos.len(), 3); - } - - #[test] - fn test_update_todo() { - let client = Client::tracked(rocket()).expect("valid rocket instance"); - let updated_todo = TodoItem { id: 1, title: "Learn Rust Programming".to_string(), completed: true }; - - let response = client.put("/todos/1") - .json(&updated_todo) - .dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string().unwrap(), "Todo updated successfully!"); - - // Verify the todo was updated - let response = client.get("/todos").dispatch(); - let todos: Option> = response.into_json().expect("valid json response"); - assert!(todos.is_some(), "Expected some todos"); - let todos = todos.unwrap(); // Unwrap here since we checked is_some - assert_eq!(todos[0].title, "Learn Rust Programming"); - assert!(todos[0].completed); - } - - #[test] - fn test_delete_todo() { - let client = Client::tracked(rocket()).expect("valid rocket instance"); - - // Initially there should be 2 todos - let initial_response = client.get("/todos").dispatch(); - let initial_todos: Option> = initial_response.into_json().expect("valid json response"); - let initial_todos = initial_todos.unwrap(); - assert_eq!(initial_todos.len(), 2); // Confirm initial count - - // Now delete the second todo (id = 2) - let response = client.delete("/todos/2").dispatch(); - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string().unwrap(), "Todo deleted successfully!"); - - // Verify the todo was deleted - let response = client.get("/todos").dispatch(); - let todos: Option> = response.into_json().expect("valid json response"); - let todos = todos.unwrap(); // Unwrap here since we checked is_some - assert_eq!(todos.len(), 1); // One todo should remain - assert_eq!(todos[0].id, 1); // The remaining todo should be the one with id 1 - } - - - #[test] - fn test_update_nonexistent_todo() { - let client = Client::tracked(rocket()).expect("valid rocket instance"); - let updated_todo = TodoItem { id: 999, title: "Nonexistent".to_string(), completed: false }; - - let response = client.put("/todos/999") - .json(&updated_todo) - .dispatch(); - - assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.into_string().unwrap(), "Todo not found!"); - } - - #[test] - fn test_delete_nonexistent_todo() { - let client = Client::tracked(rocket()).expect("valid rocket instance"); - - let response = client.delete("/todos/999").dispatch(); - assert_eq!(response.status(), Status::NotFound); - assert_eq!(response.into_string().unwrap(), "Todo not found!"); - } -} +fn delete_todo(id: i32) -> Result<&'static str, (Status, &'static str)> { + let mut connection = establish_connection(); + diesel::delete(todos::table.find(id)) + .execute(&mut connection) + .map_err(|_| (Status::InternalServerError, "Failed to delete todo"))?; + Ok("Todo deleted successfully!") +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..1debdcf --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,11 @@ +// src/schema.rs + +// @generated automatically by Diesel CLI. + +diesel::table! { + todos (id) { + id -> Integer, + title -> Text, + completed -> Bool, + } +}