Skip to content

Commit 447dc8b

Browse files
feat: add axum hook (#7)
1 parent c8a9c8d commit 447dc8b

6 files changed

Lines changed: 121 additions & 47 deletions

File tree

packages/breach-macros/src/http.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,18 @@ impl<'a> ToTokens for HttpError<'a> {
4444

4545
if let Some(attribute) = self.data.attribute() {
4646
if attribute.axum {
47+
let hook = attribute.axum_hook.as_ref().map(|hook| {
48+
quote! {
49+
#hook(&self);
50+
}
51+
});
52+
4753
tokens.append_all(quote! {
4854
#[automatically_derived]
4955
impl #impl_generics ::axum::response::IntoResponse for #ident #type_generics #where_clause {
5056
fn into_response(self) -> ::axum::response::Response {
57+
#hook;
58+
5159
(self.status(), ::axum::Json(self)).into_response()
5260
}
5361
}
Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,69 @@
11
use proc_macro2::TokenStream;
22
use quote::quote;
3-
use syn::{Attribute, Error, Result, Type, spanned::Spanned};
3+
use syn::{Attribute, Error, Expr, Result, Type, spanned::Spanned};
44

55
use crate::status::Status;
66

77
pub struct HttpErrorAttribute {
88
pub status: Option<Status>,
9+
}
10+
11+
impl<'a> HttpErrorAttribute {
12+
pub fn parse_slice(input: &'a [Attribute]) -> Result<Option<Self>> {
13+
let mut result = None;
14+
15+
for attribute in input {
16+
if !attribute.meta.path().is_ident("http") {
17+
continue;
18+
}
19+
20+
if result.is_some() {
21+
return Err(Error::new(
22+
attribute.span(),
23+
"only a single `http` attribute is allowed",
24+
));
25+
}
26+
27+
result = Some(Self::parse(attribute)?);
28+
}
29+
30+
Ok(result)
31+
}
32+
33+
pub fn parse(attribute: &'a Attribute) -> Result<Self> {
34+
let mut status = None;
35+
36+
attribute.parse_nested_meta(|meta| {
37+
if meta.path.is_ident("status") {
38+
status = Some(meta.value()?.parse()?);
39+
40+
Ok(())
41+
} else {
42+
Err(meta.error("unknown parameter"))
43+
}
44+
})?;
45+
46+
Ok(Self { status })
47+
}
48+
49+
pub fn status(&self) -> TokenStream {
50+
status(self.status.as_ref())
51+
}
52+
53+
pub fn responses(&self, r#type: Option<TokenStream>) -> TokenStream {
54+
responses(self.status.as_ref(), r#type)
55+
}
56+
}
57+
58+
pub struct HttpErrorDataAttribute {
59+
pub status: Option<Status>,
960
pub base: Option<Type>,
1061
pub axum: bool,
62+
pub axum_hook: Option<Expr>,
1163
pub utoipa: bool,
1264
}
1365

14-
impl<'a> HttpErrorAttribute {
66+
impl<'a> HttpErrorDataAttribute {
1567
pub fn parse_slice(input: &'a [Attribute]) -> Result<Option<Self>> {
1668
let mut result = None;
1769

@@ -37,6 +89,7 @@ impl<'a> HttpErrorAttribute {
3789
let mut status = None;
3890
let mut base = None;
3991
let mut axum = false;
92+
let mut axum_hook = None;
4093
let mut utoipa = false;
4194

4295
attribute.parse_nested_meta(|meta| {
@@ -51,6 +104,10 @@ impl<'a> HttpErrorAttribute {
51104
} else if meta.path.is_ident("axum") {
52105
axum = true;
53106

107+
Ok(())
108+
} else if meta.path.is_ident("axum_hook") {
109+
axum_hook = Some(meta.value()?.parse()?);
110+
54111
Ok(())
55112
} else if meta.path.is_ident("utoipa") {
56113
utoipa = true;
@@ -65,50 +122,59 @@ impl<'a> HttpErrorAttribute {
65122
status,
66123
base,
67124
axum,
125+
axum_hook,
68126
utoipa,
69127
})
70128
}
71129

72130
pub fn status(&self) -> TokenStream {
73-
if let Some(status) = &self.status {
74-
let status = status.as_ident();
75-
76-
quote!(::breach::http::StatusCode::#status)
77-
} else {
78-
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
79-
}
131+
status(self.status.as_ref())
80132
}
81133

82134
pub fn responses(&self, r#type: Option<TokenStream>) -> TokenStream {
83-
if let Some(status) = &self.status {
84-
let code = status.code.as_str();
85-
86-
let content = r#type.map(|r#type| {
87-
// TODO: Attempt to infer content type from schema?
88-
quote! {
89-
.content(
90-
"application/json",
91-
::utoipa::openapi::content::ContentBuilder::new()
92-
.schema(Some(<#r#type as ::utoipa::PartialSchema>::schema()))
93-
.build()
94-
)
95-
}
96-
});
135+
responses(self.status.as_ref(), r#type)
136+
}
137+
}
138+
139+
fn status(status: Option<&Status>) -> TokenStream {
140+
if let Some(status) = status {
141+
let status = status.as_ident();
97142

143+
quote!(::breach::http::StatusCode::#status)
144+
} else {
145+
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
146+
}
147+
}
148+
149+
fn responses(status: Option<&Status>, r#type: Option<TokenStream>) -> TokenStream {
150+
if let Some(status) = status {
151+
let code = status.code.as_str();
152+
153+
let content = r#type.map(|r#type| {
154+
// TODO: Attempt to infer content type from schema?
98155
quote! {
99-
::std::collections::BTreeMap::from_iter([
100-
(
101-
#code.to_owned(),
102-
::utoipa::openapi::RefOr::T(
103-
::utoipa::openapi::response::ResponseBuilder::new()
104-
#content
105-
.build()
106-
),
107-
),
108-
])
156+
.content(
157+
"application/json",
158+
::utoipa::openapi::content::ContentBuilder::new()
159+
.schema(Some(<#r#type as ::utoipa::PartialSchema>::schema()))
160+
.build()
161+
)
109162
}
110-
} else {
111-
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
163+
});
164+
165+
quote! {
166+
::std::collections::BTreeMap::from_iter([
167+
(
168+
#code.to_owned(),
169+
::utoipa::openapi::RefOr::T(
170+
::utoipa::openapi::response::ResponseBuilder::new()
171+
#content
172+
.build()
173+
),
174+
),
175+
])
112176
}
177+
} else {
178+
quote!(compile_error!("missing `#[http(status = ..)]` attribute"))
113179
}
114180
}

packages/breach-macros/src/http/data.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
22
use syn::{Data, DeriveInput, Result};
33

44
use crate::http::{
5-
attribute::HttpErrorAttribute, r#enum::HttpErrorEnum, r#struct::HttpErrorStruct,
5+
attribute::HttpErrorDataAttribute, r#enum::HttpErrorEnum, r#struct::HttpErrorStruct,
66
union::HttpErrorUnion,
77
};
88

@@ -21,7 +21,7 @@ impl<'a> HttpErrorData<'a> {
2121
})
2222
}
2323

24-
pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
24+
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
2525
match self {
2626
HttpErrorData::Struct(r#struct) => r#struct.attribute(),
2727
HttpErrorData::Enum(r#enum) => r#enum.attribute(),

packages/breach-macros/src/http/enum.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@ use proc_macro2::TokenStream;
22
use quote::{ToTokens, quote};
33
use syn::{DataEnum, DeriveInput, Error, Field, Fields, Ident, Result, Variant, spanned::Spanned};
44

5-
use crate::http::attribute::HttpErrorAttribute;
5+
use crate::http::attribute::{HttpErrorAttribute, HttpErrorDataAttribute};
66

77
pub struct HttpErrorEnum<'a> {
88
ident: &'a Ident,
99
variants: Vec<HttpErrorEnumVariant<'a>>,
10-
attribute: Option<HttpErrorAttribute>,
10+
attribute: Option<HttpErrorDataAttribute>,
1111
}
1212

1313
impl<'a> HttpErrorEnum<'a> {
1414
pub fn parse(input: &'a DeriveInput, data: &'a DataEnum) -> Result<Self> {
1515
let mut result = HttpErrorEnum {
1616
ident: &input.ident,
1717
variants: Vec::with_capacity(data.variants.len()),
18-
attribute: HttpErrorAttribute::parse_slice(&input.attrs)?,
18+
attribute: HttpErrorDataAttribute::parse_slice(&input.attrs)?,
1919
};
2020

2121
for variant in &data.variants {
@@ -27,7 +27,7 @@ impl<'a> HttpErrorEnum<'a> {
2727
Ok(result)
2828
}
2929

30-
pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
30+
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
3131
self.attribute.as_ref()
3232
}
3333

packages/breach-macros/src/http/struct.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ use proc_macro2::TokenStream;
22
use quote::quote;
33
use syn::{DataStruct, DeriveInput, Error, Result, spanned::Spanned};
44

5-
use crate::http::attribute::HttpErrorAttribute;
5+
use crate::http::attribute::HttpErrorDataAttribute;
66

77
pub struct HttpErrorStruct {
8-
attribute: HttpErrorAttribute,
8+
attribute: HttpErrorDataAttribute,
99
}
1010

1111
impl<'a> HttpErrorStruct {
1212
pub fn parse(input: &'a DeriveInput, _data: &'a DataStruct) -> Result<Self> {
13-
let Some(attribute) = HttpErrorAttribute::parse_slice(&input.attrs)? else {
13+
let Some(attribute) = HttpErrorDataAttribute::parse_slice(&input.attrs)? else {
1414
return Err(Error::new(input.span(), "missing http attribute"));
1515
};
1616

1717
Ok(HttpErrorStruct { attribute })
1818
}
1919

20-
pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
20+
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
2121
Some(&self.attribute)
2222
}
2323

packages/breach-macros/src/http/union.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use proc_macro2::TokenStream;
22
use syn::{DataUnion, DeriveInput, Result};
33

4-
use crate::http::attribute::HttpErrorAttribute;
4+
use crate::http::attribute::HttpErrorDataAttribute;
55

66
pub struct HttpErrorUnion {}
77

@@ -10,7 +10,7 @@ impl HttpErrorUnion {
1010
Err(syn::Error::new_spanned(input, "union is not supported"))
1111
}
1212

13-
pub fn attribute(&self) -> Option<&HttpErrorAttribute> {
13+
pub fn attribute(&self) -> Option<&HttpErrorDataAttribute> {
1414
None
1515
}
1616

0 commit comments

Comments
 (0)