1
0
mirror of https://github.com/actix/examples synced 2024-11-27 16:02:57 +01:00

improve documentation of diesel example

This commit is contained in:
Rob Ede 2023-03-14 02:33:45 +00:00
parent 6571dfef82
commit fd7d4c8a23
No known key found for this signature in database
GPG Key ID: 97C636207D3EF933
5 changed files with 104 additions and 59 deletions

View File

@ -66,5 +66,5 @@ jobs:
cd databases/diesel cd databases/diesel
diesel migration run diesel migration run
chmod a+rwx test.db 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 timeout-minutes: 10

View File

@ -1,6 +1,6 @@
# diesel # 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 ## Usage
@ -66,7 +66,7 @@ On success, a response like the following is returned:
<details> <details>
<summary>Client Examples</summary> <summary>Client Examples</summary>
Using [HTTPie](https://httpie.org/): Using [HTTPie]:
```sh ```sh
http POST localhost:8080/user name=bill 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
<details> <details>
<summary>Client Examples</summary> <summary>Client Examples</summary>
Using [HTTPie](https://httpie.org/): Using [HTTPie]:
```sh ```sh
http localhost:8080/user/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 http localhost:8080/user/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97
@ -115,3 +115,5 @@ sqlite> SELECT * FROM users;
## Using Other Databases ## 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) 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

View File

@ -1,63 +1,77 @@
//! Actix Web Diesel integration example //! Actix Web Diesel integration example
//! //!
//! Diesel does not support tokio, so we have to run it in separate threads using the web::block //! Diesel v2 is not an async library, so we have to execute queries in `web::block` closures which
//! function which offloads blocking code (like Diesel's) in order to not block the server's thread. //! offload blocking code (like Diesel's) to a thread-pool in order to not block the server.
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
use actix_web::{get, middleware, post, web, App, Error, HttpResponse, HttpServer}; use actix_web::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder};
use diesel::{ use diesel::{prelude::*, r2d2};
prelude::*,
r2d2::{self, ConnectionManager},
};
use uuid::Uuid; use uuid::Uuid;
mod actions; mod actions;
mod models; mod models;
mod schema; mod schema;
type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>; /// Short-hand for the database pool type to use throughout the app.
type DbPool = r2d2::Pool<r2d2::ConnectionManager<SqliteConnection>>;
/// Finds user by UID. /// Finds user by UID.
///
/// Extracts:
/// - the database pool handle from application data
/// - a user UID from the request path
#[get("/user/{user_id}")] #[get("/user/{user_id}")]
async fn get_user( async fn get_user(
pool: web::Data<DbPool>, pool: web::Data<DbPool>,
user_uid: web::Path<Uuid>, user_uid: web::Path<Uuid>,
) -> Result<HttpResponse, Error> { ) -> actix_web::Result<impl Responder> {
let user_uid = user_uid.into_inner(); 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 || { let user = web::block(move || {
// note that obtaining a connection from the pool is also potentially blocking
let mut conn = pool.get()?; let mut conn = pool.get()?;
actions::find_user_by_uid(&mut conn, user_uid) actions::find_user_by_uid(&mut conn, user_uid)
}) })
.await? .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(match user {
Ok(HttpResponse::Ok().json(user)) // user was found; return 200 response with JSON formatted user object
} else { Some(user) => HttpResponse::Ok().json(user),
let res = HttpResponse::NotFound().body(format!("No user found with uid: {user_uid}"));
Ok(res) // 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")] #[post("/user")]
async fn add_user( async fn add_user(
pool: web::Data<DbPool>, pool: web::Data<DbPool>,
form: web::Json<models::NewUser>, form: web::Json<models::NewUser>,
) -> Result<HttpResponse, Error> { ) -> actix_web::Result<impl Responder> {
// 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 || { let user = web::block(move || {
// note that obtaining a connection from the pool is also potentially blocking
let mut conn = pool.get()?; let mut conn = pool.get()?;
actions::insert_new_user(&mut conn, &form.name) actions::insert_new_user(&mut conn, &form.name)
}) })
.await? .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] #[actix_web::main]
@ -65,21 +79,18 @@ async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// set up database connection pool // initialize DB pool outside of `HttpServer::new` so that it is shared across all workers
let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); let pool = initialize_db_pool();
let manager = ConnectionManager::<SqliteConnection>::new(conn_spec);
let pool = r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool.");
log::info!("starting HTTP server at http://localhost:8080"); log::info!("starting HTTP server at http://localhost:8080");
// Start HTTP server
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
// set up DB pool to be used with web::Data<Pool> extractor // add DB pool handle to app data; enables use of `web::Data<DbPool>` extractor
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))
// add request logger middleware
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
// add route handlers
.service(get_user) .service(get_user)
.service(add_user) .service(add_user)
}) })
@ -88,23 +99,29 @@ async fn main() -> std::io::Result<()> {
.await .await
} }
/// Initialize database connection pool based on `DATABASE_URL` environment variable.
///
/// See more: <https://docs.rs/diesel/latest/diesel/r2d2/index.html>.
fn initialize_db_pool() -> DbPool {
let conn_spec = std::env::var("DATABASE_URL").expect("DATABASE_URL should be set");
let manager = r2d2::ConnectionManager::<SqliteConnection>::new(conn_spec);
r2d2::Pool::builder()
.build(manager)
.expect("database URL should be valid path to SQLite DB file")
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::test; use actix_web::{http::StatusCode, test};
use super::*; use super::*;
#[actix_web::test] #[actix_web::test]
async fn user_routes() { async fn user_routes() {
std::env::set_var("RUST_LOG", "actix_web=debug");
env_logger::init();
dotenv::dotenv().ok(); 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 pool = initialize_db_pool();
let manager = ConnectionManager::<SqliteConnection>::new(conn_spec);
let pool = r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool.");
let app = test::init_service( let app = test::init_service(
App::new() App::new()
@ -115,30 +132,46 @@ mod tests {
) )
.await; .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() let req = test::TestRequest::post()
.uri("/user") .uri("/user")
.set_json(&models::NewUser { .set_json(models::NewUser::new("Test user"))
name: "Test user".to_owned(),
})
.to_request(); .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; // get a user
assert_eq!(resp.name, "Test user");
// Get a user
let req = test::TestRequest::get() let req = test::TestRequest::get()
.uri(&format!("/user/{}", resp.id)) .uri(&format!("/user/{}", res.id))
.to_request(); .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; // delete new user from table
assert_eq!(resp.name, "Test user");
// Delete new user from table
use crate::schema::users::dsl::*; 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")) .execute(&mut pool.get().expect("couldn't get db connection from pool"))
.expect("couldn't delete test user from table"); .expect("couldn't delete test user from table");
} }

View File

@ -2,13 +2,24 @@ use serde::{Deserialize, Serialize};
use crate::schema::users; use crate::schema::users;
/// User details.
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
#[diesel(table_name = users)]
pub struct User { pub struct User {
pub id: String, pub id: String,
pub name: String, pub name: String,
} }
/// New user details.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewUser { pub struct NewUser {
pub name: String, pub name: String,
} }
impl NewUser {
/// Constructs new user details from name.
#[cfg(test)] // only needed in tests
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}

View File

@ -3,7 +3,6 @@ use std::io::Write;
use actix_multipart::{ use actix_multipart::{
form::{ form::{
tempfile::{TempFile, TempFileConfig}, tempfile::{TempFile, TempFileConfig},
text::Text,
MultipartForm, MultipartForm,
}, },
Multipart, Multipart,