diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..13f23a0 --- /dev/null +++ b/db.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS todo +( + id SERIAL PRIMARY KEY NOT NULL, + name VARCHAR(255), + created_at timestamp with time zone DEFAULT (now() at time zone 'utc'), + checked boolean DEFAULT false +); diff --git a/src/db.rs b/src/db.rs index e409b4d..186e265 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,21 +1,45 @@ use crate::{DBCon, DBPool}; +use mobc::{Pool}; use mobc_postgres::{tokio_postgres, PgConnectionManager}; use tokio_postgres::{Config, Error, NoTls}; use std::fs; use std::str::FromStr; use std::time::Duration; +use crate::error::Error::{*}; +use crate::error; + +type Result = std::result::Result; const DB_POOL_MAX_OPEN: u64 = 32; const DB_POOL_MAX_IDLE: u64 = 8; const DB_POOL_TIMEOUT_SECONDS: u64 = 15; +const INIT_SQL: &str = "./db.sql"; + +pub async fn init_db(db_pool: &DBPool) -> Result<()> { + let init_file = fs::read_to_string(INIT_SQL)?; + let con = get_db_con(db_pool).await?; + con.batch_execute(init_file.as_str()) + .await + .map_err(DBInitError)?; + Ok(()) +} + +pub async fn get_db_con(db_pool: &DBPool) -> Result { + db_pool.get().await.map_err(DBPoolError) +} pub fn create_pool() -> std::result::Result> { let config = Config::from_str("postgres://postgres@127.0.0.1:7878/postgres")?; let manager = PgConnectionManager::new(config, NoTls); Ok(Pool::builder() - .max_open(DB_POOL_MAX_OPEN) - .max_idle(DB_POOL_MAX_IDLE) - .get_timeout(Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS))) - .build(manager)) + .max_open(DB_POOL_MAX_OPEN) + .max_idle(DB_POOL_MAX_IDLE) + .get_timeout(Some(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS))) + .build(manager)) } + + + + + diff --git a/src/db.sql b/src/db.sql new file mode 100644 index 0000000..13f23a0 --- /dev/null +++ b/src/db.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS todo +( + id SERIAL PRIMARY KEY NOT NULL, + name VARCHAR(255), + created_at timestamp with time zone DEFAULT (now() at time zone 'utc'), + checked boolean DEFAULT false +); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..438fec9 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,64 @@ +use mobc_postgres::tokio_postgres; +use serde::{Serialize, Deserialize}; +use thiserror::Error; +use warp::{http::StatusCode, Filter,Rejection,Reply}; +use crate::Infallible; + +#[derive(Error, Debug)] +pub enum Error { + #[error("error getting connection from DB pool: {0}")] + DBPoolError(mobc::Error), + #[error("error executing DB query: {0}")] + DBQueryError(#[from] tokio_postgres::Error), + #[error("error creating table: {0}")] + DBInitError(tokio_postgres::Error), + #[error("error reading file: {0}")] + ReadFileError(#[from] std::io::Error), +} + +impl warp::reject::Reject for Error {} + +#[derive(Serialize)] +struct ErrorResponse { + message: String, +} + +pub async fn handle_rejection(err: Rejection) -> std::result::Result { + let code; + let message; + + if err.is_not_found() { + code = StatusCode::NOT_FOUND; + message = "Not Found"; + } else if let Some(_) = err.find::() { + code = StatusCode::BAD_REQUEST; + message = "Invalid Body"; + } else if let Some(e) = err.find::() { + match e { + Error::DBQueryError(_) => { + code = StatusCode::BAD_REQUEST; + message = "Could not Execute request"; + } + _ => { + eprintln!("unhandled application error: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "Internal Server Error"; + } + } + } else if let Some(_) = err.find::() { + code = StatusCode::METHOD_NOT_ALLOWED; + message = "Method Not Allowed"; + } else { + eprintln!("unhandled error: {:?}", err); + code = StatusCode::INTERNAL_SERVER_ERROR; + message = "Internal Server Error"; + } + + let json = warp::reply::json(&ErrorResponse { + message: message.into(), + }); + + Ok(warp::reply::with_status(json, code)) +} + + diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..b90550c --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,14 @@ +use crate::{db, DBPool}; +use crate::error::Error::{*}; +use warp::{http::StatusCode, reject, Reply, Rejection}; + +pub async fn health_handler(db_pool: DBPool) -> std::result::Result { + let db = db::get_db_con(&db_pool) + .await + .map_err(|e| reject::custom(e))?; + + db.execute("SELECT 1", &[]) + .await + .map_err(|e| reject::custom(DBQueryError(e)))?; + Ok(StatusCode::OK) +} diff --git a/src/main.rs b/src/main.rs index 9f29ba5..fb64c54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,34 @@ // mod data; -// mod db; -// mod error; -// mod handler; +mod db; +mod error; +mod handler; -use warp::{http::StatusCode, Filter}; +use warp::{http::StatusCode, Filter,Rejection}; use mobc::{Connection, Pool}; use mobc_postgres::{tokio_postgres, PgConnectionManager}; use tokio_postgres::NoTls; +use std::convert::Infallible; type DBCon = Connection>; type DBPool = Pool>; #[tokio::main] async fn main() { + let db_pool = db::create_pool().expect("database pool can be created"); + + db::init_db(&db_pool) + .await + .expect("database can be initialized"); + let health_route = warp::path!("health") - .map(|| StatusCode::OK); + .and(with_db(db_pool.clone())) + .and_then(handler::health_handler); let routes = health_route - .with(warp::cors().allow_any_origin()); + .with(warp::cors().allow_any_origin()) + .recover(error::handle_rejection); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; } + +fn with_db(db_pool: DBPool) -> impl Filter + Clone { + warp::any().map(move || db_pool.clone()) +}