use std::{fmt, future::Future, time::Duration};
use bytes::Bytes;
use http::{HeaderName, HeaderValue, Response, StatusCode, header, request::Parts};
use serde::Serialize;
use super::{HttpError, answer::json};
use crate::{Revisioned, parse_revision};
#[cfg(all(test, feature = "axum"))]
mod tests;
pub const WAIT_FOR_REVISION: HeaderName = HeaderName::from_static("wait-for-revision");
pub const REVISION: HeaderName = HeaderName::from_static("revision");
pub const DEFAULT_WAIT: Duration = Duration::from_secs(5);
#[derive(Clone, Debug)]
pub struct Reading {
wanted: Option<HeaderValue>,
tags: Vec<HeaderValue>,
resource: String,
varies: Option<String>,
}
impl Reading {
#[must_use]
pub fn of(parts: &Parts) -> Self {
Self {
wanted: parts.headers.get(&WAIT_FOR_REVISION).cloned(),
tags: parts
.headers
.get_all(header::IF_NONE_MATCH)
.iter()
.cloned()
.collect(),
resource: format!(
"{}?{}",
parts.uri.path(),
parts.uri.query().unwrap_or_default()
),
varies: None,
}
}
#[must_use]
pub fn varying(self, value: impl fmt::Display) -> Self {
Self {
varies: Some(value.to_string()),
..self
}
}
pub async fn awaiting<V: Revisioned + ?Sized>(
&self,
view: &V,
wait: Duration,
) -> Result<(), HttpError> {
let Some(wanted) = &self.wanted else {
return Ok(());
};
if wanted.is_empty() {
return Ok(());
}
let wanted = String::from_utf8_lossy(wanted.as_bytes());
let _ = parse_revision(&wanted).map_err(HttpError::malformed)?;
match tokio::time::timeout(wait, view.wait_for(&wanted)).await {
Ok(Err(failure)) => Err(HttpError::of(failure)),
Ok(Ok(())) | Err(_) => Ok(()),
}
}
pub async fn revisioned<V, Q, T, F, A>(
&self,
view: &V,
query: &Q,
wait: Duration,
answer: F,
) -> Result<QueryAnswer<T>, HttpError>
where
V: Revisioned + ?Sized,
Q: Serialize + ?Sized,
F: FnOnce() -> A,
A: Future<Output = Result<QueryAnswer<T>, HttpError>>,
{
self.awaiting(view, wait).await?;
let revision = view.revision().await.map_err(HttpError::of)?;
let Some(revision) = revision.filter(|revision| !revision.is_empty()) else {
return answer().await;
};
let tag = self.tag_of(&revision, query);
if let Some(tag) = &tag
&& self.holds(tag)
{
return Ok(QueryAnswer {
kind: Kind::Unchanged {
revision,
tag: tag.clone(),
},
});
}
Ok(answer().await?.tagged(revision, tag))
}
fn tag_of<Q: Serialize + ?Sized>(&self, revision: &str, query: &Q) -> Option<String> {
let query = serde_json::to_vec(query).ok()?;
let mut hash = Fnv64a::default();
hash.write(self.resource.as_bytes());
hash.write(b"\0");
hash.write(&query);
if let Some(varies) = &self.varies {
hash.write(b"\0");
hash.write(varies.as_bytes());
}
Some(format!("\"{revision}-{:x}\"", hash.0))
}
fn holds(&self, tag: &str) -> bool {
let joined: Vec<String> = self
.tags
.iter()
.map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned())
.collect();
let mut list = joined.join(",");
loop {
let rest = list.trim_start_matches([' ', '\t', ',']);
if rest.starts_with('*') {
return true;
}
let Some(quoted) = rest.strip_prefix("W/").unwrap_or(rest).strip_prefix('"') else {
return false;
};
let Some((opaque, after)) = quoted.split_once('"') else {
return false;
};
if format!("\"{opaque}\"") == tag {
return true;
}
list = after.to_owned();
}
}
}
struct Fnv64a(u64);
impl Default for Fnv64a {
fn default() -> Self {
Self(0xcbf2_9ce4_8422_2325)
}
}
impl Fnv64a {
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 ^= u64::from(*byte);
self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
}
}
}
#[derive(Debug)]
#[must_use]
pub struct QueryAnswer<T> {
kind: Kind<T>,
}
#[derive(Debug)]
enum Kind<T> {
Found {
result: T,
revision: Option<(String, Option<String>)>,
},
Missing,
Unchanged {
revision: String,
tag: String,
},
}
impl<T> QueryAnswer<T> {
pub fn new(result: T) -> Self {
Self {
kind: Kind::Found {
result,
revision: None,
},
}
}
pub fn found(item: Option<T>) -> Self {
item.map_or(
Self {
kind: Kind::Missing,
},
Self::new,
)
}
fn tagged(self, revision: String, tag: Option<String>) -> Self {
match self.kind {
Kind::Found { result, .. } => Self {
kind: Kind::Found {
result,
revision: Some((revision, tag)),
},
},
kind => Self { kind },
}
}
}
impl<T: Serialize> From<QueryAnswer<T>> for Response<Bytes> {
fn from(answer: QueryAnswer<T>) -> Self {
match answer.kind {
Kind::Found { result, revision } => {
let value = match serde_json::to_value(result) {
Ok(value) => value,
Err(failure) => return HttpError::internal(failure).into(),
};
let mut response = json(StatusCode::OK, &value);
if let Some((revision, tag)) = revision {
mark_revision(&mut response, &revision, tag.as_deref());
}
response
}
Kind::Missing => HttpError::nothing_found().into(),
Kind::Unchanged { revision, tag } => {
let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::NOT_MODIFIED;
mark_revision(&mut response, &revision, Some(&tag));
response
}
}
}
}
#[cfg(feature = "axum")]
impl<T: Serialize> axum::response::IntoResponse for QueryAnswer<T> {
fn into_response(self) -> axum::response::Response {
Response::<Bytes>::from(self).map(axum::body::Body::from)
}
}
fn mark_revision(response: &mut Response<Bytes>, revision: &str, tag: Option<&str>) {
let headers = response.headers_mut();
let _ = headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-cache"),
);
if let Ok(revision) = HeaderValue::from_str(revision) {
let _ = headers.insert(REVISION, revision);
}
if let Some(tag) = tag.and_then(|tag| HeaderValue::from_str(tag).ok()) {
let _ = headers.insert(header::ETAG, tag);
}
}