diff --git a/Cargo.lock b/Cargo.lock index 9a09d09..f10c8b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1602,6 +1602,7 @@ name = "shopkeeper" version = "0.1.0" dependencies = [ "anyhow 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "barrel 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", "clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 6755350..0d9ee36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ serde = { version = "1.0", features = ["derive"] } uuid = { version = "0.8", features = ["serde", "v4"] } ipnetwork = "0.16" url = "2.1" +async-trait = "0.1" diff --git a/src/filters/mod.rs b/src/filters/mod.rs new file mode 100644 index 0000000..337474b --- /dev/null +++ b/src/filters/mod.rs @@ -0,0 +1,73 @@ +use serde::de::DeserializeOwned; +use std::convert::Infallible; +use warp::{Filter, Rejection, Reply}; + +use super::handlers; +use super::models::{ListParams, Owner, Shop}; +use super::Environment; + +pub fn get_shop(env: Environment) -> impl Filter + Clone { + warp::path!("shops" / i32) + .and(warp::get()) + .and(with_env(env)) + .and_then(handlers::get_shop) +} + +pub fn create_shop( + env: Environment, +) -> impl Filter + Clone { + warp::path!("shops") + .and(warp::post()) + .and(json_body::()) + .and(with_env(env)) + .and_then(handlers::create_shop) +} + +pub fn list_shops( + env: Environment, +) -> impl Filter + Clone { + warp::path!("shops") + .and(warp::get()) + .and(warp::query::()) + .and(with_env(env)) + .and_then(handlers::list_shops) +} + +pub fn get_owner(env: Environment) -> impl Filter + Clone { + warp::path!("owners" / i32) + .and(warp::get()) + .and(with_env(env)) + .and_then(handlers::get_owner) +} + +pub fn create_owner( + env: Environment, +) -> impl Filter + Clone { + warp::path!("owners") + .and(warp::post()) + .and(json_body::()) + .and(warp::addr::remote()) + .and(with_env(env)) + .and_then(handlers::create_owner) +} + +pub fn list_owners( + env: Environment, +) -> impl Filter + Clone { + warp::path!("owners") + .and(warp::get()) + .and(warp::query::()) + .and(with_env(env)) + .and_then(handlers::list_owners) +} + +fn with_env(env: Environment) -> impl Filter + Clone { + warp::any().map(move || env.clone()) +} + +fn json_body() -> impl Filter + Clone +where + T: Send + DeserializeOwned, +{ + warp::body::content_length_limit(1024 * 16).and(warp::body::json()) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..9173eee --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,76 @@ +use ipnetwork::IpNetwork; +use std::net::SocketAddr; +use warp::http::StatusCode; +use warp::reply::{json, with_header, with_status}; +use warp::{Rejection, Reply}; + +use super::models::{ListParams, Model, Owner, Shop}; +use super::problem::reject_anyhow; +use super::Environment; + +pub async fn get_shop(id: i32, env: Environment) -> Result { + let shop = Shop::get(&env.db, id).await.map_err(reject_anyhow)?; + let reply = json(&shop); + let reply = with_status(reply, StatusCode::OK); + Ok(reply) +} + +pub async fn list_shops( + list_params: ListParams, + env: Environment, +) -> Result { + let shops = Shop::list(&env.db, list_params) + .await + .map_err(reject_anyhow)?; + let reply = json(&shops); + let reply = with_status(reply, StatusCode::OK); + Ok(reply) +} + +pub async fn create_shop(shop: Shop, env: Environment) -> Result { + let saved_shop = shop.save(&env.db).await.map_err(reject_anyhow)?; + let url = saved_shop.url(&env.api_url).map_err(reject_anyhow)?; + let reply = json(&saved_shop); + let reply = with_header(reply, "Location", url.as_str()); + let reply = with_status(reply, StatusCode::CREATED); + Ok(reply) +} + +pub async fn get_owner(id: i32, env: Environment) -> Result { + let owner = Owner::get(&env.db, id).await.map_err(reject_anyhow)?; + let reply = json(&owner); + let reply = with_status(reply, StatusCode::OK); + Ok(reply) +} + +pub async fn list_owners( + list_params: ListParams, + env: Environment, +) -> Result { + let owners = Owner::list(&env.db, list_params) + .await + .map_err(reject_anyhow)?; + let reply = json(&owners); + let reply = with_status(reply, StatusCode::OK); + Ok(reply) +} + +pub async fn create_owner( + owner: Owner, + remote_addr: Option, + env: Environment, +) -> Result { + let owner_with_ip = match remote_addr { + Some(addr) => Owner { + ip_address: Some(IpNetwork::from(addr.ip())), + ..owner + }, + None => owner, + }; + let saved_owner = owner_with_ip.save(&env.db).await.map_err(reject_anyhow)?; + let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?; + let reply = json(&saved_owner); + let reply = with_header(reply, "Location", url.as_str()); + let reply = with_status(reply, StatusCode::CREATED); + Ok(reply) +} diff --git a/src/main.rs b/src/main.rs index 2fbe760..80048f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,10 @@ use url::Url; use warp::Filter; mod db; +mod filters; +mod handlers; +mod models; +mod problem; #[derive(Clap)] #[clap(version = "0.1.0", author = "Tyler Hallada ")] @@ -66,19 +70,19 @@ async fn main() -> Result<()> { let api_url = host_url.join("/api/v1")?; let env = Environment::new(api_url).await?; - info!("warp speed ahead!"); - let home = warp::path!("api" / "v1").map(|| "Shopkeeper home page"); let get_shop = filters::get_shop(env.clone()); let create_shop = filters::create_shop(env.clone()); let list_shops = filters::list_shops(env.clone()); let get_owner = filters::get_owner(env.clone()); let create_owner = filters::create_owner(env.clone()); + let list_owners = filters::list_owners(env.clone()); let routes = create_shop .or(get_shop) .or(list_shops) .or(create_owner) .or(get_owner) + .or(list_owners) .or(home) .recover(problem::unpack_problem) .with(warp::compression::gzip()) @@ -101,390 +105,3 @@ async fn main() -> Result<()> { server.serve(make_svc).await?; Ok(()) } - -mod filters { - use serde::de::DeserializeOwned; - use std::convert::Infallible; - use warp::{Filter, Rejection, Reply}; - - use super::handlers; - use super::models::{ListParams, Owner, Shop}; - use super::Environment; - - pub fn get_shop( - env: Environment, - ) -> impl Filter + Clone { - warp::path!("shops" / i32) - .and(warp::get()) - .and(with_env(env)) - .and_then(handlers::get_shop) - } - - pub fn create_shop( - env: Environment, - ) -> impl Filter + Clone { - warp::path!("shops") - .and(warp::post()) - .and(json_body::()) - .and(with_env(env)) - .and_then(handlers::create_shop) - } - - pub fn list_shops( - env: Environment, - ) -> impl Filter + Clone { - warp::path!("shops") - .and(warp::get()) - .and(warp::query::()) - .and(with_env(env)) - .and_then(handlers::list_shops) - } - - pub fn get_owner( - env: Environment, - ) -> impl Filter + Clone { - warp::path!("owners" / i32) - .and(warp::get()) - .and(with_env(env)) - .and_then(handlers::get_owner) - } - - pub fn create_owner( - env: Environment, - ) -> impl Filter + Clone { - warp::path!("owners") - .and(warp::post()) - .and(json_body::()) - .and(warp::addr::remote()) - .and(with_env(env)) - .and_then(handlers::create_owner) - } - - fn with_env( - env: Environment, - ) -> impl Filter + Clone { - warp::any().map(move || env.clone()) - } - - fn json_body() -> impl Filter + Clone - where - T: Send + DeserializeOwned, - { - warp::body::content_length_limit(1024 * 16).and(warp::body::json()) - } -} - -mod handlers { - use http_api_problem::HttpApiProblem; - use ipnetwork::IpNetwork; - use std::net::SocketAddr; - use warp::http::StatusCode; - use warp::reply::{json, with_header, with_status}; - use warp::{Rejection, Reply}; - - use super::models::{ListParams, Owner, Shop}; - use super::problem::reject_anyhow; - use super::Environment; - - pub async fn get_shop(id: i32, env: Environment) -> Result { - let shop = Shop::get(&env.db, id).await.map_err(reject_anyhow)?; - let reply = json(&shop); - let reply = with_status(reply, StatusCode::OK); - Ok(reply) - } - - pub async fn list_shops( - list_params: ListParams, - env: Environment, - ) -> Result { - let shops = Shop::list(&env.db, list_params) - .await - .map_err(reject_anyhow)?; - if shops.is_empty() { - return Err(warp::reject::custom( - HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND), - )); - } - let reply = json(&shops); - let reply = with_status(reply, StatusCode::OK); - Ok(reply) - } - - pub async fn create_shop(shop: Shop, env: Environment) -> Result { - let saved_shop = shop.save(&env.db).await.map_err(reject_anyhow)?; - let url = saved_shop.url(&env.api_url).map_err(reject_anyhow)?; - let reply = json(&saved_shop); - let reply = with_header(reply, "Location", url.as_str()); - let reply = with_status(reply, StatusCode::CREATED); - Ok(reply) - } - - pub async fn get_owner(id: i32, env: Environment) -> Result { - let owner = Owner::get(&env.db, id).await.map_err(reject_anyhow)?; - let reply = json(&owner); - let reply = with_status(reply, StatusCode::OK); - Ok(reply) - } - - pub async fn create_owner( - owner: Owner, - remote_addr: Option, - env: Environment, - ) -> Result { - let owner_with_ip = match remote_addr { - Some(addr) => Owner { - ip_address: Some(IpNetwork::from(addr.ip())), - ..owner - }, - None => owner, - }; - let saved_owner = owner_with_ip.save(&env.db).await.map_err(reject_anyhow)?; - let url = saved_owner.url(&env.api_url).map_err(reject_anyhow)?; - let reply = json(&saved_owner); - let reply = with_header(reply, "Location", url.as_str()); - let reply = with_status(reply, StatusCode::CREATED); - Ok(reply) - } -} - -mod models { - use anyhow::{anyhow, Result}; - use chrono::prelude::*; - use ipnetwork::IpNetwork; - use serde::{Deserialize, Serialize}; - use sqlx::postgres::PgPool; - use std::fmt; - use url::Url; - use uuid::Uuid; - - #[derive(Debug, Deserialize)] - pub enum Order { - Asc, - Desc, - } - - impl fmt::Display for Order { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Order::Asc => "ASC", - Order::Desc => "DESC", - } - ) - } - } - - #[derive(Debug, Deserialize)] - pub struct ListParams { - limit: Option, - offset: Option, - order_by: Option, - order: Option, - } - - impl ListParams { - pub fn get_order_by(&self) -> String { - let default_order_by = "updated_at".to_string(); - let order_by = self.order_by.as_ref().unwrap_or(&default_order_by); - let order = self.order.as_ref().unwrap_or(&Order::Desc); - format!("{} {}", order_by, order) - } - } - - #[derive(Debug, Serialize, Deserialize, Clone)] - pub struct Shop { - pub id: Option, - pub name: String, - pub owner_id: i32, - pub description: String, - pub is_not_sell_buy: bool, - pub sell_buy_list_id: i32, - pub vendor_id: i32, - pub vendor_gold: i32, - pub created_at: Option, - pub updated_at: Option, - } - - impl Shop { - pub fn url(&self, api_url: &Url) -> Result { - if let Some(id) = self.id { - Ok(api_url.join(&format!("/shops/{}", id))?) - } else { - Err(anyhow!("Cannot get URL for shop with no id")) - } - } - - pub async fn get(db: &PgPool, id: i32) -> Result { - let timer = std::time::Instant::now(); - let result = sqlx::query_as!(Self, "SELECT * FROM shops WHERE id = $1", id) - .fetch_one(db) - .await?; - let elapsed = timer.elapsed(); - debug!("SELECT * FROM shops ... {:.3?}", elapsed); - Ok(result) - } - - pub async fn save(self, db: &PgPool) -> Result { - let timer = std::time::Instant::now(); - let result = sqlx::query_as!( - Self, - "INSERT INTO shops - (name, owner_id, description, is_not_sell_buy, sell_buy_list_id, vendor_id, - vendor_gold, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now()) - RETURNING *", - self.name, - self.owner_id, - self.description, - self.is_not_sell_buy, - self.sell_buy_list_id, - self.vendor_id, - self.vendor_gold, - ) - .fetch_one(db) - .await?; - let elapsed = timer.elapsed(); - debug!("INSERT INTO shops ... {:.3?}", elapsed); - Ok(result) - } - - pub async fn list(db: &PgPool, list_params: ListParams) -> Result> { - let timer = std::time::Instant::now(); - let result = sqlx::query_as!( - Self, - "SELECT * FROM shops - ORDER BY $1 - LIMIT $2 - OFFSET $3", - list_params.get_order_by(), - list_params.limit.unwrap_or(10), - list_params.offset.unwrap_or(0), - ) - .fetch_all(db) - .await?; - let elapsed = timer.elapsed(); - debug!("SELECT * FROM shops ... {:.3?}", elapsed); - Ok(result) - } - } - - #[derive(Debug, Serialize, Deserialize, Clone)] - pub struct Owner { - pub id: Option, - pub name: String, - pub api_key: Uuid, - pub ip_address: Option, - pub mod_version: String, - pub created_at: Option, - pub updated_at: Option, - } - - impl Owner { - pub fn url(&self, api_url: &Url) -> Result { - if let Some(id) = self.id { - Ok(api_url.join(&format!("/owners/{}", id))?) - } else { - Err(anyhow!("Cannot get URL for owner with no id")) - } - } - - pub async fn get(db: &PgPool, id: i32) -> Result { - let timer = std::time::Instant::now(); - let result = sqlx::query_as!(Self, "SELECT * FROM owners WHERE id = $1", id) - .fetch_one(db) - .await?; - let elapsed = timer.elapsed(); - debug!("SELECT * FROM owners ... {:.3?}", elapsed); - Ok(result) - } - - pub async fn save(self, db: &PgPool) -> Result { - let timer = std::time::Instant::now(); - let result = sqlx::query_as!( - Self, - "INSERT INTO owners - (name, api_key, ip_address, mod_version, created_at, updated_at) - VALUES ($1, $2, $3, $4, now(), now()) - RETURNING *", - self.name, - self.api_key, - self.ip_address, - self.mod_version, - ) - .fetch_one(db) - .await?; - let elapsed = timer.elapsed(); - debug!("INSERT INTO owners ... {:.3?}", elapsed); - Ok(result) - } - } -} - -mod problem { - use http_api_problem::HttpApiProblem; - use warp::http::StatusCode; - use warp::{reject, Rejection, Reply}; - - pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem { - let error = match error.downcast::() { - Ok(problem) => return problem, - Err(error) => error, - }; - - if let Some(sqlx_error) = error.downcast_ref::() { - match sqlx_error { - sqlx::error::Error::RowNotFound => { - return HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND) - } - sqlx::error::Error::Database(db_error) => { - error!( - "Database error: {}. {}", - db_error.message(), - db_error.details().unwrap_or("") - ); - if let Some(code) = db_error.code() { - if let Some(constraint) = db_error.constraint_name() { - if code == "23503" && constraint == "shops_owner_id_fkey" { - // foreign_key_violation - return HttpApiProblem::with_title_and_type_from_status( - StatusCode::BAD_REQUEST, - ) - .set_detail("Owner does not exist"); - } - } - } - } - _ => {} - } - } - - error!("Recovering unhandled error: {:?}", error); - // TODO: this leaks internal info, should not stringify error - HttpApiProblem::new(format!("Internal Server Error: {:?}", error)) - .set_status(StatusCode::INTERNAL_SERVER_ERROR) - } - - pub async fn unpack_problem(rejection: Rejection) -> Result { - if let Some(problem) = rejection.find::() { - let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); - - let reply = warp::reply::json(problem); - let reply = warp::reply::with_status(reply, code); - let reply = warp::reply::with_header( - reply, - warp::http::header::CONTENT_TYPE, - http_api_problem::PROBLEM_JSON_MEDIA_TYPE, - ); - - return Ok(reply); - } - - Err(rejection) - } - - pub fn reject_anyhow(error: anyhow::Error) -> Rejection { - reject::custom(from_anyhow(error)) - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..3dbe65b --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; +use std::fmt; + +pub mod model; +pub mod owner; +pub mod shop; + +pub use model::Model; +pub use owner::Owner; +pub use shop::Shop; + +#[derive(Debug, Deserialize)] +pub enum Order { + Asc, + Desc, +} + +impl fmt::Display for Order { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Order::Asc => "ASC", + Order::Desc => "DESC", + } + ) + } +} + +#[derive(Debug, Deserialize)] +pub struct ListParams { + limit: Option, + offset: Option, + order_by: Option, + order: Option, +} + +impl ListParams { + pub fn get_order_by(&self) -> String { + let default_order_by = "updated_at".to_string(); + let order_by = self.order_by.as_ref().unwrap_or(&default_order_by); + let order = self.order.as_ref().unwrap_or(&Order::Desc); + format!("{} {}", order_by, order) + } +} diff --git a/src/models/model.rs b/src/models/model.rs new file mode 100644 index 0000000..466e7e6 --- /dev/null +++ b/src/models/model.rs @@ -0,0 +1,28 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use sqlx::postgres::PgPool; +use url::Url; + +use super::ListParams; + +#[async_trait] +pub trait Model +where + Self: std::marker::Sized, +{ + fn resource_name() -> &'static str; + fn pk(&self) -> Option; + fn url(&self, api_url: &Url) -> Result { + if let Some(pk) = self.pk() { + Ok(api_url.join(&format!("/{}s/{}", Self::resource_name(), pk))?) + } else { + Err(anyhow!( + "Cannot get URL for {} with no primary key", + Self::resource_name() + )) + } + } + async fn get(db: &PgPool, id: i32) -> Result; + async fn save(self, db: &PgPool) -> Result; + async fn list(db: &PgPool, list_params: ListParams) -> Result>; +} diff --git a/src/models/owner.rs b/src/models/owner.rs new file mode 100644 index 0000000..0825192 --- /dev/null +++ b/src/models/owner.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use async_trait::async_trait; +use chrono::prelude::*; +use ipnetwork::IpNetwork; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgPool; +use uuid::Uuid; + +use super::ListParams; +use super::Model; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Owner { + pub id: Option, + pub name: String, + pub api_key: Uuid, + pub ip_address: Option, + pub mod_version: String, + pub created_at: Option, + pub updated_at: Option, +} + +#[async_trait] +impl Model for Owner { + fn resource_name() -> &'static str { + "owner" + } + + fn pk(&self) -> Option { + self.id + } + + async fn get(db: &PgPool, id: i32) -> Result { + let timer = std::time::Instant::now(); + let result = sqlx::query_as!(Self, "SELECT * FROM owners WHERE id = $1", id) + .fetch_one(db) + .await?; + let elapsed = timer.elapsed(); + debug!("SELECT * FROM owners ... {:.3?}", elapsed); + Ok(result) + } + + async fn save(self, db: &PgPool) -> Result { + let timer = std::time::Instant::now(); + let result = sqlx::query_as!( + Self, + "INSERT INTO owners + (name, api_key, ip_address, mod_version, created_at, updated_at) + VALUES ($1, $2, $3, $4, now(), now()) + RETURNING *", + self.name, + self.api_key, + self.ip_address, + self.mod_version, + ) + .fetch_one(db) + .await?; + let elapsed = timer.elapsed(); + debug!("INSERT INTO owners ... {:.3?}", elapsed); + Ok(result) + } + + async fn list(db: &PgPool, list_params: ListParams) -> Result> { + let timer = std::time::Instant::now(); + let result = sqlx::query_as!( + Self, + "SELECT * FROM owners + ORDER BY $1 + LIMIT $2 + OFFSET $3", + list_params.get_order_by(), + list_params.limit.unwrap_or(10), + list_params.offset.unwrap_or(0), + ) + .fetch_all(db) + .await?; + let elapsed = timer.elapsed(); + debug!("SELECT * FROM owners ... {:.3?}", elapsed); + Ok(result) + } +} diff --git a/src/models/shop.rs b/src/models/shop.rs new file mode 100644 index 0000000..ca3c0a3 --- /dev/null +++ b/src/models/shop.rs @@ -0,0 +1,86 @@ +use anyhow::Result; +use async_trait::async_trait; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgPool; + +use super::ListParams; +use super::Model; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Shop { + pub id: Option, + pub name: String, + pub owner_id: i32, + pub description: String, + pub is_not_sell_buy: bool, + pub sell_buy_list_id: i32, + pub vendor_id: i32, + pub vendor_gold: i32, + pub created_at: Option, + pub updated_at: Option, +} + +#[async_trait] +impl Model for Shop { + fn resource_name() -> &'static str { + "shop" + } + + fn pk(&self) -> Option { + self.id + } + + async fn get(db: &PgPool, id: i32) -> Result { + let timer = std::time::Instant::now(); + let result = sqlx::query_as!(Self, "SELECT * FROM shops WHERE id = $1", id) + .fetch_one(db) + .await?; + let elapsed = timer.elapsed(); + debug!("SELECT * FROM shops ... {:.3?}", elapsed); + Ok(result) + } + + async fn save(self, db: &PgPool) -> Result { + let timer = std::time::Instant::now(); + let result = sqlx::query_as!( + Self, + "INSERT INTO shops + (name, owner_id, description, is_not_sell_buy, sell_buy_list_id, vendor_id, + vendor_gold, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now()) + RETURNING *", + self.name, + self.owner_id, + self.description, + self.is_not_sell_buy, + self.sell_buy_list_id, + self.vendor_id, + self.vendor_gold, + ) + .fetch_one(db) + .await?; + let elapsed = timer.elapsed(); + debug!("INSERT INTO shops ... {:.3?}", elapsed); + Ok(result) + } + + async fn list(db: &PgPool, list_params: ListParams) -> Result> { + let timer = std::time::Instant::now(); + let result = sqlx::query_as!( + Self, + "SELECT * FROM shops + ORDER BY $1 + LIMIT $2 + OFFSET $3", + list_params.get_order_by(), + list_params.limit.unwrap_or(10), + list_params.offset.unwrap_or(0), + ) + .fetch_all(db) + .await?; + let elapsed = timer.elapsed(); + debug!("SELECT * FROM shops ... {:.3?}", elapsed); + Ok(result) + } +} diff --git a/src/problem/mod.rs b/src/problem/mod.rs new file mode 100644 index 0000000..7eebe7e --- /dev/null +++ b/src/problem/mod.rs @@ -0,0 +1,64 @@ +use http_api_problem::HttpApiProblem; +use warp::http::StatusCode; +use warp::{reject, Rejection, Reply}; + +pub fn from_anyhow(error: anyhow::Error) -> HttpApiProblem { + let error = match error.downcast::() { + Ok(problem) => return problem, + Err(error) => error, + }; + + if let Some(sqlx_error) = error.downcast_ref::() { + match sqlx_error { + sqlx::error::Error::RowNotFound => { + return HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND) + } + sqlx::error::Error::Database(db_error) => { + error!( + "Database error: {}. {}", + db_error.message(), + db_error.details().unwrap_or("") + ); + if let Some(code) = db_error.code() { + if let Some(constraint) = db_error.constraint_name() { + if code == "23503" && constraint == "shops_owner_id_fkey" { + // foreign_key_violation + return HttpApiProblem::with_title_and_type_from_status( + StatusCode::BAD_REQUEST, + ) + .set_detail("Owner does not exist"); + } + } + } + } + _ => {} + } + } + + error!("Recovering unhandled error: {:?}", error); + // TODO: this leaks internal info, should not stringify error + HttpApiProblem::new(format!("Internal Server Error: {:?}", error)) + .set_status(StatusCode::INTERNAL_SERVER_ERROR) +} + +pub async fn unpack_problem(rejection: Rejection) -> Result { + if let Some(problem) = rejection.find::() { + let code = problem.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + let reply = warp::reply::json(problem); + let reply = warp::reply::with_status(reply, code); + let reply = warp::reply::with_header( + reply, + warp::http::header::CONTENT_TYPE, + http_api_problem::PROBLEM_JSON_MEDIA_TYPE, + ); + + return Ok(reply); + } + + Err(rejection) +} + +pub fn reject_anyhow(error: anyhow::Error) -> Rejection { + reject::custom(from_anyhow(error)) +}