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:
parent
6571dfef82
commit
fd7d4c8a23
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user