Modularize, Model trait, list_owners
This commit is contained in:
parent
6b1f31f246
commit
9ec7fc1518
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1602,6 +1602,7 @@ name = "shopkeeper"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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)",
|
"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)",
|
"clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -25,3 +25,4 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||||
ipnetwork = "0.16"
|
ipnetwork = "0.16"
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
73
src/filters/mod.rs
Normal file
73
src/filters/mod.rs
Normal file
@ -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<Extract = impl Reply, Error = Rejection> + 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<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
warp::path!("shops")
|
||||||
|
.and(warp::post())
|
||||||
|
.and(json_body::<Shop>())
|
||||||
|
.and(with_env(env))
|
||||||
|
.and_then(handlers::create_shop)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_shops(
|
||||||
|
env: Environment,
|
||||||
|
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
warp::path!("shops")
|
||||||
|
.and(warp::get())
|
||||||
|
.and(warp::query::<ListParams>())
|
||||||
|
.and(with_env(env))
|
||||||
|
.and_then(handlers::list_shops)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_owner(env: Environment) -> impl Filter<Extract = impl Reply, Error = Rejection> + 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<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
warp::path!("owners")
|
||||||
|
.and(warp::post())
|
||||||
|
.and(json_body::<Owner>())
|
||||||
|
.and(warp::addr::remote())
|
||||||
|
.and(with_env(env))
|
||||||
|
.and_then(handlers::create_owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_owners(
|
||||||
|
env: Environment,
|
||||||
|
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
||||||
|
warp::path!("owners")
|
||||||
|
.and(warp::get())
|
||||||
|
.and(warp::query::<ListParams>())
|
||||||
|
.and(with_env(env))
|
||||||
|
.and_then(handlers::list_owners)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_env(env: Environment) -> impl Filter<Extract = (Environment,), Error = Infallible> + Clone {
|
||||||
|
warp::any().map(move || env.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + Clone
|
||||||
|
where
|
||||||
|
T: Send + DeserializeOwned,
|
||||||
|
{
|
||||||
|
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
|
||||||
|
}
|
76
src/handlers/mod.rs
Normal file
76
src/handlers/mod.rs
Normal file
@ -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<impl Reply, Rejection> {
|
||||||
|
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<impl Reply, Rejection> {
|
||||||
|
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<impl Reply, Rejection> {
|
||||||
|
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<impl Reply, Rejection> {
|
||||||
|
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<impl Reply, Rejection> {
|
||||||
|
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<SocketAddr>,
|
||||||
|
env: Environment,
|
||||||
|
) -> Result<impl Reply, Rejection> {
|
||||||
|
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)
|
||||||
|
}
|
395
src/main.rs
395
src/main.rs
@ -14,6 +14,10 @@ use url::Url;
|
|||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
|
mod filters;
|
||||||
|
mod handlers;
|
||||||
|
mod models;
|
||||||
|
mod problem;
|
||||||
|
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
#[clap(version = "0.1.0", author = "Tyler Hallada <tyler@hallada.net>")]
|
#[clap(version = "0.1.0", author = "Tyler Hallada <tyler@hallada.net>")]
|
||||||
@ -66,19 +70,19 @@ async fn main() -> Result<()> {
|
|||||||
let api_url = host_url.join("/api/v1")?;
|
let api_url = host_url.join("/api/v1")?;
|
||||||
let env = Environment::new(api_url).await?;
|
let env = Environment::new(api_url).await?;
|
||||||
|
|
||||||
info!("warp speed ahead!");
|
|
||||||
|
|
||||||
let home = warp::path!("api" / "v1").map(|| "Shopkeeper home page");
|
let home = warp::path!("api" / "v1").map(|| "Shopkeeper home page");
|
||||||
let get_shop = filters::get_shop(env.clone());
|
let get_shop = filters::get_shop(env.clone());
|
||||||
let create_shop = filters::create_shop(env.clone());
|
let create_shop = filters::create_shop(env.clone());
|
||||||
let list_shops = filters::list_shops(env.clone());
|
let list_shops = filters::list_shops(env.clone());
|
||||||
let get_owner = filters::get_owner(env.clone());
|
let get_owner = filters::get_owner(env.clone());
|
||||||
let create_owner = filters::create_owner(env.clone());
|
let create_owner = filters::create_owner(env.clone());
|
||||||
|
let list_owners = filters::list_owners(env.clone());
|
||||||
let routes = create_shop
|
let routes = create_shop
|
||||||
.or(get_shop)
|
.or(get_shop)
|
||||||
.or(list_shops)
|
.or(list_shops)
|
||||||
.or(create_owner)
|
.or(create_owner)
|
||||||
.or(get_owner)
|
.or(get_owner)
|
||||||
|
.or(list_owners)
|
||||||
.or(home)
|
.or(home)
|
||||||
.recover(problem::unpack_problem)
|
.recover(problem::unpack_problem)
|
||||||
.with(warp::compression::gzip())
|
.with(warp::compression::gzip())
|
||||||
@ -101,390 +105,3 @@ async fn main() -> Result<()> {
|
|||||||
server.serve(make_svc).await?;
|
server.serve(make_svc).await?;
|
||||||
Ok(())
|
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<Extract = impl Reply, Error = Rejection> + 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<Extract = impl Reply, Error = Rejection> + Clone {
|
|
||||||
warp::path!("shops")
|
|
||||||
.and(warp::post())
|
|
||||||
.and(json_body::<Shop>())
|
|
||||||
.and(with_env(env))
|
|
||||||
.and_then(handlers::create_shop)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_shops(
|
|
||||||
env: Environment,
|
|
||||||
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
|
|
||||||
warp::path!("shops")
|
|
||||||
.and(warp::get())
|
|
||||||
.and(warp::query::<ListParams>())
|
|
||||||
.and(with_env(env))
|
|
||||||
.and_then(handlers::list_shops)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_owner(
|
|
||||||
env: Environment,
|
|
||||||
) -> impl Filter<Extract = impl Reply, Error = Rejection> + 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<Extract = impl Reply, Error = Rejection> + Clone {
|
|
||||||
warp::path!("owners")
|
|
||||||
.and(warp::post())
|
|
||||||
.and(json_body::<Owner>())
|
|
||||||
.and(warp::addr::remote())
|
|
||||||
.and(with_env(env))
|
|
||||||
.and_then(handlers::create_owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_env(
|
|
||||||
env: Environment,
|
|
||||||
) -> impl Filter<Extract = (Environment,), Error = Infallible> + Clone {
|
|
||||||
warp::any().map(move || env.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_body<T>() -> impl Filter<Extract = (T,), Error = warp::Rejection> + 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<impl Reply, Rejection> {
|
|
||||||
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<impl Reply, Rejection> {
|
|
||||||
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<impl Reply, Rejection> {
|
|
||||||
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<impl Reply, Rejection> {
|
|
||||||
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<SocketAddr>,
|
|
||||||
env: Environment,
|
|
||||||
) -> Result<impl Reply, Rejection> {
|
|
||||||
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<i64>,
|
|
||||||
offset: Option<i64>,
|
|
||||||
order_by: Option<String>,
|
|
||||||
order: Option<Order>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<i32>,
|
|
||||||
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<NaiveDateTime>,
|
|
||||||
pub updated_at: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Shop {
|
|
||||||
pub fn url(&self, api_url: &Url) -> Result<Url> {
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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<Vec<Self>> {
|
|
||||||
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<i32>,
|
|
||||||
pub name: String,
|
|
||||||
pub api_key: Uuid,
|
|
||||||
pub ip_address: Option<IpNetwork>,
|
|
||||||
pub mod_version: String,
|
|
||||||
pub created_at: Option<NaiveDateTime>,
|
|
||||||
pub updated_at: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Owner {
|
|
||||||
pub fn url(&self, api_url: &Url) -> Result<Url> {
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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::<HttpApiProblem>() {
|
|
||||||
Ok(problem) => return problem,
|
|
||||||
Err(error) => error,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(sqlx_error) = error.downcast_ref::<sqlx::error::Error>() {
|
|
||||||
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<impl Reply, Rejection> {
|
|
||||||
if let Some(problem) = rejection.find::<HttpApiProblem>() {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
46
src/models/mod.rs
Normal file
46
src/models/mod.rs
Normal file
@ -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<i64>,
|
||||||
|
offset: Option<i64>,
|
||||||
|
order_by: Option<String>,
|
||||||
|
order: Option<Order>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
28
src/models/model.rs
Normal file
28
src/models/model.rs
Normal file
@ -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<i32>;
|
||||||
|
fn url(&self, api_url: &Url) -> Result<Url> {
|
||||||
|
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<Self>;
|
||||||
|
async fn save(self, db: &PgPool) -> Result<Self>;
|
||||||
|
async fn list(db: &PgPool, list_params: ListParams) -> Result<Vec<Self>>;
|
||||||
|
}
|
81
src/models/owner.rs
Normal file
81
src/models/owner.rs
Normal file
@ -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<i32>,
|
||||||
|
pub name: String,
|
||||||
|
pub api_key: Uuid,
|
||||||
|
pub ip_address: Option<IpNetwork>,
|
||||||
|
pub mod_version: String,
|
||||||
|
pub created_at: Option<NaiveDateTime>,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Model for Owner {
|
||||||
|
fn resource_name() -> &'static str {
|
||||||
|
"owner"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pk(&self) -> Option<i32> {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(db: &PgPool, id: i32) -> Result<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<Vec<Self>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
86
src/models/shop.rs
Normal file
86
src/models/shop.rs
Normal file
@ -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<i32>,
|
||||||
|
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<NaiveDateTime>,
|
||||||
|
pub updated_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Model for Shop {
|
||||||
|
fn resource_name() -> &'static str {
|
||||||
|
"shop"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pk(&self) -> Option<i32> {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(db: &PgPool, id: i32) -> Result<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<Vec<Self>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
64
src/problem/mod.rs
Normal file
64
src/problem/mod.rs
Normal file
@ -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::<HttpApiProblem>() {
|
||||||
|
Ok(problem) => return problem,
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(sqlx_error) = error.downcast_ref::<sqlx::error::Error>() {
|
||||||
|
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<impl Reply, Rejection> {
|
||||||
|
if let Some(problem) = rejection.find::<HttpApiProblem>() {
|
||||||
|
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))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user