Skip to content

Commit

Permalink
Merge pull request #2 from matteopolak/feat/add-debug-handler
Browse files Browse the repository at this point in the history
feat: add #[axum_codec::debug_handler]
  • Loading branch information
matteopolak authored Aug 30, 2024
2 parents 80f90c1 + 231ee32 commit af7d7a3
Show file tree
Hide file tree
Showing 10 changed files with 1,403 additions and 214 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Axum Codec

[![https://img.shields.io/crates/v/axum-codec](https://img.shields.io/crates/v/axum-codec)](https://crates.io/crates/axum-codec)
[![https://img.shields.io/docsrs/axum-codec](https://img.shields.io/docsrs/axum-codec)](https://docs.rs/axum-codec/latest/axum_codec/)
[![<https://img.shields.io/crates/v/axum-codec>](https://img.shields.io/crates/v/axum-codec)](https://crates.io/crates/axum-codec)
[![<https://img.shields.io/docsrs/axum-codec>](https://img.shields.io/docsrs/axum-codec)](https://docs.rs/axum-codec/latest/axum_codec/)
[![ci status](https://github.com/matteopolak/axum-codec/workflows/ci/badge.svg)](https://github.com/matteopolak/axum-codec/actions)

A body extractor for the [Axum](https://github.com/tokio-rs/axum) web framework.
Expand All @@ -23,6 +23,7 @@ A body extractor for the [Axum](https://github.com/tokio-rs/axum) web framework.
- [ ] Add benchmarks?

Here's a quick example that can do the following:

- Decode a `User` from the request body in any of the supported formats.
- Encode a `Greeting` to the response body in any of the supported formats.

Expand Down Expand Up @@ -98,7 +99,7 @@ async fn main() {
# Feature flags

- `macros`: Enables the `axum_codec::apply` attribute macro.
- `json`*: Enables [`JSON`](https://github.com/serde-rs/json) support.
- `json`\*: Enables [`JSON`](https://github.com/serde-rs/json) support.
- `msgpack`: Enables [`MessagePack`](https://github.com/3Hren/msgpack-rust) support.
- `bincode`: Enables [`Bincode`](https://github.com/bincode-org/bincode) support.
- `bitcode`: Enables [`Bitcode`](https://github.com/SoftbearStudios/bitcode) support.
Expand All @@ -110,7 +111,11 @@ async fn main() {

* Enabled by default.

## A note about `#[axum::debug_handler]`

Since `axum-codec` uses its own `IntoCodecResponse` trait for encoding responses, it is not compatible with `#[axum::debug_handler]`. However, a new `#[axum_codec::debug_handler]` (and `#[axum_codec::debug_middleware]`) macro
is provided as a drop-in replacement.

## License

Dual-licensed under MIT or Apache License v2.0.

1 change: 1 addition & 0 deletions examples/aide-validator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct User {
age: u8,
}

#[axum_codec::debug_handler]
async fn me() -> impl IntoCodecResponse {
Codec(User {
name: "Alice".into(),
Expand Down
3 changes: 3 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ quote = "1"
syn = { version = "2", optional = true }

[features]
default = ["debug"]

debug = []
bincode = ["dep:syn"]
bitcode = ["dep:syn"]
serde = ["dep:syn"]
Expand Down
202 changes: 202 additions & 0 deletions macros/src/apply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
Meta, Path, Token,
};

struct Args {
encode: bool,
decode: bool,
crate_name: Path,
}

impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let options = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;

let mut encode = false;
let mut decode = false;
let mut crate_name = syn::parse_str("axum_codec").expect("failed to parse crate name");

for meta in options {
match meta {
Meta::List(list) => {
return Err(syn::Error::new(
list.span(),
"expected `encode`, `decode`, or `crate`",
))
}
Meta::Path(path) => {
let ident = path.get_ident().map(|ident| ident.to_string());
match ident.as_deref() {
Some("encode") if encode => {
return Err(syn::Error::new(
path.span(),
"option `encode` is already enabled",
))
}
Some("decode") if decode => {
return Err(syn::Error::new(
path.span(),
"option `decode` is already enabled",
))
}
Some("encode") => encode = true,
Some("decode") => decode = true,
Some(other) => {
return Err(syn::Error::new(
path.span(),
format!("unknown option `{other}`, expected `encode` or `decode`"),
))
}
None => {
return Err(syn::Error::new(
path.span(),
"expected `encode` or `decode`",
))
}
}
}
Meta::NameValue(name_value) => {
if !name_value.path.is_ident("crate") {
return Err(syn::Error::new(name_value.path.span(), "expected `crate`"));
}

let path = match name_value.value {
syn::Expr::Lit(ref lit) => match &lit.lit {
syn::Lit::Str(path) => path,
_ => return Err(syn::Error::new(lit.span(), "expected a string")),
},
_ => {
return Err(syn::Error::new(
name_value.value.span(),
"expected a literal string",
))
}
};

let mut path = syn::parse_str::<Path>(&path.value()).expect("failed to parse path");

path.leading_colon = if path.is_ident("crate") {
None
} else {
Some(Token![::](name_value.value.span()))
};

crate_name = path;
}
}
}

if !encode && !decode {
return Err(syn::Error::new(
input.span(),
"at least one of `encode` or `decode` must be enabled",
));
}

Ok(Self {
encode,
decode,
crate_name,
})
}
}

pub fn apply(
attr: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let args = syn::parse_macro_input!(attr as Args);

let crate_name = &args.crate_name;
let mut tokens = TokenStream::default();

#[cfg(feature = "serde")]
{
if args.encode {
tokens.extend(quote! {
#[derive(#crate_name::__private::serde::Serialize)]
});
}

if args.decode {
tokens.extend(quote! {
#[derive(#crate_name::__private::serde::Deserialize)]
});
}

let crate_ = format!("{}::__private::serde", crate_name.to_token_stream());

tokens.extend(quote! {
#[serde(crate = #crate_)]
});
}

#[cfg(feature = "bincode")]
{
if args.encode {
tokens.extend(quote! {
#[derive(#crate_name::__private::bincode::Encode)]
});
}

if args.decode {
tokens.extend(quote! {
#[derive(#crate_name::__private::bincode::Decode)]
});
}

let crate_ = format!("{}::__private::bincode", crate_name.to_token_stream());

tokens.extend(quote! {
#[bincode(crate = #crate_)]
});
}

#[cfg(feature = "bitcode")]
{
if args.encode {
tokens.extend(quote! {
#[derive(#crate_name::__private::bitcode::Encode)]
});
}

if args.decode {
tokens.extend(quote! {
#[derive(#crate_name::__private::bitcode::Decode)]
});
}

let crate_ = format!("{}::__private::bitcode", crate_name.to_token_stream());

tokens.extend(quote! {
#[bitcode(crate = #crate_)]
});
}

#[cfg(feature = "aide")]
{
let crate_ = format!("{}::__private::schemars", crate_name.to_token_stream());

tokens.extend(quote! {
#[derive(#crate_name::__private::schemars::JsonSchema)]
#[schemars(crate = #crate_)]
});
}

// TODO: Implement #[validate(crate = "...")]
// For now, use the real crate name so the error is nicer.
#[cfg(feature = "validator")]
if args.decode {
tokens.extend(quote! {
#[derive(validator::Validate)]
});
}

tokens.extend(TokenStream::from(input));
tokens.into()
}
60 changes: 60 additions & 0 deletions macros/src/attr_parsing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This is copied from Axum under the following license:
//
// Copyright 2021 Axum Contributors
//
// 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.

use quote::ToTokens;
use syn::{
parse::{Parse, ParseStream},
Token,
};

pub(crate) fn parse_assignment_attribute<K, T>(
input: ParseStream,
out: &mut Option<(K, T)>,
) -> syn::Result<()>
where
K: Parse + ToTokens,
T: Parse,
{
let kw = input.parse()?;
input.parse::<Token![=]>()?;
let inner = input.parse()?;

if out.is_some() {
let kw_name = std::any::type_name::<K>().split("::").last().unwrap();
let msg = format!("`{kw_name}` specified more than once");
return Err(syn::Error::new_spanned(kw, msg));
}

*out = Some((kw, inner));

Ok(())
}

pub(crate) fn second<T, K>(tuple: (T, K)) -> K {
tuple.1
}
Loading

0 comments on commit af7d7a3

Please sign in to comment.