Skip to content

Instantly share code, notes, and snippets.

@lanej
Created April 13, 2021 20:12
Show Gist options
  • Save lanej/fbcdfbdccac994b3250a806127ccdb4f to your computer and use it in GitHub Desktop.
Save lanej/fbcdfbdccac994b3250a806127ccdb4f to your computer and use it in GitHub Desktop.
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