mirror of
https://github.com/robertwayne/axum-htmx
synced 2024-11-30 15:04:32 +01:00
commit
840ba216bb
6 changed files with 185 additions and 2 deletions
|
@ -32,6 +32,9 @@ serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum = { version = "0.7", default-features = false }
|
axum = { version = "0.7", default-features = false }
|
||||||
|
axum-test = "14"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-test = "0.4"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
25
README.md
25
README.md
|
@ -76,6 +76,25 @@ any of your responses.
|
||||||
| `HX-Trigger-After-Settle` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
|
| `HX-Trigger-After-Settle` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
|
||||||
| `HX-Trigger-After-Swap` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
|
| `HX-Trigger-After-Swap` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
|
||||||
|
|
||||||
|
Also, there are corresponding cache-related headers, which you may want to add to
|
||||||
|
`GET` responses, depending on the htmx headers.
|
||||||
|
|
||||||
|
_For example, if your server renders the full HTML when the `HX-Request` header is
|
||||||
|
missing or `false`, and it renders a fragment of that HTML when `HX-Request: true`,
|
||||||
|
you need to add `Vary: HX-Request`. That causes the cache to be keyed based on a
|
||||||
|
composite of the response URL and the `HX-Request` request header - rather than
|
||||||
|
being based just on the response URL._
|
||||||
|
|
||||||
|
Refer to [caching htmx docs section](https://htmx.org/docs/#caching) for details.
|
||||||
|
|
||||||
|
| Header | Responder |
|
||||||
|
|-------------------------|---------------------|
|
||||||
|
| `Vary: HX-Request` | `VaryHxRequest` |
|
||||||
|
| `Vary: HX-Target` | `VaryHxTarget` |
|
||||||
|
| `Vary: HX-Trigger` | `VaryHxTrigger` |
|
||||||
|
| `Vary: HX-Trigger-Name` | `VaryHxTriggerName` |
|
||||||
|
|
||||||
|
|
||||||
## Request Guards
|
## Request Guards
|
||||||
|
|
||||||
__Requires features `guards`.__
|
__Requires features `guards`.__
|
||||||
|
@ -200,6 +219,12 @@ Contributions are always welcome! If you have an idea for a feature or find a
|
||||||
bug, let me know. PR's are appreciated, but if it's not a small change, please
|
bug, let me know. PR's are appreciated, but if it's not a small change, please
|
||||||
open an issue first so we're all on the same page!
|
open an issue first so we're all on the same page!
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo +nightly test --all-features
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
`axum-htmx` is dual-licensed under either
|
`axum-htmx` is dual-licensed under either
|
||||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -1,11 +1,15 @@
|
||||||
use std::{error, fmt};
|
use std::{error, fmt};
|
||||||
|
|
||||||
use axum_core::response::IntoResponse;
|
use axum_core::response::IntoResponse;
|
||||||
use http::{header::InvalidHeaderValue, StatusCode};
|
use http::{
|
||||||
|
header::{InvalidHeaderValue, MaxSizeReached},
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum HxError {
|
pub enum HxError {
|
||||||
InvalidHeaderValue(InvalidHeaderValue),
|
InvalidHeaderValue(InvalidHeaderValue),
|
||||||
|
TooManyResponseHeaders(MaxSizeReached),
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||||
|
@ -18,6 +22,12 @@ impl From<InvalidHeaderValue> for HxError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<MaxSizeReached> for HxError {
|
||||||
|
fn from(value: MaxSizeReached) -> Self {
|
||||||
|
Self::TooManyResponseHeaders(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
|
||||||
impl From<serde_json::Error> for HxError {
|
impl From<serde_json::Error> for HxError {
|
||||||
|
@ -30,6 +40,7 @@ impl fmt::Display for HxError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
HxError::InvalidHeaderValue(_) => write!(f, "Invalid header value"),
|
HxError::InvalidHeaderValue(_) => write!(f, "Invalid header value"),
|
||||||
|
HxError::TooManyResponseHeaders(_) => write!(f, "Too many response headers"),
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
HxError::Json(_) => write!(f, "Json"),
|
HxError::Json(_) => write!(f, "Json"),
|
||||||
}
|
}
|
||||||
|
@ -40,6 +51,7 @@ impl error::Error for HxError {
|
||||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
match self {
|
match self {
|
||||||
HxError::InvalidHeaderValue(ref e) => Some(e),
|
HxError::InvalidHeaderValue(ref e) => Some(e),
|
||||||
|
HxError::TooManyResponseHeaders(ref e) => Some(e),
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
HxError::Json(ref e) => Some(e),
|
HxError::Json(ref e) => Some(e),
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl<'a, S> Layer<S> for HxRequestGuardLayer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tower service that implementes redirecting to non-partial routes.
|
/// Tower service that implements redirecting to non-partial routes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HxRequestGuard<'a, S> {
|
pub struct HxRequestGuard<'a, S> {
|
||||||
inner: S,
|
inner: S,
|
||||||
|
|
|
@ -11,6 +11,8 @@ mod location;
|
||||||
pub use location::*;
|
pub use location::*;
|
||||||
mod trigger;
|
mod trigger;
|
||||||
pub use trigger::*;
|
pub use trigger::*;
|
||||||
|
mod vary;
|
||||||
|
pub use vary::*;
|
||||||
|
|
||||||
const HX_SWAP_INNER_HTML: &str = "innerHTML";
|
const HX_SWAP_INNER_HTML: &str = "innerHTML";
|
||||||
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
|
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
|
||||||
|
|
141
src/responders/vary.rs
Normal file
141
src/responders/vary.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
use axum_core::response::{IntoResponseParts, ResponseParts};
|
||||||
|
use http::header::{HeaderValue, VARY};
|
||||||
|
|
||||||
|
use crate::{extractors, headers, HxError};
|
||||||
|
|
||||||
|
const HX_REQUEST: HeaderValue = HeaderValue::from_static(headers::HX_REQUEST);
|
||||||
|
const HX_TARGET: HeaderValue = HeaderValue::from_static(headers::HX_TARGET);
|
||||||
|
const HX_TRIGGER: HeaderValue = HeaderValue::from_static(headers::HX_TRIGGER);
|
||||||
|
const HX_TRIGGER_NAME: HeaderValue = HeaderValue::from_static(headers::HX_TRIGGER_NAME);
|
||||||
|
|
||||||
|
/// The `Vary: HX-Request` header.
|
||||||
|
///
|
||||||
|
/// You may want to add this header to the response if your handler responds differently based on
|
||||||
|
/// the `HX-Request` request header.
|
||||||
|
///
|
||||||
|
/// For example, if your server renders the full HTML when the `HX-Request` header is missing or
|
||||||
|
/// `false`, and it renders a fragment of that HTML when `HX-Request: true`.
|
||||||
|
///
|
||||||
|
/// 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 VaryHxRequest;
|
||||||
|
|
||||||
|
impl IntoResponseParts for VaryHxRequest {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut().try_append(VARY, HX_REQUEST)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl extractors::HxRequest {
|
||||||
|
/// Convenience method to create the corresponding `Vary` response header
|
||||||
|
pub fn vary_response() -> VaryHxRequest {
|
||||||
|
VaryHxRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Vary: HX-Target` header.
|
||||||
|
///
|
||||||
|
/// You may want to add this header to the response if your handler responds differently based on
|
||||||
|
/// the `HX-Target` 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 VaryHxTarget;
|
||||||
|
|
||||||
|
impl IntoResponseParts for VaryHxTarget {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut().try_append(VARY, HX_TARGET)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl extractors::HxTarget {
|
||||||
|
/// Convenience method to create the corresponding `Vary` response header
|
||||||
|
pub fn vary_response() -> VaryHxTarget {
|
||||||
|
VaryHxTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Vary: HX-Trigger` header.
|
||||||
|
///
|
||||||
|
/// You may want to add this header to the response if your handler responds differently based on
|
||||||
|
/// the `HX-Trigger` 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 VaryHxTrigger;
|
||||||
|
|
||||||
|
impl IntoResponseParts for VaryHxTrigger {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut().try_append(VARY, HX_TRIGGER)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl extractors::HxTrigger {
|
||||||
|
/// Convenience method to create the corresponding `Vary` response header
|
||||||
|
pub fn vary_response() -> VaryHxTrigger {
|
||||||
|
VaryHxTrigger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Vary: HX-Trigger-Name` header.
|
||||||
|
///
|
||||||
|
/// You may want to add this header to the response if your handler responds differently based on
|
||||||
|
/// the `HX-Trigger-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 VaryHxTriggerName;
|
||||||
|
|
||||||
|
impl IntoResponseParts for VaryHxTriggerName {
|
||||||
|
type Error = HxError;
|
||||||
|
|
||||||
|
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||||
|
res.headers_mut().try_append(VARY, HX_TRIGGER_NAME)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl extractors::HxTriggerName {
|
||||||
|
/// Convenience method to create the corresponding `Vary` response header
|
||||||
|
pub fn vary_response() -> VaryHxTriggerName {
|
||||||
|
VaryHxTriggerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use std::collections::hash_set::HashSet;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_headers() {
|
||||||
|
let app = Router::new().route("/", get(|| async { (VaryHxRequest, VaryHxTarget, "foo") }));
|
||||||
|
let server = axum_test::TestServer::new(app).unwrap();
|
||||||
|
|
||||||
|
let resp = server.get("/").await;
|
||||||
|
let values: HashSet<HeaderValue> = resp.iter_headers_by_name("vary").cloned().collect();
|
||||||
|
assert_eq!(values, HashSet::from([HX_REQUEST, HX_TARGET]));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue