diff --git a/diesel/.gitignore b/diesel/.gitignore new file mode 100644 index 00000000..c370cb64 --- /dev/null +++ b/diesel/.gitignore @@ -0,0 +1 @@ +test.db diff --git a/diesel/Cargo.toml b/diesel/Cargo.toml index 78cf2c45..34d2b32a 100644 --- a/diesel/Cargo.toml +++ b/diesel/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "diesel-example" version = "1.0.0" -authors = ["Nikolay Kim "] +authors = [ + "Nikolay Kim ", + "Rob Ede ", +] workspace = ".." edition = "2018" @@ -9,13 +12,12 @@ edition = "2018" actix-rt = "1.0.0" actix-web = "2.0.0" -bytes = "0.4" -env_logger = "0.6" +bytes = "0.5" +diesel = { version = "^1.1.0", features = ["sqlite", "r2d2"] } +dotenv = "0.15" +env_logger = "0.7" futures = "0.3.1" -uuid = { version = "0.5", features = ["serde", "v4"] } +r2d2 = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" - -diesel = { version = "^1.1.0", features = ["sqlite", "r2d2"] } -r2d2 = "0.8" -dotenv = "0.10" +uuid = { version = "0.8", features = ["serde", "v4"] } diff --git a/diesel/README.md b/diesel/README.md index 13d45f08..2ed15d44 100644 --- a/diesel/README.md +++ b/diesel/README.md @@ -1,46 +1,109 @@ # diesel -Diesel's `Getting Started` guide using SQLite for Actix web +Basic integration of [Diesel](https://diesel.rs/) using SQLite for Actix web. ## Usage -### init database sqlite +### Install SQLite -```bash -# if opensuse: sudo zypper install sqlite3-devel -cargo install diesel_cli --no-default-features --features sqlite +```sh +# on OpenSUSE +sudo zypper install sqlite3-devel libsqlite3-0 sqlite3 + +# on Ubuntu +sudo apt-get install libsqlite3-dev sqlite3 + +# on Fedora +sudo dnf install libsqlite3x-devel sqlite3x + +# on macOS (using homebrew) +brew install sqlite3 +``` + +### Initialize SQLite Database + +```sh cd examples/diesel +cargo install diesel_cli --no-default-features --features sqlite + echo "DATABASE_URL=test.db" > .env diesel migration run ``` -### server +There will now be a database file at `./test.db`. -```bash -# if ubuntu : sudo apt-get install libsqlite3-dev -# if fedora : sudo dnf install libsqlite3x-devel -# if opensuse: sudo zypper install libsqlite3-0 +### Running Server + +```sh cd examples/diesel cargo run (or ``cargo watch -x run``) + # Started http server: 127.0.0.1:8080 ``` -### web client +### Available Routes -[http://127.0.0.1:8080/NAME](http://127.0.0.1:8080/NAME) +#### `POST /user` -### sqlite client +Inserts a new user into the SQLite DB. -```bash -# if ubuntu : sudo apt-get install sqlite3 -# if fedora : sudo dnf install sqlite3x -# if opensuse: sudo zypper install sqlite3 +Provide a JSON payload with a name. Eg: +```json +{ "name": "bill" } +``` + +On success, a response like the following is returned: +```json +{ + "id": "9e46baba-a001-4bb3-b4cf-4b3e5bab5e97", + "name": "bill" +} +``` + +
+ Client Examples + + Using [HTTPie](https://httpie.org/): + ```sh + http POST localhost:8080/user name=bill + ``` + + Using cURL: + ```sh + curl -S -X POST --header "Content-Type: application/json" --data '{"name":"bill"}' http://localhost:8080/user + ``` +
+ +#### `GET /user/{user_uid}` + +Gets a user from the DB using its UID (returned from the insert request or taken from the DB directly). Returns a 404 when no user exists with that UID. + +
+ Client Examples + + Using [HTTPie](https://httpie.org/): + ```sh + http localhost:8080/user/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 + ``` + + Using cURL: + ```sh + curl -S http://localhost:8080/user/9e46baba-a001-4bb3-b4cf-4b3e5bab5e97 + ``` +
+ +### Explore The SQLite DB + +```sh sqlite3 test.db +``` + +``` sqlite> .tables -sqlite> select * from users; +sqlite> SELECT * FROM users; ``` -## Postgresql +## Using Other Databases -You will also find another complete example of diesel+postgresql on [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) diff --git a/diesel/src/actions.rs b/diesel/src/actions.rs new file mode 100644 index 00000000..7a4ec2f7 --- /dev/null +++ b/diesel/src/actions.rs @@ -0,0 +1,40 @@ +use diesel::prelude::*; +use uuid::Uuid; + +use crate::models; + +/// Run query using Diesel to insert a new database row and return the result. +pub fn find_user_by_uid( + uid: Uuid, + conn: &SqliteConnection, +) -> Result, diesel::result::Error> { + use crate::schema::users::dsl::*; + + let user = users + .filter(id.eq(uid.to_string())) + .first::(conn) + .optional()?; + + Ok(user) +} + +/// Run query using Diesel to insert a new database row and return the result. +pub fn insert_new_user( + // prevent collision with `name` column imported inside the function + nm: &str, + conn: &SqliteConnection, +) -> Result { + // It is common when using Diesel with Actix web to import schema-related + // modules inside a function's scope (rather than the normal module's scope) + // to prevent import collisions and namespace pollution. + use crate::schema::users::dsl::*; + + let new_user = models::User { + id: Uuid::new_v4().to_string(), + name: nm.to_owned(), + }; + + diesel::insert_into(users).values(&new_user).execute(conn)?; + + Ok(new_user) +} diff --git a/diesel/src/main.rs b/diesel/src/main.rs index f5d019e6..8494842f 100644 --- a/diesel/src/main.rs +++ b/diesel/src/main.rs @@ -1,140 +1,94 @@ -//! Actix web diesel example +//! Actix web Diesel integration example //! -//! Diesel does not support tokio, so we have to run it in separate threads. -//! Actix supports sync actors by default, so we going to create sync actor -//! that use diesel. Technically sync actors are worker style actors, multiple -//! of them can run in parallel and process messages from same queue. +//! 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. + #[macro_use] extern crate diesel; -use serde::{Deserialize, Serialize}; -use actix_web::{error, middleware, web, App, Error, HttpResponse, HttpServer}; +use actix_web::{get, middleware, post, web, App, Error, HttpResponse, HttpServer}; use diesel::prelude::*; use diesel::r2d2::{self, ConnectionManager}; -use dotenv; +use uuid::Uuid; +mod actions; mod models; mod schema; -type Pool = r2d2::Pool>; +type DbPool = r2d2::Pool>; -#[derive(Debug, Serialize, Deserialize)] -struct MyUser { - name: String, -} - -/// Diesel query -fn query( - nm: String, - pool: web::Data, -) -> Result { - use self::schema::users::dsl::*; - - let uuid = format!("{}", uuid::Uuid::new_v4()); - let new_user = models::NewUser { - id: &uuid, - name: nm.as_str(), - }; - let conn: &SqliteConnection = &pool.get().unwrap(); - - diesel::insert_into(users).values(&new_user).execute(conn)?; - - let mut items = users.filter(id.eq(&uuid)).load::(conn)?; - Ok(items.pop().unwrap()) -} - -/// Async request handler -async fn add( - name: web::Path, - pool: web::Data, +/// Finds user by UID. +#[get("/user/{user_id}")] +async fn get_user( + pool: web::Data, + user_uid: web::Path, ) -> Result { - // run diesel blocking code - Ok(web::block(move || query(name.into_inner(), pool)) + let user_uid = user_uid.into_inner(); + let conn = pool.get().expect("couldn't get db connection from pool"); + + // use web::block to offload blocking Diesel code without blocking server thread + let user = web::block(move || actions::find_user_by_uid(user_uid, &conn)) .await - .map(|user| HttpResponse::Ok().json(user)) - .map_err(|_| HttpResponse::InternalServerError())?) -} + .map_err(|e| { + eprintln!("{}", e); + HttpResponse::InternalServerError().finish() + })?; -/// This handler manually parse json object. Bytes object supports FromRequest trait (extractor) -/// and could be loaded from request payload automatically -async fn index_add( - body: web::Bytes, - pool: web::Data, -) -> Result { - // body is loaded, now we can deserialize id with serde-json - let r_obj = serde_json::from_slice::(&body); - - // Send to the db for create return response to peer - match r_obj { - Ok(obj) => { - let user = web::block(move || query(obj.name, pool)) - .await - .map_err(|_| Error::from(HttpResponse::InternalServerError()))?; - Ok(HttpResponse::Ok().json(user)) - } - Err(_) => Err(error::ErrorBadRequest("Json Decode Failed")), + 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) } } -/// This handler offloads json deserialization to actix-web's Json extrator -async fn add2( - item: web::Json, - pool: web::Data, +/// Inserts new user with name defined in form. +#[post("/user")] +async fn add_user( + pool: web::Data, + form: web::Json, ) -> Result { - // run diesel blocking code - let user = web::block(move || query(item.into_inner().name, pool)) + let conn = pool.get().expect("couldn't get db connection from pool"); + + // use web::block to offload blocking Diesel code without blocking server thread + let user = web::block(move || actions::insert_new_user(&form.name, &conn)) .await - .map_err(|_| HttpResponse::InternalServerError())?; // convert diesel error to http response + .map_err(|e| { + eprintln!("{}", e); + HttpResponse::InternalServerError().finish() + })?; Ok(HttpResponse::Ok().json(user)) } #[actix_rt::main] async fn main() -> std::io::Result<()> { - std::env::set_var("RUST_LOG", "actix_web=info"); + std::env::set_var("RUST_LOG", "actix_web=info,diesel=debug"); env_logger::init(); - dotenv::dotenv().ok(); + // set up database connection pool let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); let manager = ConnectionManager::::new(connspec); let pool = r2d2::Pool::builder() .build(manager) .expect("Failed to create pool."); - // Start http server + let bind = "127.0.0.1:8080"; + + println!("Starting server at: {}", &bind); + + // Start HTTP server HttpServer::new(move || { App::new() + // set up DB pool to be used with web::Data extractor .data(pool.clone()) - // enable logger .wrap(middleware::Logger::default()) - // This can be called with: - // curl -S --header "Content-Type: application/json" --request POST --data '{"name":"xyz"}' http://127.0.0.1:8080/add - // Use of the extractors makes some post conditions simpler such - // as size limit protections and built in json validation. - .service( - web::resource("/add2") - .data( - web::JsonConfig::default() - .limit(4096) // <- limit size of the payload - .error_handler(|err, _| { - // <- create custom error response - error::InternalError::from_response( - err, - HttpResponse::Conflict().finish(), - ) - .into() - }), - ) - .route(web::post().to(add2)), - ) - // Manual parsing would allow custom error construction, use of - // other parsers *beside* json (for example CBOR, protobuf, xml), and allows - // an application to standardise on a single parser implementation. - .service(web::resource("/add").route(web::post().to(index_add))) - .service(web::resource("/add/{name}").route(web::get().to(add))) + .service(get_user) + .service(add_user) }) - .bind("127.0.0.1:8080")? + .bind(&bind)? .run() .await } diff --git a/diesel/src/models.rs b/diesel/src/models.rs index 9d8c816e..1409be11 100644 --- a/diesel/src/models.rs +++ b/diesel/src/models.rs @@ -1,15 +1,14 @@ -use super::schema::users; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Queryable)] +use crate::schema::users; + +#[derive(Debug, Clone, Serialize, Queryable, Insertable)] pub struct User { pub id: String, pub name: String, } -#[derive(Insertable)] -#[table_name = "users"] -pub struct NewUser<'a> { - pub id: &'a str, - pub name: &'a str, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewUser { + pub name: String, }