aro-web 1.0.0

HTTP/ADR layer for the Aro web framework
Documentation
//! `ETag` and conditional response helpers.
//!
//! Provides an [`IfNoneMatch`] extractor for the `If-None-Match` request
//! header, a [`conditional`] helper that returns 304 when the `ETag` matches,
//! and a [`with_etag`] helper to attach an `ETag` header to a response.

use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};

/// Extractor for the `If-None-Match` request header.
///
/// Always succeeds — contains `None` when the header is absent.
pub struct IfNoneMatch(pub Option<String>);

impl<S> FromRequestParts<S> for IfNoneMatch
where
    S: Send + Sync,
{
    type Rejection = std::convert::Infallible;

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        let value = parts
            .headers
            .get(header::IF_NONE_MATCH)
            .and_then(|v| v.to_str().ok())
            .map(ToOwned::to_owned);
        Ok(Self(value))
    }
}

/// Returns `Some(304 Not Modified)` if `etag` matches the client's
/// `If-None-Match` value. Returns `None` otherwise, signalling the
/// caller should send the full response.
#[expect(
    clippy::expect_used,
    reason = "ETag value is from trusted internal source"
)]
pub fn conditional(etag: &str, if_none_match: &IfNoneMatch) -> Option<Response> {
    let client_etag = if_none_match.0.as_deref()?;
    if client_etag == etag {
        let mut response = StatusCode::NOT_MODIFIED.into_response();
        response.headers_mut().insert(
            header::ETAG,
            HeaderValue::from_str(etag).expect("valid ETag header value"),
        );
        Some(response)
    } else {
        None
    }
}

/// Attaches an `ETag` header to a response.
#[expect(
    clippy::expect_used,
    reason = "ETag value is from trusted internal source"
)]
pub fn with_etag<T: IntoResponse>(response: T, etag: &str) -> Response {
    let mut response = response.into_response();
    response.headers_mut().insert(
        header::ETAG,
        HeaderValue::from_str(etag).expect("valid ETag header value"),
    );
    response
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn conditional_returns_304_on_match() {
        let inm = IfNoneMatch(Some("\"abc123\"".to_owned()));
        let resp = conditional("\"abc123\"", &inm);
        assert!(resp.is_some());
        let resp = resp.unwrap();
        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
        assert_eq!(
            resp.headers().get(header::ETAG).unwrap().to_str().unwrap(),
            "\"abc123\""
        );
    }

    #[test]
    fn conditional_returns_none_on_mismatch() {
        let inm = IfNoneMatch(Some("\"old\"".to_owned()));
        assert!(conditional("\"new\"", &inm).is_none());
    }

    #[test]
    fn conditional_returns_none_when_header_absent() {
        let inm = IfNoneMatch(None);
        assert!(conditional("\"abc\"", &inm).is_none());
    }

    #[test]
    fn with_etag_sets_header() {
        let resp = with_etag("hello", "\"tag1\"");
        assert_eq!(
            resp.headers().get(header::ETAG).unwrap().to_str().unwrap(),
            "\"tag1\""
        );
    }
}