utoipa_helper_macro/
lib.rs1use 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));
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