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:
Tyler Hallada 2020-11-07 02:57:46 -05:00
parent a53eeffb0f
commit 2f69c86645
6 changed files with 112 additions and 11 deletions

12
Cargo.lock generated
View File

@ -136,6 +136,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"bincode",
"chrono",
"dotenv",
"http",
@ -145,6 +146,7 @@ dependencies = [
"lazy_static",
"listenfd",
"lru",
"mime",
"seahash",
"serde",
"serde_json",
@ -158,6 +160,16 @@ dependencies = [
"warp",
]
[[package]]
name = "bincode"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
dependencies = [
"byteorder",
"serde",
]
[[package]]
name = "bitflags"
version = "1.2.1"

View File

@ -8,12 +8,14 @@ edition = "2018"
[dependencies]
anyhow = "1.0"
bincode = "1.3"
chrono = { version = "0.4", features = ["serde"] }
dotenv = "0.15"
http-api-problem = { version = "0.17", features = ["with-warp"] }
hyper = "0.13"
lazy_static = "1.4"
listenfd = "0.3"
mime = "0.3"
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" ] }
warp = { version = "0.2", features = ["compression"] }

View File

@ -29,6 +29,7 @@ pub struct Caches {
pub list_transactions_by_shop_id: Cache<(i32, ListParams), 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_bin: Cache<i32, CachedResponse>,
}
impl Caches {
@ -48,6 +49,7 @@ impl Caches {
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),
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),
}
}
}

View File

@ -9,7 +9,7 @@ use crate::models::{ListParams, MerchandiseList};
use crate::problem::reject_anyhow;
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> {
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(
shop_id: i32,
etag: Option<String>,
accept: Option<AcceptHeader>,
env: Environment,
) -> 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
.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 = with_status(reply, StatusCode::OK);
Ok(reply)
})
.await?;
.await?
}
};
Ok(check_etag(etag, response))
}

View File

@ -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::StatusCode;
use http_api_problem::HttpApiProblem;
use mime::{FromStrError, Mime};
use seahash::hash;
use serde::Serialize;
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 {
if let Some(request_etag) = 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
}
#[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)
}
}

View File

@ -278,6 +278,7 @@ async fn main() -> Result<()> {
.and(warp::path::end())
.and(warp::get())
.and(warp::header::optional("if-none-match"))
.and(warp::header::optional("accept"))
.and(with_env(env.clone()))
.and_then(handlers::merchandise_list::get_by_shop_id),
);