commit ad8c771f0ae836d80c9d9baa1fe5c75d2a3ccd2c Author: Rob Wagner Date: Sat Jul 22 16:37:15 2023 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06e96d --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c078883 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "axum-htmx" +authors = ["Rob Wagner "] +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" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d8c669 --- /dev/null +++ b/README.md @@ -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. diff --git a/docs/LICENSE-APACHE b/docs/LICENSE-APACHE new file mode 100644 index 0000000..2347a58 --- /dev/null +++ b/docs/LICENSE-APACHE @@ -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. \ No newline at end of file diff --git a/docs/LICENSE-MIT b/docs/LICENSE-MIT new file mode 100644 index 0000000..796525d --- /dev/null +++ b/docs/LICENSE-MIT @@ -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. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f7a9e5b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +reorder_imports = true diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f6c0d3d --- /dev/null +++ b/src/lib.rs @@ -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 for more information. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum HxRequestHeader { + /// Indicates that the request is via an element using `hx-boost` attribute. + /// + /// See 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 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 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 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 for more information. + Trigger, + /// Allows you to trigger client-side events. + /// + /// See for more information. + TriggerAfterSettle, + /// Allows you to trigger client-side events. + /// + /// See 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 for more information. +#[derive(Debug, Clone, Copy)] +pub struct HxBoosted(pub bool); + +#[axum::async_trait] +impl FromRequestParts for HxBoosted +where + S: Send + Sync, +{ + type Rejection = std::convert::Infallible; + + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { + 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 FromRequestParts for HxCurrentUrl +where + S: Send + Sync, +{ + type Rejection = std::convert::Infallible; + + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { + 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())); + } +}