Skip to content

Commit

Permalink
feat: ability to specifiy details for the response
Browse files Browse the repository at this point in the history
  • Loading branch information
enzious committed Jun 13, 2024
1 parent df0d146 commit 518296b
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 9 deletions.
157 changes: 148 additions & 9 deletions actix-web-thiserror-derive/src/response_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,31 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
};

#[allow(clippy::type_complexity)]
let (forwards, _internals, mut status_map, mut reason_map): (
let (forwards, _internals, mut status_map, mut reason_map, mut type_map, mut details_map): (
HashSet<proc_macro2::Ident>,
HashSet<proc_macro2::Ident>,
HashMap<proc_macro2::Ident, proc_macro2::TokenStream>,
HashMap<proc_macro2::Ident, proc_macro2::TokenStream>,
HashMap<proc_macro2::Ident, proc_macro2::TokenStream>,
HashMap<proc_macro2::Ident, proc_macro2::TokenStream>,
) = variants.iter().fold(
(
HashSet::new(),
HashSet::new(),
HashMap::new(),
HashMap::new(),
HashMap::new(),
HashMap::new(),
),
|(
mut forwards,
mut internals,
mut status_map,
mut reason_map,
mut type_map,
mut details_map,
),
|(mut forwards, mut internals, mut status_map, mut reason_map), variant| {
variant| {
let variant_ident = variant.ident.to_owned();

for attr in &variant.attrs {
Expand Down Expand Up @@ -95,6 +107,28 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
}
}

"type" => {
let _type = get_type(&mut tokens);

match _type {
Some(_type) => {
type_map.insert(variant_ident.to_owned(), _type);
}
_ => panic!("Invalid `type` in #[response]"),
}
}

"details" => {
let details = get_details(&mut tokens);

match details {
Some(details) => {
details_map.insert(variant_ident.to_owned(), details);
}
_ => panic!("Invalid `details` in #[response]"),
}
}

_ => {
panic!("Unknown #[response] option: {}", &ident);
}
Expand All @@ -116,7 +150,14 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
}
}

(forwards, internals, status_map, reason_map)
(
forwards,
internals,
status_map,
reason_map,
type_map,
details_map,
)
},
);

Expand Down Expand Up @@ -152,10 +193,57 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
}
};

let (status_code_forwards, reason_forwards) = match forwards.len() {
0 => (None, None),
let type_match = match type_map.len() {
0 => None,
_ => {
let mut streams = vec![quote! { status_code }, quote! { reason }]
let mut body: Vec<proc_macro2::TokenStream> = type_map
.drain()
.map(|(ident, _type)| {
quote! {
#name::#ident { .. } => Some(Some(#_type.to_owned())),
}
})
.collect();

Some(proc_macro2::TokenStream::from_iter(body.drain(..)))
}
};

let details_match = match details_map.len() {
0 => None,
_ => {
let mut body: Vec<proc_macro2::TokenStream> = details_map
.drain()
.map(|(ident, details)| {
let details = Some(details.to_string())
.filter(|details| details.starts_with("\"{0") && details.ends_with("}\""))
.and_then(|details| {
format!("details{}", &details[3..details.len() - 2])
.parse::<proc_macro2::TokenStream>()
.ok()
})
.expect("Failed to find details");

quote! {
#name::#ident(details) => Some(serde_json::to_value(#details).ok()),
}
})
.collect();

Some(proc_macro2::TokenStream::from_iter(body.drain(..)))
}
};

let (status_code_forwards, reason_forwards, type_forwards, details_forwards) =
match forwards.len() {
0 => (None, None, None, None),
_ => {
let mut streams = vec![
quote! { status_code },
quote! { reason },
quote! { type },
quote! { details },
]
.drain(..)
.map(|func| {
proc_macro2::TokenStream::from_iter(forwards.iter().map(|variant| {
Expand All @@ -166,9 +254,14 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
})
.collect::<Vec<_>>();

(Some(streams.remove(0)), Some(streams.remove(0)))
}
};
(
Some(streams.remove(0)),
Some(streams.remove(0)),
Some(streams.remove(0)),
Some(streams.remove(0)),
)
}
};

let transform = ast
.attrs
Expand Down Expand Up @@ -226,6 +319,20 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
_ => None,
}
}

fn _type(&self) -> Option<Option<String>> {
match self {
#type_match
_ => None,
}
}

fn details(&self) -> Option<Option<serde_json::Value>> {
match self {
#details_match
_ => None,
}
}
}

impl actix_web::error::ResponseError for #name {
Expand All @@ -250,13 +357,29 @@ pub fn derive_response_error(input: TokenStream) -> TokenStream {
}
.and_then(|value| value));

let _type: Option<String> = ::actix_web_thiserror::ThiserrorResponse::_type(self)
.unwrap_or(match self {
#type_forwards
_ => None,
}
.and_then(|value| value));

let details: Option<serde_json::Value> = ::actix_web_thiserror::ThiserrorResponse::details(self)
.unwrap_or(match self {
#details_forwards
_ => None,
}
.and_then(|value| value));

log::error!("Response error: {err}\n\t{name}({err:?})", name = #name_str, err = &self);

#transform(
#name_str,
&self,
self.status_code(),
reason,
_type,
details,
)
}
}
Expand Down Expand Up @@ -372,3 +495,19 @@ fn get_reason(tokens: &mut Peekable<IntoIter>) -> Option<proc_macro2::TokenStrea
_ => None,
}
}

fn get_type(tokens: &mut Peekable<IntoIter>) -> Option<proc_macro2::TokenStream> {
match tokens.peek() {
Some(TokenTree::Literal(_)) => get_string(tokens).map(|tokens| tokens.into()),

_ => None,
}
}

fn get_details(tokens: &mut Peekable<IntoIter>) -> Option<proc_macro2::TokenStream> {
match tokens.peek() {
Some(TokenTree::Literal(_)) => get_string(tokens).map(|tokens| tokens.into()),

_ => None,
}
}
14 changes: 14 additions & 0 deletions actix-web-thiserror/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ pub trait ResponseTransform {
err: &dyn std::error::Error,
status_code: actix_web::http::StatusCode,
reason: Option<serde_json::Value>,
_type: Option<String>,
details: Option<serde_json::Value>,
) -> HttpResponse {
actix_web::HttpResponse::build(status_code).finish()
}
Expand Down Expand Up @@ -121,13 +123,17 @@ pub fn apply_global_transform(
err: &dyn std::error::Error,
status_code: actix_web::http::StatusCode,
reason: Option<serde_json::Value>,
_type: Option<String>,
details: Option<serde_json::Value>,
) -> HttpResponse {
ResponseTransform::transform(
(**RESPONSE_TRANSFORM.load()).as_ref(),
name,
err,
status_code,
reason,
_type,
details,
)
}

Expand All @@ -145,6 +151,14 @@ pub trait ThiserrorResponse {
fn reason(&self) -> Option<Option<serde_json::Value>> {
None
}

fn _type(&self) -> Option<Option<String>> {
None
}

fn details(&self) -> Option<Option<serde_json::Value>> {
None
}
}

#[allow(unused_imports)]
Expand Down

0 comments on commit 518296b

Please sign in to comment.