diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 6b63f13..b5b796d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -66,5 +66,5 @@ jobs: cd databases/diesel diesel migration run chmod a+rwx test.db - cargo test -p=diesel-example --all-features --no-fail-fast -- --nocapture + cargo test -p=diesel-example --no-fail-fast -- --nocapture timeout-minutes: 10 diff --git a/databases/diesel/README.md b/databases/diesel/README.md index 646cdf6..c24e083 100644 --- a/databases/diesel/README.md +++ b/databases/diesel/README.md @@ -1,6 +1,6 @@ # diesel -Basic integration of [Diesel](https://diesel.rs/) using SQLite for Actix Web. +Basic integration of [Diesel](https://diesel.rs) using SQLite for Actix Web. ## Usage @@ -66,7 +66,7 @@ On success, a response like the following is returned:
Client Examples -Using [HTTPie](https://httpie.org/): +Using [HTTPie]: ```sh http POST localhost:8080/user name=bill @@ -87,7 +87,7 @@ Gets a user from the DB using its UID (returned from the insert request or taken
Client Examples -Using [HTTPie](https://httpie.org/): +Using [HTTPie]: ```sh http localhost:8080/user/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 @@ -115,3 +115,5 @@ sqlite> SELECT * FROM users; ## Using Other Databases You can find a complete example of Diesel + PostgreSQL at: [https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix) + +[httpie]: https://httpie.io/cli diff --git a/databases/diesel/src/main.rs b/databases/diesel/src/main.rs index c2078dc..1cfe58a 100644 --- a/databases/diesel/src/main.rs +++ b/databases/diesel/src/main.rs @@ -1,63 +1,77 @@ //! Actix Web Diesel integration example //! -//! Diesel does not support tokio, so we have to run it in separate threads using the web::block -//! function which offloads blocking code (like Diesel's) in order to not block the server's thread. +//! Diesel v2 is not an async library, so we have to execute queries in `web::block` closures which +//! offload blocking code (like Diesel's) to a thread-pool in order to not block the server. #[macro_use] extern crate diesel; -use actix_web::{get, middleware, post, web, App, Error, HttpResponse, HttpServer}; -use diesel::{ - prelude::*, - r2d2::{self, ConnectionManager}, -}; +use actix_web::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder}; +use diesel::{prelude::*, r2d2}; use uuid::Uuid; mod actions; mod models; mod schema; -type DbPool = r2d2::Pool>; +/// Short-hand for the database pool type to use throughout the app. +type DbPool = r2d2::Pool>; /// Finds user by UID. +/// +/// Extracts: +/// - the database pool handle from application data +/// - a user UID from the request path #[get("/user/{user_id}")] async fn get_user( pool: web::Data, user_uid: web::Path, -) -> Result { +) -> actix_web::Result { let user_uid = user_uid.into_inner(); - // use web::block to offload blocking Diesel code without blocking server thread + // use web::block to offload blocking Diesel queries without blocking server thread let user = web::block(move || { + // note that obtaining a connection from the pool is also potentially blocking let mut conn = pool.get()?; + actions::find_user_by_uid(&mut conn, user_uid) }) .await? - .map_err(actix_web::error::ErrorInternalServerError)?; + // map diesel query errors to a 500 error response + .map_err(error::ErrorInternalServerError)?; - if let Some(user) = user { - Ok(HttpResponse::Ok().json(user)) - } else { - let res = HttpResponse::NotFound().body(format!("No user found with uid: {user_uid}")); - Ok(res) - } + Ok(match user { + // user was found; return 200 response with JSON formatted user object + Some(user) => HttpResponse::Ok().json(user), + + // user was not found; return 404 response with error message + None => HttpResponse::NotFound().body(format!("No user found with UID: {user_uid}")), + }) } -/// Inserts new user with name defined in form. +/// Creates new user. +/// +/// Extracts: +/// - the database pool handle from application data +/// - a JSON form containing new user info from the request body #[post("/user")] async fn add_user( pool: web::Data, form: web::Json, -) -> Result { - // use web::block to offload blocking Diesel code without blocking server thread +) -> actix_web::Result { + // use web::block to offload blocking Diesel queries without blocking server thread let user = web::block(move || { + // note that obtaining a connection from the pool is also potentially blocking let mut conn = pool.get()?; + actions::insert_new_user(&mut conn, &form.name) }) .await? - .map_err(actix_web::error::ErrorInternalServerError)?; + // map diesel query errors to a 500 error response + .map_err(error::ErrorInternalServerError)?; - Ok(HttpResponse::Ok().json(user)) + // user was added successfully; return 201 response with new user info + Ok(HttpResponse::Created().json(user)) } #[actix_web::main] @@ -65,21 +79,18 @@ async fn main() -> std::io::Result<()> { dotenv::dotenv().ok(); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - // set up database connection pool - let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); - let manager = ConnectionManager::::new(conn_spec); - let pool = r2d2::Pool::builder() - .build(manager) - .expect("Failed to create pool."); + // initialize DB pool outside of `HttpServer::new` so that it is shared across all workers + let pool = initialize_db_pool(); log::info!("starting HTTP server at http://localhost:8080"); - // Start HTTP server HttpServer::new(move || { App::new() - // set up DB pool to be used with web::Data extractor + // add DB pool handle to app data; enables use of `web::Data` extractor .app_data(web::Data::new(pool.clone())) + // add request logger middleware .wrap(middleware::Logger::default()) + // add route handlers .service(get_user) .service(add_user) }) @@ -88,23 +99,29 @@ async fn main() -> std::io::Result<()> { .await } +/// Initialize database connection pool based on `DATABASE_URL` environment variable. +/// +/// See more: . +fn initialize_db_pool() -> DbPool { + let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL should be set"); + let manager = r2d2::ConnectionManager::::new(conn_spec); + r2d2::Pool::builder() + .build(manager) + .expect("database URL should be valid path to SQLite DB file") +} + #[cfg(test)] mod tests { - use actix_web::test; + use actix_web::{http::StatusCode, test}; use super::*; #[actix_web::test] async fn user_routes() { - std::env::set_var("RUST_LOG", "actix_web=debug"); - env_logger::init(); dotenv::dotenv().ok(); + env_logger::try_init_from_env(env_logger::Env::new().default_filter_or("info")).ok(); - let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); - let manager = ConnectionManager::::new(conn_spec); - let pool = r2d2::Pool::builder() - .build(manager) - .expect("Failed to create pool."); + let pool = initialize_db_pool(); let app = test::init_service( App::new() @@ -115,30 +132,46 @@ mod tests { ) .await; - // Insert a user + // send something that isn't a UUID to `get_user` + let req = test::TestRequest::get().uri("/user/123").to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body = test::read_body(res).await; + assert!( + body.starts_with(b"UUID parsing failed"), + "unexpected body: {body:?}", + ); + + // try to find a non-existent user + let req = test::TestRequest::get() + .uri(&format!("/user/{}", Uuid::nil())) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + let body = test::read_body(res).await; + assert!( + body.starts_with(b"No user found"), + "unexpected body: {body:?}", + ); + + // create new user let req = test::TestRequest::post() .uri("/user") - .set_json(&models::NewUser { - name: "Test user".to_owned(), - }) + .set_json(models::NewUser::new("Test user")) .to_request(); + let res: models::User = test::call_and_read_body_json(&app, req).await; + assert_eq!(res.name, "Test user"); - let resp: models::User = test::call_and_read_body_json(&app, req).await; - - assert_eq!(resp.name, "Test user"); - - // Get a user + // get a user let req = test::TestRequest::get() - .uri(&format!("/user/{}", resp.id)) + .uri(&format!("/user/{}", res.id)) .to_request(); + let res: models::User = test::call_and_read_body_json(&app, req).await; + assert_eq!(res.name, "Test user"); - let resp: models::User = test::call_and_read_body_json(&app, req).await; - - assert_eq!(resp.name, "Test user"); - - // Delete new user from table + // delete new user from table use crate::schema::users::dsl::*; - diesel::delete(users.filter(id.eq(resp.id))) + diesel::delete(users.filter(id.eq(res.id))) .execute(&mut pool.get().expect("couldn't get db connection from pool")) .expect("couldn't delete test user from table"); } diff --git a/databases/diesel/src/models.rs b/databases/diesel/src/models.rs index 0187631..db2ae79 100644 --- a/databases/diesel/src/models.rs +++ b/databases/diesel/src/models.rs @@ -2,13 +2,24 @@ use serde::{Deserialize, Serialize}; use crate::schema::users; +/// User details. #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] +#[diesel(table_name = users)] pub struct User { pub id: String, pub name: String, } +/// New user details. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewUser { pub name: String, } + +impl NewUser { + /// Constructs new user details from name. + #[cfg(test)] // only needed in tests + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} diff --git a/forms/multipart/src/main.rs b/forms/multipart/src/main.rs index e5f8e32..12b949b 100644 --- a/forms/multipart/src/main.rs +++ b/forms/multipart/src/main.rs @@ -3,7 +3,6 @@ use std::io::Write; use actix_multipart::{ form::{ tempfile::{TempFile, TempFileConfig}, - text::Text, MultipartForm, }, Multipart,