utoipa_helper_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    spanned::Spanned, Data, DeriveInput, Expr, Fields, Lit, Meta, PathArguments, Token, Type,
5    TypePath,
6};
7
8#[proc_macro_derive(UtoipaResponse, attributes(response))]
9pub fn derive_utoipa_response_fn(input: TokenStream) -> TokenStream {
10    #[derive(Default, Debug)]
11    struct UtoipaResponse {
12        description: Option<String>,
13        content: Option<String>,
14        status: Option<String>,
15        error: Option<String>,
16    }
17    let mut utoipa_response = UtoipaResponse::default();
18    let input: DeriveInput = syn::parse(input).expect("Failed to parse");
19    let DeriveInput {
20        attrs, ident, data, ..
21    } = input;
22    for attr in &attrs {
23        if attr.meta.path().is_ident("response") {
24            if let Meta::List(metalist) = &attr.meta {
25                metalist
26                    .parse_nested_meta(|meta| {
27                        if let Some(ident) = meta.path.get_ident() {
28                            let ident = ident.to_string();
29                            if let Expr::Lit(lit) = meta.value()?.parse::<Expr>()? {
30                                if let Lit::Str(lit) = lit.lit {
31                                    let lit = Some(lit.value());
32                                    match ident.as_str() {
33                                        "description" => utoipa_response.description = lit,
34                                        "content" => utoipa_response.content = lit,
35                                        "status" => utoipa_response.status = lit,
36                                        "error" => utoipa_response.error = lit,
37                                        id => panic!("{} is not a valid key", id),
38                                    }
39                                }
40                            }
41                        }
42                        Ok(())
43                    })
44                    .map_err(|e| panic!("encountered error {}", e))
45                    .unwrap();
46            }
47        }
48    }
49    let mut inner_type: Option<TypePath> = None;
50    if let Data::Struct(data_struct) = data {
51        if let Fields::Unnamed(fields) = data_struct.fields {
52            if let Some(first) = fields.unnamed.first() {
53                if let Type::Path(typath) = &first.ty {
54                    inner_type = Some(typath.clone());
55                }
56            }
57        }
58    }
59    let inner_type = inner_type.expect("No inner type");
60    let mut inner_type_mod = inner_type.clone();
61    if let Some(first) = inner_type_mod.path.segments.first_mut() {
62        if let PathArguments::AngleBracketed(args) = &mut first.arguments {
63            args.colon2_token = Some(Token![::](args.span()));
64        }
65    }
66    let from_impl = quote! {
67        impl From<#inner_type> for #ident {
68            fn from(item: #inner_type) -> Self {
69                Self(item)
70            }
71        }
72    };
73    let content = match utoipa_response.content.as_deref() {
74        Some("text/html") => Some(quote! {utoipa_helper::content_type_trait::ContentTypeHtml}),
75        Some("text/css") => Some(quote! {utoipa_helper::content_type_trait::ContentTypeCss}),
76        Some("text/javascript") => Some(quote! {utoipa_helper::content_type_trait::ContentTypeJs}),
77        Some("application/json") => Some(quote! {utoipa_helper::content_type_trait::ContentTypeJson}),
78        Some(val) => panic!("{} is not a valid content type", val),
79        None => None,
80    };
81    let status = match utoipa_response.status.as_deref() {
82        Some("OK") => Some(quote! {utoipa_helper::status_code_trait::StatusCodeOk}),
83        Some("CREATED") => Some(quote! {utoipa_helper::status_code_trait::StatusCodeCreated}),
84        Some("NO_CONTENT") => Some(quote!(utoipa_helper::status_code_trait::StatusCodeNoContent)),
85        Some(s) => s
86            .parse::<u16>()
87            .ok()
88            .map(|c| quote!(utoipa_helper::status_code_trait::StatusCodeValue::<#c>)),
89        _ => None,
90    };
91    let content_reply = if let Some(content) = &content {
92        quote! {
93            use utoipa_helper::content_type_trait::ContentTypeTrait;
94            res.headers_mut().insert(
95                axum::http::header::CONTENT_TYPE ,
96                axum::http::HeaderValue::from_static( #content::content_type_header() )
97            );
98        }
99    } else {
100        quote! {}
101    };
102    let status_reply = if let Some(status) = &status {
103        quote! {
104            use utoipa_helper::status_code_trait::StatusCodeTrait;
105            *res.status_mut() = #status::status_code();
106        }
107    } else {
108        quote! {}
109    };
110    let axum_into_response_impl = quote! {
111        impl axum::response::IntoResponse for #ident {
112            fn into_response(self) -> axum::response::Response {
113                let mut res = self.0.into_response();
114                #content_reply
115                #status_reply
116                res
117            }
118        }
119    };
120    let content_response_entity = if let Some(content) = &content {
121        quote! {
122            use utoipa_helper::content_type_trait::ContentTypeTrait;
123            content_type = #content::content_type().into();
124        }
125    } else {
126        quote! {}
127    };
128    let description_response_entity = if let Some(description) = &utoipa_response.description {
129        quote! {
130            resp = resp.description(#description);
131        }
132    } else {
133        quote! {}
134    };
135    let status_response_entity = if let Some(status) = &status {
136        quote! {
137            use utoipa_helper::status_code_trait::StatusCodeTrait;
138            code = #status::status_code().as_u16().to_string().into();
139        }
140    } else {
141        quote! {}
142    };
143    let utoipa_into_responses_impl = quote! {
144        impl utoipa::IntoResponses for #ident {
145            fn responses() -> std::collections::BTreeMap<String, utoipa::openapi::RefOr<utoipa::openapi::Response>> {
146                let mut responses = utoipa::openapi::ResponsesBuilder::new();
147                let mut resp = utoipa::openapi::ResponseBuilder::new();
148                let mut code = std::borrow::Cow::Borrowed("200");
149                let mut content_type = std::borrow::Cow::Borrowed("text/html");
150                #status_response_entity
151                #content_response_entity
152                let content = utoipa::openapi::content::ContentBuilder::new().schema(Some(#inner_type::schema())).build();
153                resp = resp.content(content_type, content);    
154                #description_response_entity
155                responses.response(code, resp).build().into()
156            }
157        }
158    };
159    let tokens = quote! {
160        #from_impl
161        #axum_into_response_impl
162        #utoipa_into_responses_impl
163    };
164    tokens.into()
165}
166