Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/hx-boosted-by #13

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ default = []
unstable = []
guards = ["tower", "futures-core", "pin-project-lite"]
serde = ["dep:serde", "dep:serde_json"]
derive = ["dep:axum-htmx-derive"]

[workspace]
members = ["axum-htmx-derive"]

[dependencies]
axum-core = "0.4"
http = { version = "1.0", default-features = false }
async-trait = "0.1"

# Workspace dependencies
axum-htmx-derive = { path = "axum-htmx-derive", optional = true }

# Optional dependencies required for the `guards` feature.
tower = { version = "0.4", default-features = false, optional = true }
futures-core = { version = "0.3", optional = true }
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [Extractors](#extractors)
- [Responders](#responders)
- [Request Guards](#request-guards)
- [Macroses](#macroses)
- [Examples](#examples)
- [Example: Extractors](#example-extractors)
- [Example: Responders](#example-responders)
Expand Down Expand Up @@ -89,6 +90,18 @@ 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._

## Macroses

__Requires features `derive`.__

In addition to the HxBoosted extractor, the library provides macroses `hx_boosted_by` and it's async version `hx_boosted_by_async` for managing the response based on the presence of the `HX-Boosted` header.

The macro input should have a `layout_fn`, and can have arguments passed from annotated function into `layout_fn`. The macro will call the `layout_fn` if the `HX-Boosted` header is not present, otherwise it will return the response directly.

`#[hx_boosted_by(layout_fn [, arg1, agr2, ...])]`

If `layout_fn` is an async function, use `hx_boosted_by_async` instead.

## Examples

### Example: Extractors
Expand Down Expand Up @@ -185,13 +198,38 @@ fn router_two() -> Router {
}
```

### Example: Macros

```rust
use axum::extract::Path;
use axum::response::Html;
use axum_htmx::hx_boosted_by;

#[hx_boosted_by(with_layout, page_title)]
async fn get_hello(Path(name): Path<String>) -> Html<String> {
let page_title = "Hello Page";
Html(format!("Hello, {}!", name))
}

#[hx_boosted_by(with_layout, page_title)]
async fn get_bye(Path(name): Path<String>) -> Html<String> {
let page_title = "Bye Page";
Html(format!("Bye, {}!", name))
}

fn with_layout(Html(partial): Html<String>, page_title: &str) -> Html<String> {
Html(format!("<html><head><title>{}</title></head><body>{}</body></html>", page_title, partial))
}
```

## Feature Flags

<!-- markdownlint-disable -->
| Flag | Default | Description | Dependencies |
|----------|----------|------------------------------------------------------------|---------------------------------------------|
| `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` |
| `derive` | Disabled | Adds the `hx_boosted_by` and `hx_boosted_by_async` macros. | `proc-macro-error`, `proc-macro2`, `quote`, `syn` |
<!-- markdownlint-enable -->

## Contributing
Expand Down
16 changes: 16 additions & 0 deletions axum-htmx-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "axum-htmx-derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro-error = "1.0"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["extra-traits", "full", "fold"] }

[dev-dependencies]
colored-diff = "0.2.3"
3 changes: 3 additions & 0 deletions axum-htmx-derive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# axum-htmx-derive

This is an internal helper library of [`axum-htmx`](https://docs.rs/axum-htmx/latest/axum_htmx/).
100 changes: 100 additions & 0 deletions axum-htmx-derive/src/boosted_by/boosted_by.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use proc_macro2::TokenStream;
use proc_macro_error::abort;
use quote::quote;
use syn::{parse2, parse_quote, parse_str, ItemFn};

pub struct MacroInput {
pub source_fn: ItemFn,
pub layout_fn: String,
pub fn_args: Vec<String>,
}

pub fn parse_macros_input(
args: TokenStream,
input: TokenStream,
) -> Result<MacroInput, TokenStream> {
let mut args_iter = args.clone().into_iter().map(|arg| arg.to_string());

// get layout_fn from args
let layout_fn = match args_iter.next() {
Some(arg) => arg,
None => abort!(
args,
"boosted_by requires layout function (to produce non-boosted response) as an argument."
),
};

// arguments for callable function
let fn_args = args_iter.collect::<Vec<String>>();

// parse input as ItemFn
let source_fn = match parse2::<ItemFn>(input) {
Ok(syntax_tree) => syntax_tree,
Err(error) => return Err(error.to_compile_error()),
};

Ok(MacroInput {
source_fn,
layout_fn,
fn_args,
})
}

pub fn transform(input: MacroInput) -> ItemFn {
let template_fn: ItemFn = parse_quote!(
fn index(axum_htmx::HxBoosted(boosted): axum_htmx::HxBoosted) {
if boosted {
result_boosted
} else {
layout_fn(result_with_layout, fn_args)
}
}
);

transform_using_template(input, template_fn)
}

pub fn transform_async(input: MacroInput) -> ItemFn {
let template_fn: ItemFn = parse_quote!(
fn index(axum_htmx::HxBoosted(boosted): axum_htmx::HxBoosted) {
if boosted {
result_boosted
} else {
layout_fn(result_with_layout, fn_args).await
}
}
);

transform_using_template(input, template_fn)
}

pub fn transform_using_template(input: MacroInput, template_fn: ItemFn) -> ItemFn {
let mut source_fn = input.source_fn.clone();

// add HxBoosted input to source_fn
let hx_boosted_input = template_fn.sig.inputs.first().unwrap().clone();
source_fn.sig.inputs.insert(0, hx_boosted_input);

// pop the last statement and wrap it with if-else
let source_stmt = source_fn.block.stmts.pop().unwrap();
let source_stmt = quote!(#source_stmt).to_string();

let new_fn_str = quote!(#template_fn)
.to_string()
.replace("layout_fn", input.layout_fn.as_str())
.replace("result_boosted", source_stmt.as_str())
.replace("result_with_layout", source_stmt.as_str());

// add layout_args
let layout_args = input.fn_args.join("");
let new_fn_str = new_fn_str.replace(", fn_args", layout_args.as_str());

// parse new_fn_str as ItemFn
let new_fn: ItemFn = parse_str(new_fn_str.as_str()).unwrap();

// push the new statement to source_fn
let new_fn_stmt = new_fn.block.stmts.first().unwrap().clone();
source_fn.block.stmts.push(new_fn_stmt);

source_fn.to_owned()
}
27 changes: 27 additions & 0 deletions axum-htmx-derive/src/boosted_by/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use proc_macro2::TokenStream;
use quote::quote;

#[cfg(test)]
mod tests;

mod boosted_by;

pub fn macros(args: TokenStream, input: TokenStream) -> TokenStream {
match boosted_by::parse_macros_input(args, input) {
Ok(macros_input) => {
let new_item_fn = boosted_by::transform(macros_input);
quote!(#new_item_fn)
}
Err(error) => error,
}
}

pub fn macros_async(args: TokenStream, input: TokenStream) -> TokenStream {
match boosted_by::parse_macros_input(args, input) {
Ok(macros_input) => {
let new_item_fn = boosted_by::transform_async(macros_input);
quote!(#new_item_fn)
}
Err(error) => error,
}
}
53 changes: 53 additions & 0 deletions axum-htmx-derive/src/boosted_by/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#![cfg(test)]

use proc_macro2::TokenStream;
use quote::quote;

use super::macros;

#[test]
fn boosted_by() {
let before = quote! {
async fn index(Path(user_id): Path<u32>) -> Html<String> {
let ctx = HomeTemplate {
locale: "en".to_string(),
};

Html(ctx.render_once().unwrap_or(String::new()))
}
};
let expected = quote! {
async fn index(axum_htmx::HxBoosted(boosted): axum_htmx::HxBoosted, Path(user_id): Path<u32>) -> Html<String> {
let ctx = HomeTemplate {
locale: "en".to_string(),
};

if boosted {
Html(ctx.render_once().unwrap_or(String::new()))
} else {
with_layout(Html(ctx.render_once().unwrap_or(String::new())), state1, state2)
}
}
};

let after = macros(quote! {with_layout, state1, state2}, before);

assert_tokens_eq(&expected, &after);
}

fn assert_tokens_eq(expected: &TokenStream, actual: &TokenStream) {
let expected = expected.to_string();
let actual = actual.to_string();

if expected != actual {
println!(
"{}",
colored_diff::PrettyDifference {
expected: &expected,
actual: &actual,
}
);

panic!("expected != actual");
}
}
17 changes: 17 additions & 0 deletions axum-htmx-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#![doc = include_str!("../README.md")]
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;

mod boosted_by;

#[proc_macro_error]
#[proc_macro_attribute]
pub fn hx_boosted_by(args: TokenStream, input: TokenStream) -> TokenStream {
boosted_by::macros(args.into(), input.into()).into()
}

#[proc_macro_error]
#[proc_macro_attribute]
pub fn hx_boosted_by_async(args: TokenStream, input: TokenStream) -> TokenStream {
boosted_by::macros_async(args.into(), input.into()).into()
}
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ pub use guard::*;
pub use headers::*;
#[doc(inline)]
pub use responders::*;

#[cfg(feature = "derive")]
#[cfg_attr(feature = "unstable", doc(cfg(feature = "derive")))]
pub use axum_htmx_derive::*;