mirror of
https://github.com/robertwayne/axum-htmx
synced 2024-12-27 02:39:32 +01:00
Added hx-boosted-by macro
This commit is contained in:
parent
e8a0b94d03
commit
7ef8f4485c
8 changed files with 224 additions and 0 deletions
|
@ -15,12 +15,19 @@ default = []
|
||||||
unstable = []
|
unstable = []
|
||||||
guards = ["tower", "futures-core", "pin-project-lite"]
|
guards = ["tower", "futures-core", "pin-project-lite"]
|
||||||
serde = ["dep:serde", "dep:serde_json"]
|
serde = ["dep:serde", "dep:serde_json"]
|
||||||
|
derive = ["dep:axum-htmx-derive"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["axum-htmx-derive"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum-core = "0.4"
|
axum-core = "0.4"
|
||||||
http = { version = "1.0", default-features = false }
|
http = { version = "1.0", default-features = false }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
# Workspace dependencies
|
||||||
|
axum-htmx-derive = { path = "axum-htmx-derive", optional = true }
|
||||||
|
|
||||||
# Optional dependencies required for the `guards` feature.
|
# Optional dependencies required for the `guards` feature.
|
||||||
tower = { version = "0.4", default-features = false, optional = true }
|
tower = { version = "0.4", default-features = false, optional = true }
|
||||||
futures-core = { version = "0.3", optional = true }
|
futures-core = { version = "0.3", optional = true }
|
||||||
|
|
16
axum-htmx-derive/Cargo.toml
Normal file
16
axum-htmx-derive/Cargo.toml
Normal file
|
@ -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
axum-htmx-derive/README.md
Normal file
3
axum-htmx-derive/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# axum-htmx-derive
|
||||||
|
|
||||||
|
This is an internal helper library of [`axum-htmx`](https://docs.rs/axum-htmx/latest/axum_htmx/).
|
97
axum-htmx-derive/src/boosted_by/boosted_by.rs
Normal file
97
axum-htmx-derive/src/boosted_by/boosted_by.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
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.push(hx_boosted_input);
|
||||||
|
|
||||||
|
// pop the last statement and wrap it with if-else
|
||||||
|
let modify_stmt = source_fn.block.stmts.pop().unwrap();
|
||||||
|
let modify_stmt = quote!(#modify_stmt).to_string();
|
||||||
|
let modify_args = input.fn_args.join("");
|
||||||
|
|
||||||
|
let new_fn_str = quote!(#template_fn)
|
||||||
|
.to_string()
|
||||||
|
.replace("layout_fn", input.layout_fn.as_str())
|
||||||
|
.replace("result_boosted", modify_stmt.as_str())
|
||||||
|
.replace("result_with_layout", modify_stmt.as_str())
|
||||||
|
.replace(", fn_args", modify_args.as_str());
|
||||||
|
|
||||||
|
let new_fn: ItemFn = parse_str(new_fn_str.as_str()).unwrap();
|
||||||
|
let new_fn_stmt = new_fn.block.stmts.first().unwrap().clone();
|
||||||
|
|
||||||
|
// push the new statement to source_fn
|
||||||
|
source_fn.block.stmts.push(new_fn_stmt);
|
||||||
|
|
||||||
|
source_fn.to_owned()
|
||||||
|
}
|
27
axum-htmx-derive/src/boosted_by/mod.rs
Normal file
27
axum-htmx-derive/src/boosted_by/mod.rs
Normal file
|
@ -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
axum-htmx-derive/src/boosted_by/tests.rs
Normal file
53
axum-htmx-derive/src/boosted_by/tests.rs
Normal file
|
@ -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(Path(user_id): Path<u32>, axum_htmx::HxBoosted(boosted): axum_htmx::HxBoosted) -> 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
axum-htmx-derive/src/lib.rs
Normal file
17
axum-htmx-derive/src/lib.rs
Normal file
|
@ -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()
|
||||||
|
}
|
|
@ -22,3 +22,7 @@ pub use guard::*;
|
||||||
pub use headers::*;
|
pub use headers::*;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use responders::*;
|
pub use responders::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
#[cfg_attr(feature = "unstable", doc(cfg(feature = "derive")))]
|
||||||
|
pub use axum_htmx_derive::*;
|
||||||
|
|
Loading…
Reference in a new issue