Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type.
| `HX-Target` | `HxTarget` | `Option<String>` |
| `HX-Trigger-Name` | `HxTriggerName` | `Option<String>` |
| `HX-Trigger` | `HxTrigger` | `Option<String>` |
| `HX-Source-Name` | `HxSourceName` | `Option<String>` |
| `HX-Source` | `HxSource` | `Option<String>` |

## Responders

Expand Down Expand Up @@ -96,6 +98,8 @@ Refer to [caching htmx docs section][htmx-caching] for details.
| `Vary: HX-Target` | `VaryHxTarget` |
| `Vary: HX-Trigger` | `VaryHxTrigger` |
| `Vary: HX-Trigger-Name` | `VaryHxTriggerName` |
| `Vary: HX-Source` | `VaryHxSource` |
| `Vary: HX-Source-Name` | `VaryHxSourceName` |

Look at the [Auto Caching Management](#auto-caching-management) section for
automatic `Vary` headers management.
Expand Down
46 changes: 39 additions & 7 deletions src/auto_vary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ use tower::{Layer, Service};

use crate::{
HxError,
headers::{HX_REQUEST_STR, HX_TARGET_STR, HX_TRIGGER_NAME_STR, HX_TRIGGER_STR},
headers::{
HX_REQUEST_STR, HX_SOURCE_NAME_STR, HX_SOURCE_STR, HX_TARGET_STR, HX_TRIGGER_NAME_STR,
HX_TRIGGER_STR,
},
};
#[cfg(doc)]
use crate::{HxRequest, HxTarget, HxTrigger, HxTriggerName};
use crate::{HxRequest, HxSource, HxSourceName, HxTarget, HxTrigger, HxTriggerName};

const MIDDLEWARE_DOUBLE_USE: &str =
"Configuration error: `axum_httpx::vary_middleware` is used twice";

/// Addresses [htmx caching issues](https://htmx.org/docs/#caching)
/// by automatically adding a corresponding `Vary` header when
/// [`HxRequest`], [`HxTarget`], [`HxTrigger`], [`HxTriggerName`]
/// [`HxRequest`], [`HxTarget`], [`HxTrigger`], [`HxTriggerName`], [`HxSource`], [`HxSourceName`]
/// or their combination is used.
#[derive(Clone)]
pub struct AutoVaryLayer;
Expand Down Expand Up @@ -80,7 +83,9 @@ define_notifiers!(
HxRequestExtracted,
HxTargetExtracted,
HxTriggerExtracted,
HxTriggerNameExtracted
HxTriggerNameExtracted,
HxSourceExtracted,
HxSourceNameExtracted
);

impl<S> Layer<S> for AutoVaryLayer {
Expand Down Expand Up @@ -111,6 +116,8 @@ where
(HxTargetExtracted::insert(exts), HX_TARGET_STR),
(HxTriggerExtracted::insert(exts), HX_TRIGGER_STR),
(HxTriggerNameExtracted::insert(exts), HX_TRIGGER_NAME_STR),
(HxSourceExtracted::insert(exts), HX_SOURCE_STR),
(HxSourceNameExtracted::insert(exts), HX_SOURCE_NAME_STR),
];
let future = self.inner.call(request);
Box::pin(async move {
Expand Down Expand Up @@ -148,7 +155,7 @@ mod tests {
use axum::{Router, routing::get};

use super::*;
use crate::{HxRequest, HxTarget, HxTrigger, HxTriggerName};
use crate::{HxRequest, HxSource, HxSourceName, HxTarget, HxTrigger, HxTriggerName};

fn vary_headers(resp: &axum_test::TestResponse) -> Vec<HeaderValue> {
resp.iter_headers_by_name("vary").cloned().collect()
Expand All @@ -161,13 +168,22 @@ mod tests {
.route("/hx-target", get(|_: HxTarget| async { () }))
.route("/hx-trigger", get(|_: HxTrigger| async { () }))
.route("/hx-trigger-name", get(|_: HxTriggerName| async { () }))
.route("/hx-source", get(|_: HxSource| async { () }))
.route("/hx-source-name", get(|_: HxSourceName| async { () }))
.route(
"/repeated-extractor",
get(|_: HxRequest, _: HxRequest| async { () }),
)
.route(
"/multiple-extractors",
get(|_: HxRequest, _: HxTarget, _: HxTrigger, _: HxTriggerName| async { () }),
get(
|_: HxRequest,
_: HxTarget,
_: HxTrigger,
_: HxTriggerName,
_: HxSource,
_: HxSourceName| async { () },
),
)
.layer(AutoVaryLayer);
axum_test::TestServer::new(app).unwrap()
Expand Down Expand Up @@ -210,6 +226,22 @@ mod tests {
);
}

#[tokio::test]
async fn single_hx_source() {
assert_eq!(
vary_headers(&server().get("/hx-source").await),
["hx-source"]
);
}

#[tokio::test]
async fn single_hx_source_name() {
assert_eq!(
vary_headers(&server().get("/hx-source-name").await),
["hx-source-name"]
);
}

#[tokio::test]
async fn repeated_extractor() {
assert_eq!(
Expand All @@ -223,7 +255,7 @@ mod tests {
async fn multiple_extractors() {
assert_eq!(
vary_headers(&server().get("/multiple-extractors").await),
["hx-request, hx-target, hx-trigger, hx-trigger-name"],
["hx-request, hx-target, hx-trigger, hx-trigger-name, hx-source, hx-source-name"],
);
}
}
72 changes: 70 additions & 2 deletions src/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use axum_core::extract::FromRequestParts;
use http::request::Parts;

use crate::{
HX_BOOSTED, HX_CURRENT_URL, HX_HISTORY_RESTORE_REQUEST, HX_PROMPT, HX_REQUEST, HX_TARGET,
HX_TRIGGER, HX_TRIGGER_NAME,
HX_BOOSTED, HX_CURRENT_URL, HX_HISTORY_RESTORE_REQUEST, HX_PROMPT, HX_REQUEST, HX_SOURCE,
HX_SOURCE_NAME, HX_TARGET, HX_TRIGGER, HX_TRIGGER_NAME,
};

/// The `HX-Boosted` header.
Expand Down Expand Up @@ -246,3 +246,71 @@ where
Ok(HxTrigger(None))
}
}

/// The `HX-Source-Name` header.
///
/// This is set when a request is made from an element that has the `hx-source`
/// attribute set. The value will contain the source element's name. If the
/// name does not exist on the page, the value will be None.
///
/// This extractor will always return a value. If the header is not present, it
/// will return `None`.
#[derive(Debug, Clone)]
pub struct HxSourceName(pub Option<String>);

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

async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
#[cfg(feature = "auto-vary")]
parts
.extensions
.get_mut::<crate::auto_vary::HxSourceNameExtracted>()
.map(crate::auto_vary::Notifier::notify);

if let Some(source_name) = parts.headers.get(HX_SOURCE_NAME) {
if let Ok(source_name) = source_name.to_str() {
return Ok(HxSourceName(Some(source_name.to_string())));
}
}

Ok(HxSourceName(None))
}
}

/// The `HX-Source` header.
///
/// This is set when a request is made from an element that has the `hx-source`
/// attribute set. The value will contain the source element's id. If the id
/// does not exist on the page, the value will be None.
///
/// This extractor will always return a value. If the header is not present, it
/// will return `None`.
#[derive(Debug, Clone)]
pub struct HxSource(pub Option<String>);

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

async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
#[cfg(feature = "auto-vary")]
parts
.extensions
.get_mut::<crate::auto_vary::HxSourceExtracted>()
.map(crate::auto_vary::Notifier::notify);

if let Some(source) = parts.headers.get(HX_SOURCE) {
if let Ok(source) = source.to_str() {
return Ok(HxSource(Some(source.to_string())));
}
}

Ok(HxSource(None))
}
}
15 changes: 15 additions & 0 deletions src/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ pub(crate) const HX_TRIGGER_NAME_STR: &str = "hx-trigger-name";
/// The `name` of the triggered element, if it exists.
pub const HX_TRIGGER_NAME: HeaderName = HeaderName::from_static(HX_TRIGGER_NAME_STR);

pub(crate) const HX_SOURCE_NAME_STR: &str = "hx-source-name";

/// The `name` of the source element, if it exists.
/// Htmx Four only
pub const HX_SOURCE_NAME: HeaderName = HeaderName::from_static(HX_SOURCE_NAME_STR);

/// Allows you to do a client-side redirect that does not do a full page reload.
pub const HX_LOCATION: HeaderName = HeaderName::from_static("hx-location");

Expand Down Expand Up @@ -76,6 +82,15 @@ pub(crate) const HX_TRIGGER_STR: &str = "hx-trigger";
/// See <https://htmx.org/headers/hx-trigger/> for more information.
pub const HX_TRIGGER: HeaderName = HeaderName::from_static(HX_TRIGGER_STR);

pub(crate) const HX_SOURCE_STR: &str = "hx-source";

/// Contains the `id` of the source element that triggered the
/// request.
///
/// Htmx Four only
/// See <https://four.htmx.org/reference/#request_headers> for more information.
pub const HX_SOURCE: HeaderName = HeaderName::from_static(HX_SOURCE_STR);

/// Allows you to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
Expand Down
59 changes: 59 additions & 0 deletions src/responders/vary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const HX_REQUEST: HeaderValue = HeaderValue::from_static(headers::HX_REQUEST_STR
const HX_TARGET: HeaderValue = HeaderValue::from_static(headers::HX_TARGET_STR);
const HX_TRIGGER: HeaderValue = HeaderValue::from_static(headers::HX_TRIGGER_STR);
const HX_TRIGGER_NAME: HeaderValue = HeaderValue::from_static(headers::HX_TRIGGER_NAME_STR);
const HX_SOURCE: HeaderValue = HeaderValue::from_static(headers::HX_SOURCE_STR);
const HX_SOURCE_NAME: HeaderValue = HeaderValue::from_static(headers::HX_SOURCE_NAME_STR);

/// The `Vary: HX-Request` header.
///
Expand Down Expand Up @@ -127,6 +129,63 @@ impl extractors::HxTriggerName {
VaryHxTriggerName
}
}
/// The `Vary: HX-Source` header.
///
/// You may want to add this header to the response if your handler responds
/// differently based on the `HX-Source` request header.
///
/// You probably need this only for `GET` requests, as other HTTP methods are
/// not cached by default.
///
/// See <https://htmx.org/docs/#caching> for more information.
#[derive(Debug, Clone)]
pub struct VaryHxSource;

impl IntoResponseParts for VaryHxSource {
type Error = HxError;

fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().try_append(VARY, HX_SOURCE)?;

Ok(res)
}
}

impl extractors::HxSource {
/// Convenience method to create the corresponding `Vary` response header
pub fn vary_response() -> VaryHxSource {
VaryHxSource
}
}

/// The `Vary: HX-Source-Name` header.
///
/// You may want to add this header to the response if your handler responds
/// differently based on the `HX-Source-Name` request header.
///
/// You probably need this only for `GET` requests, as other HTTP methods are
/// not cached by default.
///
/// See <https://htmx.org/docs/#caching> for more information.
#[derive(Debug, Clone)]
pub struct VaryHxSourceName;

impl IntoResponseParts for VaryHxSourceName {
type Error = HxError;

fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().try_append(VARY, HX_SOURCE_NAME)?;

Ok(res)
}
}

impl extractors::HxSourceName {
/// Convenience method to create the corresponding `Vary` response header
pub fn vary_response() -> VaryHxSourceName {
VaryHxSourceName
}
}

#[cfg(test)]
mod tests {
Expand Down