Repository: robertwayne/axum-htmx
Branch: main
Commit: 70a63fd43ae5
Files: 19
Total size: 75.2 KB
Directory structure:
gitextract_pmy2ca9y/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── examples/
│ └── auto-vary.rs
├── rustfmt.toml
└── src/
├── auto_vary.rs
├── error.rs
├── extractors.rs
├── guard.rs
├── headers.rs
├── lib.rs
├── responders/
│ ├── location.rs
│ ├── trigger.rs
│ └── vary.rs
└── responders.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
env:
CARGO_TERM_COLOR: always
on:
push:
branches: [main]
pull_request: {}
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- name: run clippy
run: cargo clippy --all-features -- -D warnings
- name: run formatter checks
run: cargo fmt --all --check
test:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
- uses: Swatinem/rust-cache@v2
- name: run tests
run: cargo test --all-features
================================================
FILE: .gitignore
================================================
# Directories
.cargo/
.turbo/
assets/
build/
data/
dist/
node_modules/
public/
target/
# Files
.env
.env.development
.env.production
.log
Cargo.lock
pnpm-lock.yaml
# User Settings
.idea
.vscode
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## v0.8.1
- Revert an accidental breaking change introducting a nightly-only feature.
## v0.8.0
- Change `HxLocation`, `HxPushUrl`, `HxRedirect`, and `HxReplaceUrl` to take `String` instead of `Uri` for better flexibility and ease-of-use. _([@skwee357](https://github.com/skwee357))_
- `HxCurrentUrl` remains unchanged in order to align with axum.
- Changed how `LocationOptions` is handled internally with regard to non-exhaustiveness,
allowing external crates to use the functional record update syntax.
## v0.7.0
- Support axum v0.8. _([@kakalos12](https://github.com/kakalos12))_
## v0.6.0
- Added support for Vary headers in responses via the `VaryHxRequest`, `VaryHxTarget`, `VaryHxTrigger`, and `VaryHxTriggerName` responders. _([@imbolc](https://github.com/imbolc))_
- Header names/values are now typed as `HeaderName` and `HeaderValue` instead of `&str`. _([@imbolc](https://github.com/imbolc))_
- `HxError` now implements source on `error::Error`. _([@imbolc](https://github.com/imbolc))_
- Added `AutoVaryLayer` middleware to automatically manage `Vary` headers when using corresponding extractors. The middleware is behind the `auto-vary` feature. [See this section of the README for more details.](https://github.com/robertwayne/axum-htmx?tab=readme-ov-file#vary-responders). _([@imbolc](https://github.com/imbolc))_
## v0.5.0
There are some several breaking changes in this release. Big thanks to [@ItsEthra](https://github.com/ItsEthra) for their work in several PRs!
- All responders now take an `HxEvent` instead of a `String | HxEvent`. When the `serde` flag is enabled, it will expose additional data fields.
- `HxResponseTrigger` is now a simple struct containing an `TriggerMode` and a `Vec<HxEvent>`. There are several methods to make constructing these easier: `HxResponseTrigger::normal`, `HxResponseTrigger::after_settle`, and `HxResponseTrigger::after_swap`.
- The `HxCurrentUrl` extractor now returns an `Option<axum::http::Uri>` instead of a `String`. If the Uri cannot be parsed, it will return `None`.
- All Uri-related responders now impl `TryFrom<&str>`.
- `HxError::Serialization` has been renamed to `HxError::Json`.
- The `HxResponseTrigger*` header will not be added to the response if the event list is empty.
- Added feature flag badges and made additional updates to the docs.rs pages.
- Reduced dependency count / compile time by swapping `axum` out for the `axum-core`, `async-trait`, and `http` crates.
## v0.4.0
- Added support for all [htmx response headers](https://htmx.org/reference/#response_headers) via a type implementing `IntoResponseParts`. These "responders" allow you to simply and safely apply the HX-* headers to any of your responses. Thanks to [@pfz4](https://github.com/pfz4) for the implementation work! ([#5](https://github.com/robertwayne/axum-htmx/pull/5))
## v0.3.1
- Rebuild docs with features enabled so `HxRequestGuardLayer` is visible on docs.rs.
## v0.3.0
- `HxRequestGuardLayer` now redirects on failures instead of returning a 403\. By default, it will redirect to "/", but you can specify a different route to redirect to with `HxRequestGuardLayer::new("/your-route-here")`.
## v0.2.0
- Added `HxRequestGuardLayer`, allowing you to protect an entire router from non-htmx requests.
## v0.1.0
- Initial release.
================================================
FILE: Cargo.toml
================================================
[package]
name = "axum-htmx"
authors = ["Rob Wagner <rob@sombia.com>"]
license = "MIT OR Apache-2.0"
description = "A set of htmx extractors, responders, and request guards for axum."
repository = "https://github.com/robertwayne/axum-htmx"
categories = ["web-programming"]
keywords = ["axum", "htmx"]
readme = "README.md"
version = "0.8.1"
edition = "2024"
rust-version = "1.87"
[features]
default = []
unstable = []
guards = ["tower", "futures-core", "pin-project-lite"]
serde = ["dep:serde", "dep:serde_json"]
auto-vary = ["futures", "tokio", "tower"]
[dependencies]
axum-core = "0.5"
http = { version = "1", default-features = false }
# Optional dependencies required for the `guards` feature.
tower = { version = "0.5", default-features = false, optional = true }
futures-core = { version = "0.3", optional = true }
pin-project-lite = { version = "0.2", optional = true }
# Optional dependencies required for the `serde` feature.
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
# Optional dependencies required for the `auto-vary` feature.
tokio = { version = "1", features = ["sync"], optional = true }
futures = { version = "0.3", default-features = false, features = [
"alloc",
], optional = true }
[dev-dependencies]
axum = { version = "0.8", default-features = false }
axum-test = "18"
tokio = { version = "1", features = ["full"] }
tokio-test = "0.4"
[package.metadata.docs.rs]
all-features = true
================================================
FILE: LICENSE-APACHE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: LICENSE-MIT
================================================
Copyright 2023 Rob Wagner
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# axum-htmx
<!-- markdownlint-disable -->
<div align="right">
<a href="https://crates.io/crates/axum-htmx">
<img src="https://img.shields.io/crates/v/axum-htmx?style=flat-square" alt="crates.io badge">
</a>
<a href="https://docs.rs/axum-htmx/latest/">
<img src="https://img.shields.io/docsrs/axum-htmx?style=flat-square" alt="docs.rs badge">
</a>
</div>
<br>
<!-- markdownlint-enable -->
`axum-htmx` is a small extension library providing extractors, responders, and
request guards for [htmx](https://htmx.org/) headers within
[axum](https://github.com/tokio-rs/axum).
## Table of Contents
- [Getting Started](#getting-started)
- [Extractors](#extractors)
- [Responders](#responders)
- [Vary Responders](#vary-responders)
- [Auto Caching Management](#auto-caching-management)
- [Request Guards](#request-guards)
- [Examples](#examples)
- [Example: Extractors](#example-extractors)
- [Example: Responders](#example-responders)
- [Example: Router Guard](#example-router-guard)
- [Feature Flags](#feature-flags)
- [Contributing](#contributing)
- [Testing](#testing)
- [License](#license)
## Getting Started
Run `cargo add axum-htmx` to add the library to your project.
## Extractors
All of the [htmx request headers](https://htmx.org/reference/#request_headers)
have a supported extractor. Extractors are infallible, meaning they will always
succeed and never return an error. In the case where a header is not present,
the extractor will return `None` or `false` dependant on the expected return
type.
| Header | Extractor | Value |
|------------------------------|---------------------------|---------------------------|
| `HX-Boosted` | `HxBoosted` | `bool` |
| `HX-Current-URL` | `HxCurrentUrl` | `Option<axum::http::Uri>` |
| `HX-History-Restore-Request` | `HxHistoryRestoreRequest` | `bool` |
| `HX-Prompt` | `HxPrompt` | `Option<String>` |
| `HX-Request` | `HxRequest` | `bool` |
| `HX-Target` | `HxTarget` | `Option<String>` |
| `HX-Trigger-Name` | `HxTriggerName` | `Option<String>` |
| `HX-Trigger` | `HxTrigger` | `Option<String>` |
## Responders
All of the [htmx response headers](https://htmx.org/reference/#response_headers)
have a supported responder. A responder is a basic type that implements
`IntoResponseParts`, allowing you to simply and safely apply the HX-* headers to
any of your responses.
| Header | Responder | Value |
|---------------------------|---------------------|-------------------------------------|
| `HX-Location` | `HxLocation` | `String` |
| `HX-Push-Url` | `HxPushUrl` | `String` |
| `HX-Redirect` | `HxRedirect` | `String` |
| `HX-Refresh` | `HxRefresh` | `bool` |
| `HX-Replace-Url` | `HxReplaceUrl` | `String` |
| `HX-Reswap` | `HxReswap` | `axum_htmx::responders::SwapOption` |
| `HX-Retarget` | `HxRetarget` | `String` |
| `HX-Reselect` | `HxReselect` | `String` |
| `HX-Trigger` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
| `HX-Trigger-After-Settle` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
| `HX-Trigger-After-Swap` | `HxResponseTrigger` | `axum_htmx::serde::HxEvent` |
### Vary Responders
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][htmx-caching] for details.
| Header | Responder |
|-------------------------|---------------------|
| `Vary: HX-Request` | `VaryHxRequest` |
| `Vary: HX-Target` | `VaryHxTarget` |
| `Vary: HX-Trigger` | `VaryHxTrigger` |
| `Vary: HX-Trigger-Name` | `VaryHxTriggerName` |
Look at the [Auto Caching Management](#auto-caching-management) section for
automatic `Vary` headers management.
## Auto Caching Management
__Requires feature `auto-vary`.__
Manual use of [Vary Reponders](#vary-responders) adds fragility to the code,
because of the need to manually control correspondence between used extractors
and the responders.
We provide a [middleware](crate::AutoVaryLayer) to address this issue by
automatically adding `Vary` headers when corresponding extractors are used.
For example, on extracting [`HxRequest`], the middleware automatically adds
`Vary: hx-request` header to the response.
Look at the usage [example][auto-vary-example].
## Request Guards
__Requires feature `guards`.__
In addition to the extractors, there is also a route-wide layer request guard
for the `HX-Request` header. This will redirect any requests without the header
to "/" by default.
_It should be noted that this is NOT a replacement for an auth guard. A user can
trivially set the `HX-Request` header themselves. This is merely a convenience
for preventing users from receiving partial responses without context. If you
need to secure an endpoint you should be using a proper auth system._
## Examples
### Example: Extractors
In this example, we'll look for the `HX-Boosted` header, which is set when
applying the [hx-boost](https://htmx.org/attributes/hx-boost/) attribute to an
element. In our case, we'll use it to determine what kind of response we send.
When is this useful? When using a templating engine, like
[minijinja](https://github.com/mitsuhiko/minijinja), it is common to extend
different templates from a `_base.html` template. However, htmx works by sending
partial responses, so extending our `_base.html` would result in lots of extra
data being sent over the wire.
If we wanted to swap between pages, we would need to support both full template
responses and partial responses _(as the page can be accessed directly or
through a boosted anchor)_, so we look for the `HX-Boosted` header and extend
from a `_partial.html` template instead.
```rust
use axum::response::IntoResponse;
use axum_htmx::HxBoosted;
async fn get_index(HxBoosted(boosted): HxBoosted) -> impl IntoResponse {
if boosted {
// Send a template extending from _partial.html
} else {
// Send a template extending from _base.html
}
}
```
### Example: Responders
We can trigger any event being listened to by the DOM using an [htmx
trigger](https://htmx.org/attributes/hx-trigger/) header.
```rust
use axum_htmx::HxResponseTrigger;
// When we load our page, we will trigger any event listeners for "my-event.
async fn index() -> (HxResponseTrigger, &'static str) {
// Note: As HxResponseTrigger only implements `IntoResponseParts`, we must
// return our trigger first here.
(
HxResponseTrigger::normal(["my-event", "second-event"]),
"Hello, world!",
)
}
```
`htmx` also allows arbitrary data to be sent along with the event, which we can
use via the `serde` feature flag and the `HxEvent` type.
```rust
use serde_json::json;
// Note that we are using `HxResponseTrigger` from the `axum_htmx::serde` module
// instead of the root module.
use axum_htmx::{HxEvent, HxResponseTrigger};
async fn index() -> (HxResponseTrigger, &'static str) {
let event = HxEvent::new_with_data(
"my-event",
// May be any object that implements `serde::Serialize`
json!({"level": "info", "message": {
"title": "Hello, world!",
"body": "This is a test message.",
}}),
)
.unwrap();
// Note: As HxResponseTrigger only implements `IntoResponseParts`, we must
// return our trigger first here.
(HxResponseTrigger::normal([event]), "Hello, world!")
}
```
### Example: Router Guard
```rust
use axum::Router;
use axum_htmx::HxRequestGuardLayer;
fn router_one() -> Router {
Router::new()
// Redirects to "/" if the HX-Request header is not present
.layer(HxRequestGuardLayer::default())
}
fn router_two() -> Router {
Router::new()
.layer(HxRequestGuardLayer::new("/redirect-to-this-route"))
}
```
## Feature Flags
<!-- markdownlint-disable -->
| Flag | Default | Description | Dependencies |
|-------------|----------|------------------------------------------------------------|---------------------------------------------|
| `auto-vary` | Disabled | A middleware to address [htmx caching issue][htmx-caching] | `futures`, `tokio`, `tower` |
| `guards` | Disabled | Adds request guard layers. | `tower`, `futures-core`, `pin-project-lite` |
| `serde` | Disabled | Adds serde support for the `HxEvent` and `LocationOptions` | `serde`, `serde_json` |
<!-- markdownlint-enable -->
## Contributing
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
open an issue first so we're all on the same page!
### Testing
```sh
cargo +nightly test --all-features
```
## License
`axum-htmx` is dual-licensed under either
- **[MIT License](/LICENSE-MIT)**
- **[Apache License, Version 2.0](/LICENSE-APACHE)**
at your option.
[htmx-caching]: https://htmx.org/docs/#caching
[auto-vary-example]: https://github.com/robertwayne/axum-htmx/blob/main/examples/auto-vary.rs
================================================
FILE: examples/auto-vary.rs
================================================
//! Using `auto-vary` middleware
//!
//! Don't forget about the feature while running it:
//! `cargo run --features auto-vary --example auto-vary`
use std::time::Duration;
use axum::{Router, response::Html, routing::get, serve};
use axum_htmx::{AutoVaryLayer, HxRequest};
use tokio::{net::TcpListener, time::sleep};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler))
// Add the middleware
.layer(AutoVaryLayer);
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
serve(listener, app).await.unwrap();
}
// Our handler differentiates full-page GET requests from htmx-based ones by looking at the `hx-request`
// requestheader.
//
// The middleware sees the usage of the `HxRequest` extractor and automatically adds the
// `Vary: hx-request` response header.
async fn handler(HxRequest(hx_request): HxRequest) -> Html<&'static str> {
if hx_request {
// For htmx-based GET request, it returns a partial page update
sleep(Duration::from_secs(3)).await;
return Html("htmx response");
}
// While for a normal GET request, it returns the whole page
Html(
r#"
<script src="https://unpkg.com/htmx.org@1"></script>
<p hx-get="/" hx-trigger="load">Loading ...</p>
"#,
)
}
================================================
FILE: rustfmt.toml
================================================
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
reorder_imports = true
================================================
FILE: src/auto_vary.rs
================================================
//! A middleware to automatically add a `Vary` header when needed to address
//! [htmx caching issue](https://htmx.org/docs/#caching)
use std::{
sync::Arc,
task::{Context, Poll},
};
use axum_core::{
extract::Request,
response::{IntoResponse, Response},
};
use futures::future::{BoxFuture, join_all};
use http::{
Extensions,
header::{HeaderValue, VARY},
};
use tokio::sync::oneshot::{self, Receiver, Sender};
use tower::{Layer, Service};
use crate::{
HxError,
headers::{HX_REQUEST_STR, HX_TARGET_STR, HX_TRIGGER_NAME_STR, HX_TRIGGER_STR},
};
#[cfg(doc)]
use crate::{HxRequest, 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`]
/// or their combination is used.
#[derive(Clone)]
pub struct AutoVaryLayer;
/// Tower service for [`AutoVaryLayer`]
#[derive(Clone)]
pub struct AutoVaryMiddleware<S> {
inner: S,
}
pub(crate) trait Notifier {
fn sender(&mut self) -> Option<Sender<()>>;
fn notify(&mut self) {
if let Some(sender) = self.sender() {
sender.send(()).ok();
}
}
fn insert(extensions: &mut Extensions) -> Receiver<()>;
}
macro_rules! define_notifiers {
($($name:ident),*) => {
$(
#[derive(Clone)]
pub(crate) struct $name(Option<Arc<Sender<()>>>);
impl Notifier for $name {
fn sender(&mut self) -> Option<Sender<()>> {
self.0.take().and_then(Arc::into_inner)
}
fn insert(extensions: &mut Extensions) -> Receiver<()> {
let (tx, rx) = oneshot::channel();
if extensions.insert(Self(Some(Arc::new(tx)))).is_some() {
panic!("{}", MIDDLEWARE_DOUBLE_USE);
}
rx
}
}
)*
}
}
define_notifiers!(
HxRequestExtracted,
HxTargetExtracted,
HxTriggerExtracted,
HxTriggerNameExtracted
);
impl<S> Layer<S> for AutoVaryLayer {
type Service = AutoVaryMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
AutoVaryMiddleware { inner }
}
}
impl<S> Service<Request> for AutoVaryMiddleware<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut request: Request) -> Self::Future {
let exts = request.extensions_mut();
let rx_header = [
(HxRequestExtracted::insert(exts), HX_REQUEST_STR),
(HxTargetExtracted::insert(exts), HX_TARGET_STR),
(HxTriggerExtracted::insert(exts), HX_TRIGGER_STR),
(HxTriggerNameExtracted::insert(exts), HX_TRIGGER_NAME_STR),
];
let future = self.inner.call(request);
Box::pin(async move {
let mut response: Response = future.await?;
let used_headers: Vec<_> = join_all(
rx_header
.into_iter()
.map(|(rx, header)| async move { rx.await.ok().map(|_| header) }),
)
.await
.into_iter()
.flatten()
.collect();
if used_headers.is_empty() {
return Ok(response);
}
let value = match HeaderValue::from_str(&used_headers.join(", ")) {
Ok(x) => x,
Err(e) => return Ok(HxError::from(e).into_response()),
};
if let Err(e) = response.headers_mut().try_append(VARY, value) {
return Ok(HxError::from(e).into_response());
}
Ok(response)
})
}
}
#[cfg(test)]
mod tests {
use axum::{Router, routing::get};
use super::*;
use crate::{HxRequest, HxTarget, HxTrigger, HxTriggerName};
fn vary_headers(resp: &axum_test::TestResponse) -> Vec<HeaderValue> {
resp.iter_headers_by_name("vary").cloned().collect()
}
fn server() -> axum_test::TestServer {
let app = Router::new()
.route("/no-extractors", get(|| async { () }))
.route("/hx-request", get(|_: HxRequest| async { () }))
.route("/hx-target", get(|_: HxTarget| async { () }))
.route("/hx-trigger", get(|_: HxTrigger| async { () }))
.route("/hx-trigger-name", get(|_: HxTriggerName| async { () }))
.route(
"/repeated-extractor",
get(|_: HxRequest, _: HxRequest| async { () }),
)
.route(
"/multiple-extractors",
get(|_: HxRequest, _: HxTarget, _: HxTrigger, _: HxTriggerName| async { () }),
)
.layer(AutoVaryLayer);
axum_test::TestServer::new(app).unwrap()
}
#[tokio::test]
async fn no_extractors() {
assert!(vary_headers(&server().get("/no-extractors").await).is_empty());
}
#[tokio::test]
async fn single_hx_request() {
assert_eq!(
vary_headers(&server().get("/hx-request").await),
["hx-request"]
);
}
#[tokio::test]
async fn single_hx_target() {
assert_eq!(
vary_headers(&server().get("/hx-target").await),
["hx-target"]
);
}
#[tokio::test]
async fn single_hx_trigger() {
assert_eq!(
vary_headers(&server().get("/hx-trigger").await),
["hx-trigger"]
);
}
#[tokio::test]
async fn single_hx_trigger_name() {
assert_eq!(
vary_headers(&server().get("/hx-trigger-name").await),
["hx-trigger-name"]
);
}
#[tokio::test]
async fn repeated_extractor() {
assert_eq!(
vary_headers(&server().get("/repeated-extractor").await),
["hx-request"]
);
}
// Extractors can be used multiple times e.g. in middlewares
#[tokio::test]
async fn multiple_extractors() {
assert_eq!(
vary_headers(&server().get("/multiple-extractors").await),
["hx-request, hx-target, hx-trigger, hx-trigger-name"],
);
}
}
================================================
FILE: src/error.rs
================================================
use std::{error, fmt};
use axum_core::response::IntoResponse;
use http::{
StatusCode,
header::{InvalidHeaderValue, MaxSizeReached},
};
#[derive(Debug)]
pub enum HxError {
InvalidHeaderValue(InvalidHeaderValue),
TooManyResponseHeaders(MaxSizeReached),
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
Json(serde_json::Error),
}
impl From<InvalidHeaderValue> for HxError {
fn from(value: InvalidHeaderValue) -> Self {
Self::InvalidHeaderValue(value)
}
}
impl From<MaxSizeReached> for HxError {
fn from(value: MaxSizeReached) -> Self {
Self::TooManyResponseHeaders(value)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
impl From<serde_json::Error> for HxError {
fn from(value: serde_json::Error) -> Self {
Self::Json(value)
}
}
impl fmt::Display for HxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HxError::InvalidHeaderValue(_) => write!(f, "Invalid header value"),
HxError::TooManyResponseHeaders(_) => write!(f, "Too many response headers"),
#[cfg(feature = "serde")]
HxError::Json(_) => write!(f, "Json"),
}
}
}
impl error::Error for HxError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
HxError::InvalidHeaderValue(e) => Some(e),
HxError::TooManyResponseHeaders(e) => Some(e),
#[cfg(feature = "serde")]
HxError::Json(e) => Some(e),
}
}
}
impl IntoResponse for HxError {
fn into_response(self) -> axum_core::response::Response {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
}
================================================
FILE: src/extractors.rs
================================================
//! Axum extractors for htmx request headers.
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,
};
/// The `HX-Boosted` header.
///
/// This is set when a request is made from an element where its parent has the
/// `hx-boost` attribute set to `true`.
///
/// This extractor will always return a value. If the header is not present, it
/// will return `false`.
///
/// See <https://htmx.org/attributes/hx-boost/> for more information.
#[derive(Debug, Clone, Copy)]
pub struct HxBoosted(pub bool);
impl<S> FromRequestParts<S> for HxBoosted
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
if parts.headers.contains_key(HX_BOOSTED) {
Ok(HxBoosted(true))
} else {
Ok(HxBoosted(false))
}
}
}
/// The `HX-Current-Url` header.
///
/// This is set on every request made by htmx itself. As its name implies, it
/// just contains the current url.
///
/// This extractor will always return a value. If the header is not present, or
/// extractor fails to parse the url it will return `None`.
#[derive(Debug, Clone)]
pub struct HxCurrentUrl(pub Option<http::Uri>);
impl<S> FromRequestParts<S> for HxCurrentUrl
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
if let Some(url) = parts.headers.get(HX_CURRENT_URL) {
let url = url
.to_str()
.ok()
.and_then(|url| url.parse::<http::Uri>().ok());
return Ok(HxCurrentUrl(url));
}
Ok(HxCurrentUrl(None))
}
}
/// The `HX-History-Restore-Request` header.
///
/// This extractor will always return a value. If the header is not present, it
/// will return `false`.
#[derive(Debug, Clone, Copy)]
pub struct HxHistoryRestoreRequest(pub bool);
impl<S> FromRequestParts<S> for HxHistoryRestoreRequest
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
if parts.headers.contains_key(HX_HISTORY_RESTORE_REQUEST) {
Ok(HxHistoryRestoreRequest(true))
} else {
Ok(HxHistoryRestoreRequest(false))
}
}
}
/// The `HX-Prompt` header.
///
/// This is set when a request is made from an element that has the `hx-prompt`
/// attribute set. The value will contain the string input by the user.
///
/// This extractor will always return a value. If the header is not present, it
/// will return `None`.
#[derive(Debug, Clone)]
pub struct HxPrompt(pub Option<String>);
impl<S> FromRequestParts<S> for HxPrompt
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
if let Some(prompt) = parts.headers.get(HX_PROMPT) {
if let Ok(prompt) = prompt.to_str() {
return Ok(HxPrompt(Some(prompt.to_string())));
}
}
Ok(HxPrompt(None))
}
}
/// The `HX-Request` header.
///
/// This is set on every request made by htmx itself. It won't be present on
/// requests made manually, or by other libraries.
///
/// This extractor will always return a value. If the header is not present, it
/// will return `false`.
#[derive(Debug, Clone, Copy)]
pub struct HxRequest(pub bool);
impl<S> FromRequestParts<S> for HxRequest
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::HxRequestExtracted>()
.map(crate::auto_vary::Notifier::notify);
if parts.headers.contains_key(HX_REQUEST) {
Ok(HxRequest(true))
} else {
Ok(HxRequest(false))
}
}
}
/// The `HX-Target` header.
///
/// This is set when a request is made from an element that has the `hx-target`
/// attribute set. The value will contain the target 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 HxTarget(pub Option<String>);
impl<S> FromRequestParts<S> for HxTarget
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::HxTargetExtracted>()
.map(crate::auto_vary::Notifier::notify);
if let Some(target) = parts.headers.get(HX_TARGET) {
if let Ok(target) = target.to_str() {
return Ok(HxTarget(Some(target.to_string())));
}
}
Ok(HxTarget(None))
}
}
/// The `HX-Trigger-Name` header.
///
/// This is set when a request is made from an element that has the `hx-trigger`
/// attribute set. The value will contain the trigger 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 HxTriggerName(pub Option<String>);
impl<S> FromRequestParts<S> for HxTriggerName
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::HxTriggerNameExtracted>()
.map(crate::auto_vary::Notifier::notify);
if let Some(trigger_name) = parts.headers.get(HX_TRIGGER_NAME) {
if let Ok(trigger_name) = trigger_name.to_str() {
return Ok(HxTriggerName(Some(trigger_name.to_string())));
}
}
Ok(HxTriggerName(None))
}
}
/// The `HX-Trigger` header.
///
/// This is set when a request is made from an element that has the `hx-trigger`
/// attribute set. The value will contain the trigger 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 HxTrigger(pub Option<String>);
impl<S> FromRequestParts<S> for HxTrigger
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::HxTriggerExtracted>()
.map(crate::auto_vary::Notifier::notify);
if let Some(trigger) = parts.headers.get(HX_TRIGGER) {
if let Ok(trigger) = trigger.to_str() {
return Ok(HxTrigger(Some(trigger.to_string())));
}
}
Ok(HxTrigger(None))
}
}
================================================
FILE: src/guard.rs
================================================
//! Request guard for protecting a router against non-htmx requests.
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use futures_core::ready;
use http::{Request, StatusCode, header::LOCATION, response::Response};
use pin_project_lite::pin_project;
use tower::{Layer, Service};
use crate::HX_REQUEST;
/// Checks if the request contains the `HX-Request` header, redirecting to the
/// given location if not.
///
/// This can be useful for preventing users from accidently ending up on a route
/// which would otherwise return only partial HTML data.
#[derive(Debug, Clone)]
pub struct HxRequestGuardLayer<'a> {
redirect_to: &'a str,
}
impl<'a> HxRequestGuardLayer<'a> {
pub fn new(redirect_to: &'a str) -> Self {
Self { redirect_to }
}
}
impl Default for HxRequestGuardLayer<'_> {
fn default() -> Self {
Self { redirect_to: "/" }
}
}
impl<'a, S> Layer<S> for HxRequestGuardLayer<'a> {
type Service = HxRequestGuard<'a, S>;
fn layer(&self, inner: S) -> Self::Service {
HxRequestGuard {
inner,
hx_request: false,
layer: self.clone(),
}
}
}
/// Tower service that implements redirecting to non-partial routes.
#[derive(Debug, Clone)]
pub struct HxRequestGuard<'a, S> {
inner: S,
hx_request: bool,
layer: HxRequestGuardLayer<'a>,
}
impl<'a, S, T, U> Service<Request<T>> for HxRequestGuard<'a, S>
where
S: Service<Request<T>, Response = Response<U>>,
U: Default,
{
type Response = S::Response;
type Error = S::Error;
type Future = private::ResponseFuture<'a, S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<T>) -> Self::Future {
// This will always contain a "true" value.
if req.headers().contains_key(HX_REQUEST) {
self.hx_request = true;
}
let response_future = self.inner.call(req);
private::ResponseFuture {
response_future,
hx_request: self.hx_request,
layer: self.layer.clone(),
}
}
}
mod private {
use super::*;
pin_project! {
pub struct ResponseFuture<'a, F> {
#[pin]
pub(super) response_future: F,
pub(super) hx_request: bool,
pub(super) layer: HxRequestGuardLayer<'a>,
}
}
impl<F, B, E> Future for ResponseFuture<'_, F>
where
F: Future<Output = Result<Response<B>, E>>,
B: Default,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let response: Response<B> = ready!(this.response_future.poll(cx))?;
match *this.hx_request {
true => Poll::Ready(Ok(response)),
false => {
let res = Response::builder()
.status(StatusCode::SEE_OTHER)
.header(LOCATION, this.layer.redirect_to)
.body(B::default())
.expect("failed to build response");
Poll::Ready(Ok(res))
}
}
}
}
}
================================================
FILE: src/headers.rs
================================================
//! HTTP headers used by htmx.
use http::HeaderName;
/// Indicates that the request is via an element using `hx-boost` attribute.
///
/// See <https://htmx.org/attributes/hx-boost/> for more information.
pub const HX_BOOSTED: HeaderName = HeaderName::from_static("hx-boosted");
/// The current URL of the browser.
pub const HX_CURRENT_URL: HeaderName = HeaderName::from_static("hx-current-url");
/// `true` if the request is for history restoration after a miss in the local
/// history cache.
pub const HX_HISTORY_RESTORE_REQUEST: HeaderName =
HeaderName::from_static("hx-history-restore-request");
/// The user response to an `hx-prompt`
///
/// See <https://htmx.org/attributes/hx-prompt/> for more information.
pub const HX_PROMPT: HeaderName = HeaderName::from_static("hx-prompt");
pub(crate) const HX_REQUEST_STR: &str = "hx-request";
/// Always `true`.
pub const HX_REQUEST: HeaderName = HeaderName::from_static(HX_REQUEST_STR);
pub(crate) const HX_TARGET_STR: &str = "hx-target";
/// The `id` of the target element, if it exists.
pub const HX_TARGET: HeaderName = HeaderName::from_static(HX_TARGET_STR);
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);
/// 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");
/// Pushes a new URL onto the history stack.
pub const HX_PUSH_URL: HeaderName = HeaderName::from_static("hx-push-url");
/// Can be used to do a client-side redirect to a new location.
pub const HX_REDIRECT: HeaderName = HeaderName::from_static("hx-redirect");
/// If set to `true`, the client will do a full refresh on the page.
pub const HX_REFRESH: HeaderName = HeaderName::from_static("hx-refresh");
/// Replaces the currelt URL in the location bar.
pub const HX_REPLACE_URL: HeaderName = HeaderName::from_static("hx-replace-url");
/// Allows you to specify how the response value will be swapped.
///
/// See <https://htmx.org/attributes/hx-swap/> for more information.
pub const HX_RESWAP: HeaderName = HeaderName::from_static("hx-reswap");
/// A CSS selector that update the target of the content update to a different
/// element on the page.
pub const HX_RETARGET: HeaderName = HeaderName::from_static("hx-retarget");
/// A CSS selector that allows you to choose which part of the response is used
/// to be swapped in. Overrides an existing `hx-select` on the triggering
/// element
pub const HX_RESELECT: HeaderName = HeaderName::from_static("hx-reselect");
pub(crate) const HX_TRIGGER_STR: &str = "hx-trigger";
/// Can be set as a request or response header.
///
/// In a request, it contains the `id` of the element that triggered the
/// request.
///
/// In a response, it can be used to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
pub const HX_TRIGGER: HeaderName = HeaderName::from_static(HX_TRIGGER_STR);
/// Allows you to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
pub const HX_TRIGGER_AFTER_SETTLE: HeaderName = HeaderName::from_static("hx-trigger-after-settle");
/// Allows you to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
pub const HX_TRIGGER_AFTER_SWAP: HeaderName = HeaderName::from_static("hx-trigger-after-swap");
================================================
FILE: src/lib.rs
================================================
#![cfg_attr(feature = "unstable", feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![allow(clippy::too_long_first_doc_paragraph)]
mod error;
pub use error::*;
#[cfg(feature = "auto-vary")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "auto-vary")))]
pub mod auto_vary;
pub mod extractors;
#[cfg(feature = "guards")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "guards")))]
pub mod guard;
pub mod headers;
pub mod responders;
#[cfg(feature = "auto-vary")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "auto-vary")))]
#[doc(inline)]
pub use auto_vary::*;
#[doc(inline)]
pub use extractors::*;
#[cfg(feature = "guards")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "guards")))]
#[doc(inline)]
pub use guard::*;
#[doc(inline)]
pub use headers::*;
#[doc(inline)]
pub use responders::*;
================================================
FILE: src/responders/location.rs
================================================
use axum_core::response::{IntoResponseParts, ResponseParts};
use http::HeaderValue;
use crate::{HxError, headers};
/// The `HX-Location` header.
///
/// This response header can be used to trigger a client side redirection
/// without reloading the whole page. If you intend to redirect to a specific
/// target on the page, you must enable the `serde` feature flag and specify
/// [`LocationOptions`].
///
/// Will fail if the supplied uri contains characters that are not visible ASCII
/// (32-127).
///
/// See <https://htmx.org/headers/hx-location/> for more information.
#[derive(Debug, Clone)]
pub struct HxLocation {
/// Uri of the new location.
pub uri: String,
/// Extra options.
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub options: LocationOptions,
}
impl HxLocation {
/// Parses `uri` and sets it as location.
#[allow(clippy::should_implement_trait)]
pub fn from_str(uri: impl AsRef<str>) -> Self {
Self {
#[cfg(feature = "serde")]
options: LocationOptions::default(),
uri: uri.as_ref().to_string(),
}
}
/// Parses `uri` and sets it as location with additional options.
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub fn from_str_with_options(uri: impl AsRef<str>, options: LocationOptions) -> Self {
Self {
options,
uri: uri.as_ref().to_string(),
}
}
#[cfg(feature = "serde")]
fn into_header_with_options(self) -> Result<String, HxError> {
if self.options.is_default() {
return Ok(self.uri.to_string());
}
#[derive(::serde::Serialize)]
struct LocWithOpts {
path: String,
#[serde(flatten)]
opts: LocationOptions,
}
let loc_with_opts = LocWithOpts {
path: self.uri.to_string(),
opts: self.options,
};
Ok(serde_json::to_string(&loc_with_opts)?)
}
}
impl<'a> From<&'a str> for HxLocation {
fn from(uri: &'a str) -> Self {
Self::from_str(uri)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
impl<'a> From<(&'a str, LocationOptions)> for HxLocation {
fn from((uri, options): (&'a str, LocationOptions)) -> Self {
Self::from_str_with_options(uri, options)
}
}
impl IntoResponseParts for HxLocation {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
#[cfg(feature = "serde")]
let header = self.into_header_with_options()?;
#[cfg(not(feature = "serde"))]
let header = self.uri.to_string();
res.headers_mut().insert(
headers::HX_LOCATION,
HeaderValue::from_maybe_shared(header)?,
);
Ok(res)
}
}
/// More options for `HX-Location` header.
///
/// - `source` - the source element of the request
/// - `event` - an event that “triggered” the request
/// - `handler` - a callback that will handle the response HTML
/// - `target` - the target to swap the response into
/// - `swap` - how the response will be swapped in relative to the target
/// - `values` - values to submit with the request
/// - `headers` - headers to submit with the request
/// - `select` - allows you to select the content you want swapped from a
/// response
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
#[derive(Debug, Clone, serde::Serialize, Default)]
pub struct LocationOptions {
/// The source element of the request.
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
/// An event that "triggered" the request.
#[serde(skip_serializing_if = "Option::is_none")]
pub event: Option<String>,
/// A callback that will handle the response HTML.
#[serde(skip_serializing_if = "Option::is_none")]
pub handler: Option<String>,
/// The target to swap the response into.
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
/// How the response will be swapped in relative to the target.
#[serde(skip_serializing_if = "Option::is_none")]
pub swap: Option<crate::SwapOption>,
/// Values to submit with the request.
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<serde_json::Value>,
/// Headers to submit with the request.
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<serde_json::Value>,
// Hacky way of making this struct non-exhaustive.
// See <https://rust-lang.github.io/rfcs/2008-non-exhaustive.html> and <https://github.com/robertwayne/axum-htmx/issues/29> for reasoning.
#[serde(skip)]
pub non_exhaustive: (),
}
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
impl LocationOptions {
pub(super) fn is_default(&self) -> bool {
let Self {
source: None,
event: None,
handler: None,
target: None,
swap: None,
values: None,
headers: None,
non_exhaustive: (),
} = self
else {
return false;
};
true
}
}
#[cfg(test)]
#[cfg(feature = "serde")]
mod tests {
use super::*;
#[test]
fn test_serialize_location() {
use crate::SwapOption;
let loc = HxLocation::from("/foo");
assert_eq!(loc.into_header_with_options().unwrap(), "/foo");
let loc = HxLocation::from_str_with_options(
"/foo",
LocationOptions {
event: Some("click".into()),
swap: Some(SwapOption::InnerHtml),
..Default::default()
},
);
assert_eq!(
loc.into_header_with_options().unwrap(),
r#"{"path":"/foo","event":"click","swap":"innerHTML"}"#
);
}
}
================================================
FILE: src/responders/trigger.rs
================================================
use axum_core::response::{IntoResponseParts, ResponseParts};
use crate::{HxError, headers};
/// Represents a client-side event carrying optional data.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct HxEvent {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub data: Option<serde_json::Value>,
}
impl HxEvent {
/// Creates new event with no associated data.
pub fn new(name: impl AsRef<str>) -> Self {
Self {
name: name.as_ref().to_owned(),
#[cfg(feature = "serde")]
data: None,
}
}
/// Creates new event with data.
#[cfg(feature = "serde")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "serde")))]
pub fn new_with_data<T: ::serde::Serialize>(
name: impl AsRef<str>,
data: T,
) -> Result<Self, serde_json::Error> {
let data = serde_json::to_value(data)?;
Ok(Self {
name: name.as_ref().to_owned(),
#[cfg(feature = "serde")]
data: Some(data),
})
}
}
impl<N: AsRef<str>> From<N> for HxEvent {
fn from(name: N) -> Self {
Self {
name: name.as_ref().to_owned(),
#[cfg(feature = "serde")]
data: None,
}
}
}
#[cfg(not(feature = "serde"))]
fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderValue, HxError> {
let header = events
.into_iter()
.map(|HxEvent { name }| name)
.collect::<Vec<_>>()
.join(", ");
http::HeaderValue::from_str(&header).map_err(Into::into)
}
#[cfg(feature = "serde")]
fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderValue, HxError> {
use std::collections::HashMap;
use http::HeaderValue;
use serde_json::Value;
let with_data = events.iter().any(|e| e.data.is_some());
let header_value = if with_data {
// at least one event contains data so the header_value needs to be json
// encoded.
let header_value = events
.into_iter()
.map(|e| (e.name, e.data.unwrap_or_default()))
.collect::<HashMap<String, Value>>();
serde_json::to_string(&header_value)?
} else {
// no event contains data, the event names can be put in the header
// value separated by a comma.
events
.into_iter()
.map(|e| e.name)
.reduce(|acc, e| acc + ", " + &e)
.unwrap_or_default()
};
HeaderValue::from_maybe_shared(header_value).map_err(HxError::from)
}
/// Describes when should event be triggered.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TriggerMode {
Normal,
AfterSettle,
AfterSwap,
}
/// The `HX-Trigger*` header.
///
/// Allows you to trigger client-side events. Corresponds to `HX-Trigger`,
/// `HX-Trigger-After-Settle` and `HX-Trigger-After-Swap` headers. To change
/// when events trigger use appropriate `mode`.
///
/// Will fail if the supplied events contain or produce characters that are not
/// visible ASCII (32-127) when serializing to JSON.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
///
/// Note: An `HxResponseTrigger` implements `IntoResponseParts` and should be
/// used before any other response object would consume the response parts.
#[derive(Debug, Clone)]
pub struct HxResponseTrigger {
pub mode: TriggerMode,
pub events: Vec<HxEvent>,
}
impl HxResponseTrigger {
/// Creates new [trigger](https://htmx.org/headers/hx-trigger/) with
/// specified mode and events.
pub fn new<T: Into<HxEvent>>(mode: TriggerMode, events: impl IntoIterator<Item = T>) -> Self {
Self {
mode,
events: events.into_iter().map(Into::into).collect(),
}
}
/// Creates new [normal](https://htmx.org/headers/hx-trigger/) trigger from
/// events.
///
/// See `HxResponseTrigger` for more information.
pub fn normal<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -> Self {
Self::new(TriggerMode::Normal, events)
}
/// Creates new [after settle](https://htmx.org/headers/hx-trigger/) trigger
/// from events.
///
/// See `HxResponseTrigger` for more information.
pub fn after_settle<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -> Self {
Self::new(TriggerMode::AfterSettle, events)
}
/// Creates new [after swap](https://htmx.org/headers/hx-trigger/) trigger
/// from events.
///
/// See `HxResponseTrigger` for more information.
pub fn after_swap<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -> Self {
Self::new(TriggerMode::AfterSwap, events)
}
}
impl<T> From<(TriggerMode, T)> for HxResponseTrigger
where
T: IntoIterator,
T::Item: Into<HxEvent>,
{
fn from((mode, events): (TriggerMode, T)) -> Self {
Self {
mode,
events: events.into_iter().map(Into::into).collect(),
}
}
}
impl IntoResponseParts for HxResponseTrigger {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
if !self.events.is_empty() {
let header = match self.mode {
TriggerMode::Normal => headers::HX_TRIGGER,
TriggerMode::AfterSettle => headers::HX_TRIGGER_AFTER_SETTLE,
TriggerMode::AfterSwap => headers::HX_TRIGGER_AFTER_SETTLE,
};
res.headers_mut()
.insert(header, events_to_header_value(self.events)?);
}
Ok(res)
}
}
#[cfg(test)]
#[cfg(feature = "serde")]
mod tests {
use http::HeaderValue;
use serde_json::json;
use super::*;
#[test]
fn valid_event_to_header_encoding() {
let evt = HxEvent::new_with_data(
"my-event",
json!({"level": "info", "message": {
"body": "This is a test message.",
"title": "Hello, world!",
}}),
)
.unwrap();
let header_value = events_to_header_value(vec![evt]).unwrap();
let expected_value = r#"{"my-event":{"level":"info","message":{"body":"This is a test message.","title":"Hello, world!"}}}"#;
assert_eq!(header_value, HeaderValue::from_static(expected_value));
let value =
events_to_header_value(HxResponseTrigger::normal(["foo", "bar"]).events).unwrap();
assert_eq!(value, HeaderValue::from_static("foo, bar"));
}
}
================================================
FILE: src/responders/vary.rs
================================================
use axum_core::response::{IntoResponseParts, ResponseParts};
use http::header::{HeaderValue, VARY};
use crate::{HxError, extractors, headers};
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);
/// 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 std::collections::hash_set::HashSet;
use axum::{Router, routing::get};
use super::*;
#[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]));
}
}
================================================
FILE: src/responders.rs
================================================
//! Axum responses for htmx response headers.
use std::convert::Infallible;
use axum_core::response::{IntoResponseParts, ResponseParts};
use http::HeaderValue;
use crate::{HxError, headers};
mod location;
pub use location::*;
mod trigger;
pub use trigger::*;
mod vary;
pub use vary::*;
const HX_SWAP_INNER_HTML: &str = "innerHTML";
const HX_SWAP_OUTER_HTML: &str = "outerHTML";
const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin";
const HX_SWAP_AFTER_BEGIN: &str = "afterbegin";
const HX_SWAP_BEFORE_END: &str = "beforeend";
const HX_SWAP_AFTER_END: &str = "afterend";
const HX_SWAP_DELETE: &str = "delete";
const HX_SWAP_NONE: &str = "none";
/// The `HX-Push-Url` header.
///
/// Pushes a new url into the history stack.
///
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
///
/// See <https://htmx.org/headers/hx-push-url/> for more information.
#[derive(Debug, Clone)]
pub struct HxPushUrl(pub String);
impl IntoResponseParts for HxPushUrl {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_PUSH_URL,
HeaderValue::from_maybe_shared(self.0)?,
);
Ok(res)
}
}
impl<'a> From<&'a str> for HxPushUrl {
fn from(value: &'a str) -> Self {
Self(value.to_string())
}
}
/// The `HX-Redirect` header.
///
/// Can be used to do a client-side redirect to a new location.
///
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
#[derive(Debug, Clone)]
pub struct HxRedirect(pub String);
impl IntoResponseParts for HxRedirect {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_REDIRECT,
HeaderValue::from_maybe_shared(self.0)?,
);
Ok(res)
}
}
impl<'a> From<&'a str> for HxRedirect {
fn from(value: &'a str) -> Self {
Self(value.to_string())
}
}
/// The `HX-Refresh`header.
///
/// If set to `true` the client-side will do a full refresh of the page.
///
/// This responder will never fail.
#[derive(Debug, Copy, Clone)]
pub struct HxRefresh(pub bool);
impl From<bool> for HxRefresh {
fn from(value: bool) -> Self {
Self(value)
}
}
impl IntoResponseParts for HxRefresh {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_REFRESH,
if self.0 {
HeaderValue::from_static("true")
} else {
HeaderValue::from_static("false")
},
);
Ok(res)
}
}
/// The `HX-Replace-Url` header.
///
/// Replaces the currelt URL in the location bar.
///
/// Will fail if the supplied Uri contains characters that are not visible ASCII
/// (32-127).
///
/// See <https://htmx.org/headers/hx-replace-url/> for more information.
#[derive(Debug, Clone)]
pub struct HxReplaceUrl(pub String);
impl IntoResponseParts for HxReplaceUrl {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_REPLACE_URL,
HeaderValue::from_maybe_shared(self.0)?,
);
Ok(res)
}
}
impl<'a> From<&'a str> for HxReplaceUrl {
fn from(value: &'a str) -> Self {
Self(value.to_string())
}
}
/// The `HX-Reswap` header.
///
/// Allows you to specidy how the response will be swapped.
///
/// This responder will never fail.
#[derive(Debug, Copy, Clone)]
pub struct HxReswap(pub SwapOption);
impl IntoResponseParts for HxReswap {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(headers::HX_RESWAP, self.0.into());
Ok(res)
}
}
impl From<SwapOption> for HxReswap {
fn from(value: SwapOption) -> Self {
Self(value)
}
}
/// The `HX-Retarget` header.
///
/// A CSS selector that updates the target of the content update to a different
/// element on the page.
///
/// Will fail if the supplied String contains characters that are not visible
/// ASCII (32-127).
#[derive(Debug, Clone)]
pub struct HxRetarget(pub String);
impl IntoResponseParts for HxRetarget {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_RETARGET,
HeaderValue::from_maybe_shared(self.0)?,
);
Ok(res)
}
}
impl<T: Into<String>> From<T> for HxRetarget {
fn from(value: T) -> Self {
Self(value.into())
}
}
/// The `HX-Reselect` header.
///
/// A CSS selector that allows you to choose which part of the response is used
/// to be swapped in. Overrides an existing hx-select on the triggering element.
///
/// Will fail if the supplied String contains characters that are not visible
/// ASCII (32-127).
#[derive(Debug, Clone)]
pub struct HxReselect(pub String);
impl IntoResponseParts for HxReselect {
type Error = HxError;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().insert(
headers::HX_RESELECT,
HeaderValue::from_maybe_shared(self.0)?,
);
Ok(res)
}
}
impl<T: Into<String>> From<T> for HxReselect {
fn from(value: T) -> Self {
Self(value.into())
}
}
/// Values of the `hx-swap` attribute.
// serde::Serialize is implemented in responders/serde.rs
#[derive(Debug, Copy, Clone)]
pub enum SwapOption {
/// Replace the inner html of the target element.
InnerHtml,
/// Replace the entire target element with the response.
OuterHtml,
/// Insert the response before the target element.
BeforeBegin,
/// Insert the response before the first child of the target element.
AfterBegin,
/// Insert the response after the last child of the target element
BeforeEnd,
/// Insert the response after the target element
AfterEnd,
/// Deletes the target element regardless of the response
Delete,
/// Does not append content from response (out of band items will still be
/// processed).
None,
}
// can be removed and automatically derived when
// https://github.com/serde-rs/serde/issues/2485 is implemented
#[cfg(feature = "serde")]
impl ::serde::Serialize for SwapOption {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
const UNIT_NAME: &str = "SwapOption";
match self {
Self::InnerHtml => serializer.serialize_unit_variant(UNIT_NAME, 0, HX_SWAP_INNER_HTML),
Self::OuterHtml => serializer.serialize_unit_variant(UNIT_NAME, 1, HX_SWAP_OUTER_HTML),
Self::BeforeBegin => {
serializer.serialize_unit_variant(UNIT_NAME, 2, HX_SWAP_BEFORE_BEGIN)
}
Self::AfterBegin => {
serializer.serialize_unit_variant(UNIT_NAME, 3, HX_SWAP_AFTER_BEGIN)
}
Self::BeforeEnd => serializer.serialize_unit_variant(UNIT_NAME, 4, HX_SWAP_BEFORE_END),
Self::AfterEnd => serializer.serialize_unit_variant(UNIT_NAME, 5, HX_SWAP_AFTER_END),
Self::Delete => serializer.serialize_unit_variant(UNIT_NAME, 6, HX_SWAP_DELETE),
Self::None => serializer.serialize_unit_variant(UNIT_NAME, 7, HX_SWAP_NONE),
}
}
}
impl From<SwapOption> for HeaderValue {
fn from(value: SwapOption) -> Self {
match value {
SwapOption::InnerHtml => HeaderValue::from_static(HX_SWAP_INNER_HTML),
SwapOption::OuterHtml => HeaderValue::from_static(HX_SWAP_OUTER_HTML),
SwapOption::BeforeBegin => HeaderValue::from_static(HX_SWAP_BEFORE_BEGIN),
SwapOption::AfterBegin => HeaderValue::from_static(HX_SWAP_AFTER_BEGIN),
SwapOption::BeforeEnd => HeaderValue::from_static(HX_SWAP_BEFORE_END),
SwapOption::AfterEnd => HeaderValue::from_static(HX_SWAP_AFTER_END),
SwapOption::Delete => HeaderValue::from_static(HX_SWAP_DELETE),
SwapOption::None => HeaderValue::from_static(HX_SWAP_NONE),
}
}
}
gitextract_pmy2ca9y/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── examples/
│ └── auto-vary.rs
├── rustfmt.toml
└── src/
├── auto_vary.rs
├── error.rs
├── extractors.rs
├── guard.rs
├── headers.rs
├── lib.rs
├── responders/
│ ├── location.rs
│ ├── trigger.rs
│ └── vary.rs
└── responders.rs
SYMBOL INDEX (178 symbols across 10 files)
FILE: examples/auto-vary.rs
function main (line 12) | async fn main() {
function handler (line 27) | async fn handler(HxRequest(hx_request): HxRequest) -> Html<&'static str> {
FILE: src/auto_vary.rs
constant MIDDLEWARE_DOUBLE_USE (line 28) | const MIDDLEWARE_DOUBLE_USE: &str =
type AutoVaryLayer (line 36) | pub struct AutoVaryLayer;
type Service (line 87) | type Service = AutoVaryMiddleware<S>;
method layer (line 89) | fn layer(&self, inner: S) -> Self::Service {
type AutoVaryMiddleware (line 40) | pub struct AutoVaryMiddleware<S> {
type Notifier (line 44) | pub(crate) trait Notifier {
method sender (line 45) | fn sender(&mut self) -> Option<Sender<()>>;
method notify (line 47) | fn notify(&mut self) {
method insert (line 53) | fn insert(extensions: &mut Extensions) -> Receiver<()>;
type Response (line 99) | type Response = S::Response;
type Error (line 100) | type Error = S::Error;
type Future (line 101) | type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
function poll_ready (line 103) | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::...
function call (line 107) | fn call(&mut self, mut request: Request) -> Self::Future {
function vary_headers (line 153) | fn vary_headers(resp: &axum_test::TestResponse) -> Vec<HeaderValue> {
function server (line 157) | fn server() -> axum_test::TestServer {
function no_extractors (line 177) | async fn no_extractors() {
function single_hx_request (line 182) | async fn single_hx_request() {
function single_hx_target (line 190) | async fn single_hx_target() {
function single_hx_trigger (line 198) | async fn single_hx_trigger() {
function single_hx_trigger_name (line 206) | async fn single_hx_trigger_name() {
function repeated_extractor (line 214) | async fn repeated_extractor() {
function multiple_extractors (line 223) | async fn multiple_extractors() {
FILE: src/error.rs
type HxError (line 10) | pub enum HxError {
method from (line 20) | fn from(value: InvalidHeaderValue) -> Self {
method from (line 26) | fn from(value: MaxSizeReached) -> Self {
method from (line 34) | fn from(value: serde_json::Error) -> Self {
method fmt (line 40) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method source (line 51) | fn source(&self) -> Option<&(dyn error::Error + 'static)> {
method into_response (line 62) | fn into_response(self) -> axum_core::response::Response {
FILE: src/extractors.rs
type HxBoosted (line 21) | pub struct HxBoosted(pub bool);
type Rejection (line 27) | type Rejection = std::convert::Infallible;
method from_request_parts (line 29) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxCurrentUrl (line 46) | pub struct HxCurrentUrl(pub Option<http::Uri>);
type Rejection (line 52) | type Rejection = std::convert::Infallible;
method from_request_parts (line 54) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxHistoryRestoreRequest (line 73) | pub struct HxHistoryRestoreRequest(pub bool);
type Rejection (line 79) | type Rejection = std::convert::Infallible;
method from_request_parts (line 81) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxPrompt (line 98) | pub struct HxPrompt(pub Option<String>);
type Rejection (line 104) | type Rejection = std::convert::Infallible;
method from_request_parts (line 106) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxRequest (line 125) | pub struct HxRequest(pub bool);
type Rejection (line 131) | type Rejection = std::convert::Infallible;
method from_request_parts (line 133) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxTarget (line 157) | pub struct HxTarget(pub Option<String>);
type Rejection (line 163) | type Rejection = std::convert::Infallible;
method from_request_parts (line 165) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxTriggerName (line 191) | pub struct HxTriggerName(pub Option<String>);
type Rejection (line 197) | type Rejection = std::convert::Infallible;
method from_request_parts (line 199) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
type HxTrigger (line 225) | pub struct HxTrigger(pub Option<String>);
type Rejection (line 231) | type Rejection = std::convert::Infallible;
method from_request_parts (line 233) | async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, ...
FILE: src/guard.rs
type HxRequestGuardLayer (line 22) | pub struct HxRequestGuardLayer<'a> {
function new (line 27) | pub fn new(redirect_to: &'a str) -> Self {
method default (line 33) | fn default() -> Self {
type Service (line 39) | type Service = HxRequestGuard<'a, S>;
function layer (line 41) | fn layer(&self, inner: S) -> Self::Service {
type HxRequestGuard (line 52) | pub struct HxRequestGuard<'a, S> {
type Response (line 63) | type Response = S::Response;
type Error (line 64) | type Error = S::Error;
type Future (line 65) | type Future = private::ResponseFuture<'a, S::Future>;
type Output (line 104) | type Output = Result<Response<B>, E>;
method poll (line 106) | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Outp...
function poll_ready (line 67) | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::...
function call (line 71) | fn call(&mut self, req: Request<T>) -> Self::Future {
FILE: src/headers.rs
constant HX_BOOSTED (line 8) | pub const HX_BOOSTED: HeaderName = HeaderName::from_static("hx-boosted");
constant HX_CURRENT_URL (line 11) | pub const HX_CURRENT_URL: HeaderName = HeaderName::from_static("hx-curre...
constant HX_HISTORY_RESTORE_REQUEST (line 15) | pub const HX_HISTORY_RESTORE_REQUEST: HeaderName =
constant HX_PROMPT (line 21) | pub const HX_PROMPT: HeaderName = HeaderName::from_static("hx-prompt");
constant HX_REQUEST_STR (line 23) | pub(crate) const HX_REQUEST_STR: &str = "hx-request";
constant HX_REQUEST (line 26) | pub const HX_REQUEST: HeaderName = HeaderName::from_static(HX_REQUEST_STR);
constant HX_TARGET_STR (line 28) | pub(crate) const HX_TARGET_STR: &str = "hx-target";
constant HX_TARGET (line 31) | pub const HX_TARGET: HeaderName = HeaderName::from_static(HX_TARGET_STR);
constant HX_TRIGGER_NAME_STR (line 33) | pub(crate) const HX_TRIGGER_NAME_STR: &str = "hx-trigger-name";
constant HX_TRIGGER_NAME (line 36) | pub const HX_TRIGGER_NAME: HeaderName = HeaderName::from_static(HX_TRIGG...
constant HX_LOCATION (line 39) | pub const HX_LOCATION: HeaderName = HeaderName::from_static("hx-location");
constant HX_PUSH_URL (line 42) | pub const HX_PUSH_URL: HeaderName = HeaderName::from_static("hx-push-url");
constant HX_REDIRECT (line 45) | pub const HX_REDIRECT: HeaderName = HeaderName::from_static("hx-redirect");
constant HX_REFRESH (line 48) | pub const HX_REFRESH: HeaderName = HeaderName::from_static("hx-refresh");
constant HX_REPLACE_URL (line 51) | pub const HX_REPLACE_URL: HeaderName = HeaderName::from_static("hx-repla...
constant HX_RESWAP (line 56) | pub const HX_RESWAP: HeaderName = HeaderName::from_static("hx-reswap");
constant HX_RETARGET (line 60) | pub const HX_RETARGET: HeaderName = HeaderName::from_static("hx-retarget");
constant HX_RESELECT (line 65) | pub const HX_RESELECT: HeaderName = HeaderName::from_static("hx-reselect");
constant HX_TRIGGER_STR (line 67) | pub(crate) const HX_TRIGGER_STR: &str = "hx-trigger";
constant HX_TRIGGER (line 77) | pub const HX_TRIGGER: HeaderName = HeaderName::from_static(HX_TRIGGER_STR);
constant HX_TRIGGER_AFTER_SETTLE (line 82) | pub const HX_TRIGGER_AFTER_SETTLE: HeaderName = HeaderName::from_static(...
constant HX_TRIGGER_AFTER_SWAP (line 87) | pub const HX_TRIGGER_AFTER_SWAP: HeaderName = HeaderName::from_static("h...
FILE: src/responders.rs
constant HX_SWAP_INNER_HTML (line 17) | const HX_SWAP_INNER_HTML: &str = "innerHTML";
constant HX_SWAP_OUTER_HTML (line 18) | const HX_SWAP_OUTER_HTML: &str = "outerHTML";
constant HX_SWAP_BEFORE_BEGIN (line 19) | const HX_SWAP_BEFORE_BEGIN: &str = "beforebegin";
constant HX_SWAP_AFTER_BEGIN (line 20) | const HX_SWAP_AFTER_BEGIN: &str = "afterbegin";
constant HX_SWAP_BEFORE_END (line 21) | const HX_SWAP_BEFORE_END: &str = "beforeend";
constant HX_SWAP_AFTER_END (line 22) | const HX_SWAP_AFTER_END: &str = "afterend";
constant HX_SWAP_DELETE (line 23) | const HX_SWAP_DELETE: &str = "delete";
constant HX_SWAP_NONE (line 24) | const HX_SWAP_NONE: &str = "none";
type HxPushUrl (line 35) | pub struct HxPushUrl(pub String);
method from (line 51) | fn from(value: &'a str) -> Self {
type Error (line 38) | type Error = HxError;
method into_response_parts (line 40) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type HxRedirect (line 63) | pub struct HxRedirect(pub String);
method from (line 79) | fn from(value: &'a str) -> Self {
type Error (line 66) | type Error = HxError;
method into_response_parts (line 68) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type HxRefresh (line 90) | pub struct HxRefresh(pub bool);
method from (line 93) | fn from(value: bool) -> Self {
type Error (line 99) | type Error = Infallible;
method into_response_parts (line 101) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type HxReplaceUrl (line 124) | pub struct HxReplaceUrl(pub String);
method from (line 140) | fn from(value: &'a str) -> Self {
type Error (line 127) | type Error = HxError;
method into_response_parts (line 129) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type HxReswap (line 151) | pub struct HxReswap(pub SwapOption);
method from (line 164) | fn from(value: SwapOption) -> Self {
type Error (line 154) | type Error = Infallible;
method into_response_parts (line 156) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type HxRetarget (line 177) | pub struct HxRetarget(pub String);
method from (line 193) | fn from(value: T) -> Self {
type Error (line 180) | type Error = HxError;
method into_response_parts (line 182) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type HxReselect (line 206) | pub struct HxReselect(pub String);
method from (line 222) | fn from(value: T) -> Self {
type Error (line 209) | type Error = HxError;
method into_response_parts (line 211) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type SwapOption (line 230) | pub enum SwapOption {
method serialize (line 254) | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
method from (line 277) | fn from(value: SwapOption) -> Self {
FILE: src/responders/location.rs
type HxLocation (line 18) | pub struct HxLocation {
method from_str (line 30) | pub fn from_str(uri: impl AsRef<str>) -> Self {
method from_str_with_options (line 41) | pub fn from_str_with_options(uri: impl AsRef<str>, options: LocationOp...
method into_header_with_options (line 49) | fn into_header_with_options(self) -> Result<String, HxError> {
method from (line 71) | fn from(uri: &'a str) -> Self {
method from (line 79) | fn from((uri, options): (&'a str, LocationOptions)) -> Self {
type Error (line 85) | type Error = HxError;
method into_response_parts (line 87) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
type LocationOptions (line 116) | pub struct LocationOptions {
method is_default (line 147) | pub(super) fn is_default(&self) -> bool {
function test_serialize_location (line 172) | fn test_serialize_location() {
FILE: src/responders/trigger.rs
type HxEvent (line 8) | pub struct HxEvent {
method new (line 18) | pub fn new(name: impl AsRef<str>) -> Self {
method new_with_data (line 29) | pub fn new_with_data<T: ::serde::Serialize>(
method from (line 44) | fn from(name: N) -> Self {
function events_to_header_value (line 54) | fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderVa...
function events_to_header_value (line 65) | fn events_to_header_value(events: Vec<HxEvent>) -> Result<http::HeaderVa...
type TriggerMode (line 98) | pub enum TriggerMode {
type HxResponseTrigger (line 118) | pub struct HxResponseTrigger {
method new (line 126) | pub fn new<T: Into<HxEvent>>(mode: TriggerMode, events: impl IntoItera...
method normal (line 137) | pub fn normal<T: Into<HxEvent>>(events: impl IntoIterator<Item = T>) -...
method after_settle (line 145) | pub fn after_settle<T: Into<HxEvent>>(events: impl IntoIterator<Item =...
method after_swap (line 153) | pub fn after_swap<T: Into<HxEvent>>(events: impl IntoIterator<Item = T...
method from (line 163) | fn from((mode, events): (TriggerMode, T)) -> Self {
type Error (line 172) | type Error = HxError;
method into_response_parts (line 174) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
function valid_event_to_header_encoding (line 199) | fn valid_event_to_header_encoding() {
FILE: src/responders/vary.rs
constant HX_REQUEST (line 6) | const HX_REQUEST: HeaderValue = HeaderValue::from_static(headers::HX_REQ...
constant HX_TARGET (line 7) | const HX_TARGET: HeaderValue = HeaderValue::from_static(headers::HX_TARG...
constant HX_TRIGGER (line 8) | const HX_TRIGGER: HeaderValue = HeaderValue::from_static(headers::HX_TRI...
constant HX_TRIGGER_NAME (line 9) | const HX_TRIGGER_NAME: HeaderValue = HeaderValue::from_static(headers::H...
type VaryHxRequest (line 25) | pub struct VaryHxRequest;
type Error (line 28) | type Error = HxError;
method into_response_parts (line 30) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
function vary_response (line 39) | pub fn vary_response() -> VaryHxRequest {
type VaryHxTarget (line 54) | pub struct VaryHxTarget;
type Error (line 57) | type Error = HxError;
method into_response_parts (line 59) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
function vary_response (line 68) | pub fn vary_response() -> VaryHxTarget {
type VaryHxTrigger (line 83) | pub struct VaryHxTrigger;
type Error (line 86) | type Error = HxError;
method into_response_parts (line 88) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
function vary_response (line 97) | pub fn vary_response() -> VaryHxTrigger {
type VaryHxTriggerName (line 112) | pub struct VaryHxTriggerName;
type Error (line 115) | type Error = HxError;
method into_response_parts (line 117) | fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseP...
function vary_response (line 126) | pub fn vary_response() -> VaryHxTriggerName {
function multiple_headers (line 140) | async fn multiple_headers() {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (81K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 783,
"preview": "name: ci\n\nenv:\n CARGO_TERM_COLOR: always\n\non:\n push:\n branches: [main]\n pull_request: {}\n\njobs:\n check:\n runs-"
},
{
"path": ".gitignore",
"chars": 195,
"preview": "# Directories\n.cargo/\n.turbo/\nassets/\nbuild/\ndata/\ndist/\nnode_modules/\npublic/\ntarget/\n\n# Files\n.env\n.env.development\n.e"
},
{
"path": "CHANGELOG.md",
"chars": 3320,
"preview": "# Changelog\n\n## v0.8.1\n\n- Revert an accidental breaking change introducting a nightly-only feature.\n\n## v0.8.0\n\n- Change"
},
{
"path": "Cargo.toml",
"chars": 1485,
"preview": "[package]\nname = \"axum-htmx\"\nauthors = [\"Rob Wagner <rob@sombia.com>\"]\nlicense = \"MIT OR Apache-2.0\"\ndescription = \"A se"
},
{
"path": "LICENSE-APACHE",
"chars": 9135,
"preview": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AN"
},
{
"path": "LICENSE-MIT",
"chars": 1050,
"preview": "Copyright 2023 Rob Wagner\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software"
},
{
"path": "README.md",
"chars": 10237,
"preview": "# axum-htmx\n\n<!-- markdownlint-disable -->\n<div align=\"right\">\n<a href=\"https://crates.io/crates/axum-htmx\">\n <img sr"
},
{
"path": "examples/auto-vary.rs",
"chars": 1322,
"preview": "//! Using `auto-vary` middleware\n//!\n//! Don't forget about the feature while running it:\n//! `cargo run --features auto"
},
{
"path": "rustfmt.toml",
"chars": 88,
"preview": "group_imports = \"StdExternalCrate\"\nimports_granularity = \"Crate\"\nreorder_imports = true\n"
},
{
"path": "src/auto_vary.rs",
"chars": 6614,
"preview": "//! A middleware to automatically add a `Vary` header when needed to address\n//! [htmx caching issue](https://htmx.org/d"
},
{
"path": "src/error.rs",
"chars": 1798,
"preview": "use std::{error, fmt};\n\nuse axum_core::response::IntoResponse;\nuse http::{\n StatusCode,\n header::{InvalidHeaderVal"
},
{
"path": "src/extractors.rs",
"chars": 7496,
"preview": "//! Axum extractors for htmx request headers.\n\nuse axum_core::extract::FromRequestParts;\nuse http::request::Parts;\n\nuse "
},
{
"path": "src/guard.rs",
"chars": 3328,
"preview": "//! Request guard for protecting a router against non-htmx requests.\n\nuse std::{\n future::Future,\n pin::Pin,\n t"
},
{
"path": "src/headers.rs",
"chars": 3514,
"preview": "//! HTTP headers used by htmx.\n\nuse http::HeaderName;\n\n/// Indicates that the request is via an element using `hx-boost`"
},
{
"path": "src/lib.rs",
"chars": 844,
"preview": "#![cfg_attr(feature = \"unstable\", feature(doc_cfg))]\n#![doc = include_str!(\"../README.md\")]\n#![forbid(unsafe_code)]\n#![a"
},
{
"path": "src/responders/location.rs",
"chars": 6042,
"preview": "use axum_core::response::{IntoResponseParts, ResponseParts};\nuse http::HeaderValue;\n\nuse crate::{HxError, headers};\n\n///"
},
{
"path": "src/responders/trigger.rs",
"chars": 6673,
"preview": "use axum_core::response::{IntoResponseParts, ResponseParts};\n\nuse crate::{HxError, headers};\n\n/// Represents a client-si"
},
{
"path": "src/responders/vary.rs",
"chars": 4596,
"preview": "use axum_core::response::{IntoResponseParts, ResponseParts};\nuse http::header::{HeaderValue, VARY};\n\nuse crate::{HxError"
},
{
"path": "src/responders.rs",
"chars": 8497,
"preview": "//! Axum responses for htmx response headers.\n\nuse std::convert::Infallible;\n\nuse axum_core::response::{IntoResponsePart"
}
]
About this extraction
This page contains the full source code of the robertwayne/axum-htmx GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (75.2 KB), approximately 18.9k tokens, and a symbol index with 178 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.