Initial commit

This commit is contained in:
Rob Wagner 2023-07-22 16:37:15 -04:00
commit ad8c771f0a
No known key found for this signature in database
GPG key ID: 53CCB4497B15CF61
7 changed files with 254 additions and 0 deletions

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# 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

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "axum-htmx"
authors = ["Rob Wagner <rob@sombia.com>"]
license = "MIT OR Apache-2.0"
description = "HTMX header extractors for axum."
repository = "https://github.com/robertwayne/axum-htmx"
categories = ["web-programming", "http-server"]
keywords = ["axum", "htmx", "header", "extractor"]
readme = "README.md"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
axum = { git = "https://github.com/tokio-rs/axum", branch = "main" }

39
README.md Normal file
View file

@ -0,0 +1,39 @@
# axum-htmx
`axum-htmx` is a small extension library providing extractors for the various
[htmx](https://htmx.org/) headers within [axum](https://github.com/tokio-rs/axum).
__This crate is current a work-in-progress. There are many missing header implementations, and it builds against the upcoming, unreleased branch for `axum`.__
## Usage
```toml
axum-htmx = { git = "https://github.com/robertwayne/axum-htmx", branch = "main" }
```
## Examples
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.
```rs
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
}
}
```
## License
`axum-htmx` is dual-licensed under either
- **[MIT License](/docs/LICENSE-MIT)**
- **[Apache License, Version 2.0](/docs/LICENSE-APACHE)**
at your option.

13
docs/LICENSE-APACHE Normal file
View file

@ -0,0 +1,13 @@
Copyright 2023 Rob Wagner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

7
docs/LICENSE-MIT Normal file
View file

@ -0,0 +1,7 @@
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.

3
rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
reorder_imports = true

155
src/lib.rs Normal file
View file

@ -0,0 +1,155 @@
#![forbid(unsafe_code)]
use axum::{extract::FromRequestParts, http::request::Parts};
/// Represents all of the headers that can be sent in a request to the server.
///
/// See <https://htmx.org/reference/#request_headers> for more information.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HxRequestHeader {
/// Indicates that the request is via an element using `hx-boost` attribute.
///
/// See <https://htmx.org/attributes/hx-boost/> for more information.
Boosted,
/// The current URL of the browser.
CurrentUrl,
/// `true` if the request is for history restoration after a miss in the
/// local history cache.
HistoryRestoreRequest,
/// The user response to an `hx-prompt`
///
/// See <https://htmx.org/attributes/hx-prompt/> for more information.
Prompt,
/// Always `true`.
Request,
/// The `id` of the target element, if it exists.
Target,
/// The `name` of the triggered element, if it exists.
TriggerName,
/// The `id` of the triggered element, if it exists.
Trigger,
}
impl HxRequestHeader {
pub fn as_str(&self) -> &'static str {
match self {
HxRequestHeader::Boosted => "HX-Boosted",
HxRequestHeader::CurrentUrl => "HX-Current-Url",
HxRequestHeader::HistoryRestoreRequest => "HX-History-Restore-Request",
HxRequestHeader::Prompt => "HX-Prompt",
HxRequestHeader::Request => "HX-Request",
HxRequestHeader::Target => "HX-Target",
HxRequestHeader::TriggerName => "HX-Trigger-Name",
HxRequestHeader::Trigger => "HX-Trigger",
}
}
}
/// Represents all of the headers that can be sent in a response to the client.
///
/// See <https://htmx.org/reference/#response_headers> for more information.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HxResponseHeader {
/// Allows you to do a client-side redirect that does not do a full page
/// reload.
Location,
/// Pushes a new URL onto the history stack.
PushUrl,
/// Can be used to do a client-side redirect to a new location.
Redirect,
/// If set to `true`, the client will do a full refresh on the page.
Refresh,
/// Replaces the currelt URL in the location bar.
ReplaceUrl,
/// Allows you to specify how the response value will be swapped.
///
/// See <https://htmx.org/attributes/hx-swap/> for more information.
Reswap,
/// A CSS selector that update the target of the content update to a
/// different element on the page.
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
Reselect,
/// Allows you to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
Trigger,
/// Allows you to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
TriggerAfterSettle,
/// Allows you to trigger client-side events.
///
/// See <https://htmx.org/headers/hx-trigger/> for more information.
TriggerAfterSwap,
}
impl HxResponseHeader {
pub fn as_str(&self) -> &'static str {
match self {
HxResponseHeader::Location => "HX-Location",
HxResponseHeader::PushUrl => "HX-Push-Url",
HxResponseHeader::Redirect => "HX-Redirect",
HxResponseHeader::Refresh => "HX-Refresh",
HxResponseHeader::ReplaceUrl => "HX-Replace-Url",
HxResponseHeader::Reswap => "HX-Reswap",
HxResponseHeader::Retarget => "HX-Retarget",
HxResponseHeader::Reselect => "HX-Reselect",
HxResponseHeader::Trigger => "HX-Trigger",
HxResponseHeader::TriggerAfterSettle => "HX-Trigger-After-Settle",
HxResponseHeader::TriggerAfterSwap => "HX-Trigger-After-Swap",
}
}
}
/// The `HX-Boosted` header. This header is set when a request is made with the
/// "hx-boost" attribute is set on an element.
///
/// This extractor does not fail if no header is present, instead returning a
/// `false` value.
///
/// See <https://htmx.org/attributes/hx-boost/> for more information.
#[derive(Debug, Clone, Copy)]
pub struct HxBoosted(pub bool);
#[axum::async_trait]
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(HxRequestHeader::Boosted.as_str())
{
return Ok(HxBoosted(true));
} else {
return Ok(HxBoosted(false));
}
}
}
#[derive(Debug, Clone)]
pub struct HxCurrentUrl(pub String);
#[axum::async_trait]
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(HxRequestHeader::CurrentUrl.as_str()) {
if let Ok(url) = url.to_str() {
return Ok(HxCurrentUrl(url.to_string()));
}
}
return Ok(HxCurrentUrl("".to_string()));
}
}