Initial commit

This commit is contained in:
davidontop 2024-06-15 11:22:26 +02:00
commit bb9084dfed
Signed by: DavidOnTop
GPG key ID: 5D05538A45D5149F
13 changed files with 1934 additions and 0 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

11
.rustfmt.toml Normal file
View file

@ -0,0 +1,11 @@
condense_wildcard_suffixes = true
edition = "2021"
fn_single_line = true
format_code_in_doc_comments = true
format_macro_matchers = true
hard_tabs = true
match_block_trailing_comma = true
imports_granularity = "Crate"
newline_style = "Unix"
group_imports = "StdExternalCrate"
tab_spaces = 4

1653
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

29
Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[workspace]
members = ["macros", "."]
[package]
name = "leptos_reactive_axum"
version = "0.1.0"
edition = "2021"
description = "reactive context for axum handlers"
authors = ["davidontop <me@davidon.top>"]
readme = "README.md"
documentation = "https://docs.rs/leptos_reactive_axum"
license = "MIT"
repository = "https://git.davidon.top/public/leptos_reactive_axum.git"
[dependencies]
leptos_reactive_axum_macros = { path = "./macros" }
leptos_reactive = { version = "0.6", features = ["ssr"] }
axum = "0.7"
scopeguard = "1.2.0"
thiserror = "1.0.61"
http = "1.1.0"
[features]
nightly = ["leptos_reactive/nightly"]
[dev-dependencies]
axum-test = "15.1.0"
serde_json = "1"
tokio = { version = "1", features = ["full"] }

22
LICENSE Normal file
View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2023 DavidOnTop
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.

1
README.md Normal file
View file

@ -0,0 +1 @@
# ctxt

12
macros/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "leptos_reactive_axum_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "2"
proc-macro2 = "1"
quote = "1"

9
macros/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
mod reactive;
#[proc_macro_attribute]
pub fn reactive(
attr: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
reactive::reactive(attr.into(), input.into()).into()
}

80
macros/src/reactive.rs Normal file
View file

@ -0,0 +1,80 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
fn map_fnarg(fnarg: &syn::FnArg) -> Option<TokenStream> {
match fnarg {
syn::FnArg::Receiver(_rec) => None,
syn::FnArg::Typed(pat_typed) => {
let pat = &pat_typed.pat;
let ty = match pat_typed.ty.as_ref().to_owned() {
syn::Type::Infer(_) => None,
_ => Some(pat_typed.ty.as_ref()),
};
let ty = ty.map(|t| quote! {: #t});
Some(quote! {
let #pat #ty = ::leptos_reactive_axum::extract().await.unwrap();
})
},
}
}
pub(crate) fn reactive(attr: TokenStream, input: TokenStream) -> TokenStream {
let syn::ItemFn {
attrs,
vis,
mut sig,
block,
} = match syn::parse2(input) {
Ok(input) => input,
Err(err) => return err.to_compile_error(),
};
let extract_body = match syn::parse2::<syn::LitBool>(attr) {
Ok(attr) => attr,
Err(err) => return err.to_compile_error(),
}
.value();
let prepend = quote! {
let __runtime = leptos_reactive::create_runtime();
scopeguard::defer!(__runtime.dispose());
leptos_reactive::provide_context(__parts);
};
let stmts = &block.stmts;
let mut extractors = Vec::new();
let body = if extract_body {
if let Some(fnarg) = sig.inputs.pop() {
Some(fnarg.into_value())
} else {
None
}
} else {
None
};
sig.inputs.iter().for_each(|fnarg| {
extractors.push(map_fnarg(fnarg));
});
let extractors = extractors.iter().filter(|e| e.is_some());
sig.inputs = Punctuated::new();
sig.inputs
.push(syn::parse2(quote! {__parts: ::http::request::Parts}).unwrap());
if let Some(body) = body {
sig.inputs.push(body);
}
quote! {
#(#attrs)* #vis #sig {
#prepend
#(#extractors)*
#(#stmts)*
}
}
}

11
src/error.rs Normal file
View file

@ -0,0 +1,11 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ExtractionError {
#[error("axum extracting information failed")]
AxumError(String),
#[error("leptos_reactive error occured")]
LeptosError(String),
#[error("the body of the request has been extracted more then once this isn't allowed")]
MultipleBodyExtractor,
}

28
src/lib.rs Normal file
View file

@ -0,0 +1,28 @@
pub mod error;
use std::fmt::Debug;
use axum::{extract::FromRequestParts, http::request::Parts};
use error::ExtractionError;
pub use leptos_reactive_axum_macros::reactive;
pub async fn extract<T>() -> Result<T, ExtractionError>
where
T: FromRequestParts<()>,
T::Rejection: Debug,
{
extract_with_state::<T, ()>(&()).await
}
pub async fn extract_with_state<T, S>(state: &S) -> Result<T, ExtractionError>
where
T: FromRequestParts<S>,
T::Rejection: Debug,
{
let mut parts = leptos_reactive::use_context::<Parts>().ok_or_else(|| {
ExtractionError::LeptosError("failed to extract Parts from leptos_reactive's Runtime defined by leptos_reactive_axum".to_string())
})?;
T::from_request_parts(&mut parts, state)
.await
.map_err(|e| ExtractionError::AxumError(format!("{e:?}")))
}

64
tests/test.rs Normal file
View file

@ -0,0 +1,64 @@
use axum::{
http::{HeaderMap, HeaderValue},
response::IntoResponse,
routing::get,
Json, Router,
};
use axum_test::TestServer;
use http::HeaderName;
use serde_json::json;
async fn demo_expanded_handler(
__parts: ::http::request::Parts,
Json(payload): Json<serde_json::Value>,
) -> impl IntoResponse {
let __runtime = leptos_reactive::create_runtime();
scopeguard::defer!(__runtime.dispose());
{
leptos_reactive::provide_context(__parts);
}
let headers: HeaderMap = ::leptos_reactive_axum::extract().await.unwrap();
assert_eq!(
headers.get("macrosareawesome"),
Some(&HeaderValue::from_static("YehTheyAre"))
);
assert_eq!(payload.as_str(), Some("hello there"));
""
}
#[leptos_reactive_axum::reactive(true)]
async fn handler(headers: HeaderMap, Json(payload): Json<serde_json::Value>) -> impl IntoResponse {
assert_eq!(
headers.get("macrosareawesome"),
Some(&HeaderValue::from_static("YehTheyAre"))
);
assert_eq!(payload.as_str(), Some("hello there"));
""
}
#[tokio::test]
async fn test_handlers() {
let router = Router::new()
.route("/demo", get(demo_expanded_handler))
.route("/", get(handler));
let server = TestServer::new(router).unwrap();
let _ = server
.get("/")
.add_header(
HeaderName::from_static("macrosareawesome"),
HeaderValue::from_static("YehTheyAre"),
)
.json(&json!("hello there"));
let _ = server
.get("/demo")
.add_header(
HeaderName::from_static("macrosareawesome"),
HeaderValue::from_static("YehTheyAre"),
)
.json(&json!("hello there"));
}