Add bincode format to endpointI'm testing out serializing data with bincode and using the Accept
header to switch between formats for GET responses.If this works, I'll extend it to all endpoints and also add deserializing bincode from POST and PATCH requests.
This commit is contained in:
parent
a53eeffb0f
commit
2f69c86645
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -136,6 +136,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"http",
|
"http",
|
||||||
@ -145,6 +146,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"listenfd",
|
"listenfd",
|
||||||
"lru",
|
"lru",
|
||||||
|
"mime",
|
||||||
"seahash",
|
"seahash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -158,6 +160,16 @@ dependencies = [
|
|||||||
"warp",
|
"warp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
@ -8,12 +8,14 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
bincode = "1.3"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
http-api-problem = { version = "0.17", features = ["with-warp"] }
|
http-api-problem = { version = "0.17", features = ["with-warp"] }
|
||||||
hyper = "0.13"
|
hyper = "0.13"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
listenfd = "0.3"
|
listenfd = "0.3"
|
||||||
|
mime = "0.3"
|
||||||
tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] }
|
tokio = { version = "0.2", features = ["macros", "rt-threaded", "sync"] }
|
||||||
sqlx = { version = "0.3", default-features = false, features = [ "runtime-tokio", "macros", "postgres", "chrono", "uuid", "ipnetwork", "json" ] }
|
sqlx = { version = "0.3", default-features = false, features = [ "runtime-tokio", "macros", "postgres", "chrono", "uuid", "ipnetwork", "json" ] }
|
||||||
warp = { version = "0.2", features = ["compression"] }
|
warp = { version = "0.2", features = ["compression"] }
|
||||||
|
@ -29,6 +29,7 @@ pub struct Caches {
|
|||||||
pub list_transactions_by_shop_id: Cache<(i32, ListParams), CachedResponse>,
|
pub list_transactions_by_shop_id: Cache<(i32, ListParams), CachedResponse>,
|
||||||
pub interior_ref_list_by_shop_id: Cache<i32, CachedResponse>,
|
pub interior_ref_list_by_shop_id: Cache<i32, CachedResponse>,
|
||||||
pub merchandise_list_by_shop_id: Cache<i32, CachedResponse>,
|
pub merchandise_list_by_shop_id: Cache<i32, CachedResponse>,
|
||||||
|
pub merchandise_list_by_shop_id_bin: Cache<i32, CachedResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Caches {
|
impl Caches {
|
||||||
@ -48,6 +49,7 @@ impl Caches {
|
|||||||
list_transactions_by_shop_id: Cache::new("list_transaction_by_shop_id", 100),
|
list_transactions_by_shop_id: Cache::new("list_transaction_by_shop_id", 100),
|
||||||
interior_ref_list_by_shop_id: Cache::new("interior_ref_list_by_shop_id", 100),
|
interior_ref_list_by_shop_id: Cache::new("interior_ref_list_by_shop_id", 100),
|
||||||
merchandise_list_by_shop_id: Cache::new("merchandise_list_by_shop_id", 100),
|
merchandise_list_by_shop_id: Cache::new("merchandise_list_by_shop_id", 100),
|
||||||
|
merchandise_list_by_shop_id_bin: Cache::new("merchandise_list_by_shop_id_bin", 100),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use crate::models::{ListParams, MerchandiseList};
|
|||||||
use crate::problem::reject_anyhow;
|
use crate::problem::reject_anyhow;
|
||||||
use crate::Environment;
|
use crate::Environment;
|
||||||
|
|
||||||
use super::{authenticate, check_etag, JsonWithETag};
|
use super::{authenticate, check_etag, AcceptHeader, BincodeWithETag, JsonWithETag};
|
||||||
|
|
||||||
pub async fn get(id: i32, etag: Option<String>, env: Environment) -> Result<impl Reply, Rejection> {
|
pub async fn get(id: i32, etag: Option<String>, env: Environment) -> Result<impl Reply, Rejection> {
|
||||||
let response = CACHES
|
let response = CACHES
|
||||||
@ -27,17 +27,35 @@ pub async fn get(id: i32, etag: Option<String>, env: Environment) -> Result<impl
|
|||||||
pub async fn get_by_shop_id(
|
pub async fn get_by_shop_id(
|
||||||
shop_id: i32,
|
shop_id: i32,
|
||||||
etag: Option<String>,
|
etag: Option<String>,
|
||||||
|
accept: Option<AcceptHeader>,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
) -> Result<impl Reply, Rejection> {
|
) -> Result<impl Reply, Rejection> {
|
||||||
let response = CACHES
|
let response = match accept {
|
||||||
|
Some(accept) if accept.accepts_bincode() => {
|
||||||
|
CACHES
|
||||||
|
.merchandise_list_by_shop_id_bin
|
||||||
|
.get_response(shop_id, || async {
|
||||||
|
let merchandise_list =
|
||||||
|
MerchandiseList::get_by_shop_id(&env.db, shop_id).await?;
|
||||||
|
let reply = BincodeWithETag::from_serializable(&merchandise_list)?;
|
||||||
|
let reply = with_status(reply, StatusCode::OK);
|
||||||
|
Ok(reply)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
CACHES
|
||||||
.merchandise_list_by_shop_id
|
.merchandise_list_by_shop_id
|
||||||
.get_response(shop_id, || async {
|
.get_response(shop_id, || async {
|
||||||
let merchandise_list = MerchandiseList::get_by_shop_id(&env.db, shop_id).await?;
|
let merchandise_list =
|
||||||
|
MerchandiseList::get_by_shop_id(&env.db, shop_id).await?;
|
||||||
let reply = JsonWithETag::from_serializable(&merchandise_list)?;
|
let reply = JsonWithETag::from_serializable(&merchandise_list)?;
|
||||||
let reply = with_status(reply, StatusCode::OK);
|
let reply = with_status(reply, StatusCode::OK);
|
||||||
Ok(reply)
|
Ok(reply)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(check_etag(etag, response))
|
Ok(check_etag(etag, response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
use http::header::{HeaderValue, CONTENT_TYPE, ETAG};
|
use http::header::{HeaderValue, CONTENT_TYPE, ETAG};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use http_api_problem::HttpApiProblem;
|
use http_api_problem::HttpApiProblem;
|
||||||
|
use mime::{FromStrError, Mime};
|
||||||
use seahash::hash;
|
use seahash::hash;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::{error, instrument, warn};
|
use tracing::{error, instrument, warn};
|
||||||
@ -84,6 +87,45 @@ impl JsonWithETag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct BincodeWithETag {
|
||||||
|
body: Vec<u8>,
|
||||||
|
etag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reply for BincodeWithETag {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let mut res = Response::new(self.body.into());
|
||||||
|
res.headers_mut().insert(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("application/octet-stream"),
|
||||||
|
);
|
||||||
|
if let Ok(val) = HeaderValue::from_str(&self.etag) {
|
||||||
|
res.headers_mut().insert(ETAG, val);
|
||||||
|
} else {
|
||||||
|
// This should never happen in practice since etag values should only be hex-encoded strings
|
||||||
|
warn!("omitting etag header with invalid ASCII characters")
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BincodeWithETag {
|
||||||
|
pub fn from_serializable<T: Serialize>(val: &T) -> Result<Self> {
|
||||||
|
let bytes = bincode::serialize(val).map_err(|err| {
|
||||||
|
error!("Failed to serialize database value to bincode: {}", err);
|
||||||
|
anyhow!(HttpApiProblem::with_title_and_type_from_status(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
.set_detail(format!(
|
||||||
|
"Failed to serialize database value to bincode: {}",
|
||||||
|
err
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
let etag = format!("{:x}", hash(&bytes));
|
||||||
|
Ok(Self { body: bytes, etag })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_etag(etag: Option<String>, response: CachedResponse) -> CachedResponse {
|
pub fn check_etag(etag: Option<String>, response: CachedResponse) -> CachedResponse {
|
||||||
if let Some(request_etag) = etag {
|
if let Some(request_etag) = etag {
|
||||||
if let Some(response_etag) = response.headers.get("etag") {
|
if let Some(response_etag) = response.headers.get("etag") {
|
||||||
@ -94,3 +136,27 @@ pub fn check_etag(etag: Option<String>, response: CachedResponse) -> CachedRespo
|
|||||||
}
|
}
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct AcceptHeader {
|
||||||
|
mimes: Vec<Mime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AcceptHeader {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
mimes: s
|
||||||
|
.split(',')
|
||||||
|
.map(|part| part.trim().parse::<Mime>())
|
||||||
|
.collect::<std::result::Result<Vec<Mime>, FromStrError>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcceptHeader {
|
||||||
|
pub fn accepts_bincode(&self) -> bool {
|
||||||
|
self.mimes.contains(&mime::APPLICATION_OCTET_STREAM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -278,6 +278,7 @@ async fn main() -> Result<()> {
|
|||||||
.and(warp::path::end())
|
.and(warp::path::end())
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.and(warp::header::optional("if-none-match"))
|
.and(warp::header::optional("if-none-match"))
|
||||||
|
.and(warp::header::optional("accept"))
|
||||||
.and(with_env(env.clone()))
|
.and(with_env(env.clone()))
|
||||||
.and_then(handlers::merchandise_list::get_by_shop_id),
|
.and_then(handlers::merchandise_list::get_by_shop_id),
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user