1
0
mirror of https://github.com/actix/examples synced 2025-01-23 14:24:35 +01:00

179 lines
6.0 KiB
Rust
Raw Normal View History

2022-02-06 08:25:38 +00:00
//! Actix Web Diesel integration example
//!
//! 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::{error, get, middleware, post, web, App, HttpResponse, HttpServer, Responder};
use diesel::{prelude::*, r2d2};
use uuid::Uuid;
mod actions;
mod models;
mod schema;
/// 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>,
) -> actix_web::Result<impl Responder> {
let user_uid = user_uid.into_inner();
2019-12-07 23:59:24 +06:00
// use web::block to offload blocking Diesel queries without blocking server thread
2021-10-22 15:47:12 +08:00
let user = web::block(move || {
// note that obtaining a connection from the pool is also potentially blocking
2022-09-10 23:33:09 +01:00
let mut conn = pool.get()?;
2022-09-10 23:33:09 +01:00
actions::find_user_by_uid(&mut conn, user_uid)
2021-10-22 15:47:12 +08:00
})
.await?
// map diesel query errors to a 500 error response
.map_err(error::ErrorInternalServerError)?;
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}")),
})
}
/// 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>,
) -> actix_web::Result<impl Responder> {
// use web::block to offload blocking Diesel queries without blocking server thread
2021-10-22 15:47:12 +08:00
let user = web::block(move || {
// note that obtaining a connection from the pool is also potentially blocking
2022-09-10 23:33:09 +01:00
let mut conn = pool.get()?;
2022-09-10 23:33:09 +01:00
actions::insert_new_user(&mut conn, &form.name)
2021-10-22 15:47:12 +08:00
})
.await?
// map diesel query errors to a 500 error response
.map_err(error::ErrorInternalServerError)?;
2019-12-07 23:59:24 +06:00
// user was added successfully; return 201 response with new user info
Ok(HttpResponse::Created().json(user))
}
2020-09-12 16:49:45 +01:00
#[actix_web::main]
2019-12-07 23:59:24 +06:00
async fn main() -> std::io::Result<()> {
2023-10-29 01:18:40 +01:00
dotenvy::dotenv().ok();
2022-02-18 02:32:44 +00:00
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// initialize DB pool outside of `HttpServer::new` so that it is shared across all workers
let pool = initialize_db_pool();
2022-02-18 02:32:44 +00:00
log::info!("starting HTTP server at http://localhost:8080");
2019-03-07 14:50:29 -08:00
HttpServer::new(move || {
App::new()
// 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
2019-03-26 04:29:00 +01:00
.wrap(middleware::Logger::default())
// add route handlers
.service(get_user)
.service(add_user)
2019-03-07 14:50:29 -08:00
})
2022-02-18 02:32:44 +00:00
.bind(("127.0.0.1", 8080))?
2019-12-25 20:48:33 +04:00
.run()
2019-12-07 23:59:24 +06:00
.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::{http::StatusCode, test};
2022-10-16 21:20:54 +01:00
use super::*;
#[actix_web::test]
async fn user_routes() {
2023-10-29 01:18:40 +01:00
dotenvy::dotenv().ok();
env_logger::try_init_from_env(env_logger::Env::new().default_filter_or("info")).ok();
let pool = initialize_db_pool();
2022-06-07 22:53:28 -04:00
let app = test::init_service(
App::new()
.app_data(web::Data::new(pool.clone()))
.wrap(middleware::Logger::default())
.service(get_user)
.service(add_user),
)
.await;
// 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::new("Test user"))
.to_request();
let res: models::User = test::call_and_read_body_json(&app, req).await;
assert_eq!(res.name, "Test user");
// get a user
let req = test::TestRequest::get()
.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");
// delete new user from table
use crate::schema::users::dsl::*;
diesel::delete(users.filter(id.eq(res.id)))
2022-09-10 23:33:09 +01:00
.execute(&mut pool.get().expect("couldn't get db connection from pool"))
.expect("couldn't delete test user from table");
}
}