1
0
mirror of https://github.com/actix/examples synced 2024-11-23 22:41:07 +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
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

View File

@ -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:
<details>
<summary>Client Examples</summary>
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
<details>
<summary>Client Examples</summary>
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

View File

@ -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<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.
///
/// 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<DbPool>,
user_uid: web::Path<Uuid>,
) -> Result<HttpResponse, Error> {
) -> actix_web::Result<impl Responder> {
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<DbPool>,
form: web::Json<models::NewUser>,
) -> Result<HttpResponse, Error> {
// use web::block to offload blocking Diesel code without blocking server thread
) -> actix_web::Result<impl Responder> {
// 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::<SqliteConnection>::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<Pool> extractor
// add DB pool handle to app data; enables use of `web::Data<DbPool>` 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: <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)]
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::<SqliteConnection>::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");
}

View File

@ -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<String>) -> Self {
Self { name: name.into() }
}
}

View File

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