diff --git a/Cargo.toml b/Cargo.toml index 6efbd2b3..e5e54c48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "async_db", "async_ex1", "async_ex2", + "async_pg", "basics", "cookie-auth", "cookie-session", diff --git a/async_pg/.gitignore b/async_pg/.gitignore new file mode 100644 index 00000000..53eaa219 --- /dev/null +++ b/async_pg/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/async_pg/Cargo.toml b/async_pg/Cargo.toml new file mode 100644 index 00000000..c7d81273 --- /dev/null +++ b/async_pg/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "async_pg" +version = "0.1.0" +authors = ["dowwie "] +edition = "2018" + + +[dependencies] +actix-rt = "1.0.0" +actix-web = "2.0.0" +deadpool-postgres = "0.4.3" +derive_more = "0.99.2" +serde = { version = "1.0.104", features=["derive"] } +tokio-pg-mapper = "0.1.4" +tokio-pg-mapper-derive = "0.1.4" +tokio-postgres = "0.5.1" diff --git a/async_pg/README.md b/async_pg/README.md new file mode 100644 index 00000000..4cedd651 --- /dev/null +++ b/async_pg/README.md @@ -0,0 +1,12 @@ +This example illustrates: + - tokio_postgres + - use of tokio_pg_mapper for postgres data mapping + - deadpool_postgres for connection pooling + + +# Instructions +1. Set up the testing database by running /sql/create_db.sh +2. `cargo run` +3. from the command line (linux), POST a user to the endpoint: + echo '{"email": "ferris@thecrab.com", "first_name": "ferris", "last_name": "crab", "username": "ferreal"}' | http -f --json --print h POST http://127.0.0.1:8080/users + - a unique constraint exists for username, so running this twice will return a 500 diff --git a/async_pg/sql/add_user.sql b/async_pg/sql/add_user.sql new file mode 100644 index 00000000..80ef03f8 --- /dev/null +++ b/async_pg/sql/add_user.sql @@ -0,0 +1,3 @@ +INSERT INTO testing.users(email, first_name, last_name, username) +VALUES ($1, $2, $3, $4) +RETURNING $table_fields; diff --git a/async_pg/sql/create_db.sh b/async_pg/sql/create_db.sh new file mode 100755 index 00000000..6505d073 --- /dev/null +++ b/async_pg/sql/create_db.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +psql -U postgres -h 127.0.0.1 -f setup_db_and_user.sql diff --git a/async_pg/sql/get_users.sql b/async_pg/sql/get_users.sql new file mode 100644 index 00000000..3ff247dd --- /dev/null +++ b/async_pg/sql/get_users.sql @@ -0,0 +1 @@ +SELECT $table_fields FROM testing.users; diff --git a/async_pg/sql/setup_db_and_user.sql b/async_pg/sql/setup_db_and_user.sql new file mode 100644 index 00000000..dc515dd2 --- /dev/null +++ b/async_pg/sql/setup_db_and_user.sql @@ -0,0 +1,20 @@ +DROP DATABASE IF EXISTS testing_db; + +CREATE USER test_user WITH PASSWORD 'testing'; + +CREATE DATABASE testing_db OWNER test_user; + +\connect testing_db; + +DROP SCHEMA IF EXISTS testing CASCADE; +CREATE SCHEMA testing; + + +CREATE TABLE testing.users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(200) NOT NULL, + first_name VARCHAR(200) NOT NULL, + last_name VARCHAR(200) NOT NULL, + username VARCHAR(50) UNIQUE NOT NULL, + UNIQUE (username) +); diff --git a/async_pg/src/main.rs b/async_pg/src/main.rs new file mode 100644 index 00000000..c285c4bb --- /dev/null +++ b/async_pg/src/main.rs @@ -0,0 +1,127 @@ + +mod models { + use serde::{Deserialize, Serialize}; + use tokio_pg_mapper_derive::PostgresMapper; + + #[derive(Deserialize, PostgresMapper, Serialize)] + #[pg_mapper(table = "users")] // singular 'user' is a keyword.. + pub struct User { + pub email: String, + pub first_name: String, + pub last_name: String, + pub username: String + } +} + + +mod errors { + use actix_web::{HttpResponse, ResponseError}; + use derive_more::{Display, From}; + use deadpool_postgres::PoolError; + use tokio_postgres::error::Error as PGError; + use tokio_pg_mapper::Error as PGMError; + + + #[derive(Display, From, Debug)] + pub enum MyError { + NotFound, + PGError(PGError), + PGMError(PGMError), + PoolError(PoolError) + } + impl std::error::Error for MyError {} + + impl ResponseError for MyError { + fn error_response(&self) -> HttpResponse { + match *self { + MyError::NotFound => HttpResponse::NotFound().finish(), + _ => HttpResponse::InternalServerError().finish() + } + } + } +} + + +mod db { + use crate::{errors::MyError, models::User}; + use deadpool_postgres::Client; + use tokio_pg_mapper::FromTokioPostgresRow; + + + pub async fn add_user(client: &Client, user_info: User) -> Result { + let _stmt = include_str!("../sql/add_user.sql"); + let _stmt = _stmt.replace("$table_fields", &User::sql_table_fields()); + let stmt = client.prepare(&_stmt) + .await + .unwrap(); + + client.query(&stmt, + &[&user_info.email, + &user_info.first_name, + &user_info.last_name, + &user_info.username + ]) + .await? + .iter() + .map(|row| User::from_row_ref(row).unwrap()) + .collect::>() + .pop() + .ok_or(MyError::NotFound) // more applicable for SELECTs + } +} + + +mod handlers { + use actix_web::{HttpResponse, web, Error}; + use deadpool_postgres::{Client, Pool}; + use crate::{db, errors::MyError, models::User}; + + + pub async fn add_user(user: web::Json, db_pool: web::Data) + -> Result { + + let user_info: User = user.into_inner(); + + let client: Client = + db_pool.get() + .await + .map_err(|err| MyError::PoolError(err))?; + + let new_user = db::add_user(&client, user_info).await?; + + Ok(HttpResponse::Ok().json(new_user)) + } +} + + +use actix_web::{App, HttpServer, web}; +use deadpool_postgres::{Pool, Manager}; +use handlers::add_user; +use tokio_postgres::{Config, NoTls}; + + +#[actix_rt::main] +async fn main() -> std::io::Result<()> { + const SERVER_ADDR: &str = "127.0.0.1:8080"; + + let pg_config = "postgres://test_user:testing@127.0.0.1:5432/testing_db" + .parse::() + .unwrap(); + + let pool = Pool::new( + Manager::new(pg_config, NoTls), + 16 // # of connections in pool + ); + + + let server = HttpServer::new(move || + App::new() + .data(pool.clone()) + .service(web::resource("/users").route(web::post().to(add_user))) + ) + .bind(SERVER_ADDR)? + .run(); + println!("Server running at http://{}/", SERVER_ADDR); + + server.await +}