Created
April 13, 2021 20:12
-
-
Save lanej/fbcdfbdccac994b3250a806127ccdb4f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use dotenv; | |
use serde::{Deserialize, Serialize}; | |
use sqlx::{Executor, SqlitePool}; | |
use warp::{http::StatusCode, reject, Filter, Rejection, Reply}; | |
#[tokio::main] | |
async fn main() { | |
env_logger::init(); | |
dotenv::dotenv().unwrap(); | |
let log = warp::log("blog::app"); | |
let pool = setup_db().await.unwrap(); | |
let routes = warp::path("blogs") | |
.and(warp::post()) | |
.and(warp::body::json()) | |
.and(with_db(pool)) | |
.and_then(create_blog) | |
.with(log); | |
warp::serve(routes) | |
.run( | |
option_env!("PORT") | |
.unwrap_or("3030") | |
.parse::<std::net::SocketAddr>() | |
.expect("Invalid PORT"), | |
) | |
.await; | |
} | |
pub struct Blog<'a> { | |
name: &'a str, | |
id: i64, | |
} | |
struct BlogsRepository<'a, T: sqlx::Database + sqlx::Executor<'a>> { | |
pool: sqlx::Pool<T>, | |
} | |
impl std::convert::From<sqlx::Error> for DatabaseFailure { | |
fn from(e: sqlx::Error) -> Self { | |
DatabaseFailure { e } | |
} | |
} | |
impl<T: sqlx::Database> BlogsRepository<T> { | |
async fn create<'a>(&self, name: &'a str) -> Result<Blog<'a>, sqlx::Error> { | |
let id = self | |
.pool | |
.try_acquire() | |
.unwrap() | |
.execute(sqlx::query("INSERT INTO blogs (name) VALUES (?)").bind(&name)) | |
.await? | |
.last_insert_rowid(); | |
Ok(Blog { name, id }) | |
} | |
} | |
#[derive(Debug)] | |
struct DatabaseFailure { | |
e: sqlx::Error, | |
} | |
impl warp::reject::Reject for DatabaseFailure {} | |
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace | |
pub async fn health_handler(pool: SqlitePool) -> std::result::Result<impl Reply, Rejection> { | |
let mut db = pool.try_acquire().ok_or_else(|| reject::not_found())?; | |
db.execute("SELECT 1") | |
.await | |
.map_err(|e| reject::custom(DatabaseFailure { e }))?; | |
Ok(StatusCode::OK) | |
} | |
fn with_db( | |
pool: SqlitePool, | |
) -> impl Filter<Extract = (SqlitePool,), Error = std::convert::Infallible> + Clone { | |
warp::any().map(move || pool.clone()) | |
} | |
pub async fn create_blog( | |
request: BlogRequest, | |
pool: SqlitePool, | |
) -> std::result::Result<impl Reply, Rejection> { | |
let blogs = BlogsRepository { pool }; | |
let blog = blogs.create(&request.name).await?; | |
Ok(warp::reply::json(&BlogResponse::of(blog))) | |
} | |
#[derive(Debug, Deserialize)] | |
pub struct BlogRequest { | |
pub name: String, | |
} | |
#[derive(Debug, Deserialize, Serialize, PartialEq)] | |
pub struct BlogResponse<'a> { | |
pub id: i64, | |
pub name: &'a str, | |
} | |
impl BlogResponse<'_> { | |
pub fn of(blog: Blog) -> BlogResponse { | |
BlogResponse { | |
id: blog.id, | |
name: blog.name, | |
} | |
} | |
} | |
async fn setup_db() -> anyhow::Result<sqlx::SqlitePool> { | |
let pool = SqlitePool::connect("sqlite::memory:").await?; | |
pool.execute(sqlx::query(&std::fs::read_to_string("db/structure.sql")?)) | |
.await?; | |
Ok(pool) | |
} | |
#[tokio::test(flavor = "multi_thread")] | |
async fn test_create_blog() { | |
let pool = setup_db().await.unwrap(); | |
let mut response = create_blog( | |
BlogRequest { | |
name: "foo".to_string(), | |
}, | |
pool, | |
) | |
.await | |
.unwrap() | |
.into_response(); | |
let content = &hyper::body::to_bytes(response.body_mut()) | |
.await | |
.unwrap() | |
.slice(..); | |
let body: BlogResponse = serde_json::from_str(std::str::from_utf8(content).unwrap()).unwrap(); | |
assert_eq!( | |
(StatusCode::OK, BlogResponse { id: 1, name: "foo" }), | |
(response.status(), body), | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment