Skip to main content

poem_openapi_problemdetails_macro/
lib.rs

1//! # `poem-openapi-problemdetails-macro`
2
3use darling::{
4    FromDeriveInput, FromVariant,
5    ast::{Data, Fields},
6    util::Ignored,
7};
8use proc_macro::TokenStream;
9use proc_macro2::Span;
10use syn::{Attribute, DeriveInput, Error, Expr, ExprLit, Ident, Lit, Meta, parse_macro_input};
11
12#[proc_macro_derive(ApiProblemDetails, attributes(oai_problemdetails))]
13/// Macro to generate problemdetails responses based on the provided macro input. See the root
14/// libary for documentation.
15pub fn derive_response(input: TokenStream) -> TokenStream {
16    let args = parse_macro_input!(input as DeriveInput);
17
18    match generate(args) {
19        Ok(stream) => stream,
20        Err(err) => err.write_errors().into(),
21    }
22}
23
24fn generate(args: DeriveInput) -> Result<TokenStream, GeneratorError> {
25    let args = ResponseArgs::from_derive_input(&args)?;
26
27    let mut statuses = vec![];
28    let mut responses_meta = vec![];
29    let mut as_response = vec![];
30
31    let ident = args.ident;
32
33    let items = match args.data {
34        Data::Enum(items) => items,
35        Data::Struct(_) => {
36            return Err(Error::new_spanned(
37                ident,
38                "error_response can only be applied to an enum.",
39            )
40            .into());
41        }
42    };
43
44    for item in items {
45        let item_ident = item.ident.clone();
46        let status = get_status(item_ident.span(), &item.status)?;
47        let fields = item
48            .fields
49            .iter()
50            .map(|_| quote::quote! { _ })
51            .collect::<Vec<_>>();
52
53        let match_pattern = match item.fields.style {
54            darling::ast::Style::Tuple => quote::quote! { #ident::#item_ident ( #(#fields),* ) },
55            darling::ast::Style::Struct => quote::quote! { #ident::#item_ident { .. } },
56            darling::ast::Style::Unit => quote::quote! { #ident::#item_ident },
57        };
58
59        statuses.push(quote::quote! {
60			#match_pattern => ::poem_openapi_problemdetails::poem::http::StatusCode::from_u16(#status).expect("Provided an invalid statuscode")
61		});
62
63        let description = get_description(&item.attrs)?
64            .map(|tokens| quote::quote! { Some(#tokens) })
65            .unwrap_or_else(|| quote::quote! { None::<&'static str> });
66
67        let title = get_field(&item.title).unwrap_or_default();
68        let title_property = schema_string("title", &title);
69
70        let error_type = get_field(&item.ty).unwrap_or_else(|| {
71            quote::quote! { "about:blank" }
72        });
73
74        let error_type_property = schema_string("type", &error_type);
75
76        let status_property = schema_number("status", &status);
77
78        let detail = get_field(&item.detail).unwrap_or_default();
79        let detail_property = schema_string("detail", &detail);
80
81        responses_meta.push(quote::quote! {
82            ::poem_openapi_problemdetails::poem_openapi::registry::MetaResponse {
83                description: #description.unwrap_or_default(),
84                status: ::std::option::Option::Some(#status),
85                content: ::std::vec![
86                    ::poem_openapi_problemdetails::poem_openapi::registry::MetaMediaType {
87                        content_type: "application/problem+json",
88                        schema: ::poem_openapi_problemdetails::poem_openapi::registry::MetaSchemaRef::Inline(
89                            ::std::boxed::Box::new(::poem_openapi_problemdetails::poem_openapi::registry::MetaSchema {
90                                properties: ::std::vec![
91                                    #error_type_property
92                                    #status_property
93                                    #title_property
94                                    #detail_property
95                                ],
96                                ..::poem_openapi_problemdetails::poem_openapi::registry::MetaSchema::ANY
97                            })
98                        )
99                    }
100                ],
101                status_range: None,
102                headers: ::std::vec![],
103            }
104        });
105
106        let with_title = if title.is_empty() {
107            proc_macro2::TokenStream::default()
108        } else {
109            quote::quote! {
110                .with_title(#title)
111            }
112        };
113
114        let with_detail = if detail.is_empty() {
115            proc_macro2::TokenStream::default()
116        } else {
117            quote::quote! {
118                .with_detail(#detail)
119            }
120        };
121
122        as_response.push(quote::quote! {
123            #match_pattern => {
124                ::poem_openapi_problemdetails::problemdetails::new(::poem_openapi_problemdetails::poem::http::StatusCode::from_u16(#status).expect("An invalid status code was provided"))
125                    .with_type(#error_type)
126                    .with_value("status", #status)
127                    #with_title
128                    #with_detail
129                    .into_response()
130            }
131        });
132    }
133
134    let stream = quote::quote! {
135        impl ::poem_openapi_problemdetails::poem_openapi::ApiResponse for #ident {
136            fn meta() -> ::poem_openapi_problemdetails::poem_openapi::registry::MetaResponses {
137                ::poem_openapi_problemdetails::poem_openapi::registry::MetaResponses {
138                    responses: ::std::vec![#(#responses_meta),*]
139                }
140            }
141
142            fn register(registry: &mut ::poem_openapi_problemdetails::poem_openapi::registry::Registry) {
143                <::poem_openapi_problemdetails::poem_openapi::payload::Json<::std::vec::Vec::<u8>> as ::poem_openapi_problemdetails::poem_openapi::ResponseContent>::register(registry);
144            }
145        }
146
147        impl ::poem_openapi_problemdetails::poem::error::ResponseError for #ident {
148            fn status(&self) -> ::poem_openapi_problemdetails::poem::http::StatusCode {
149                match &self {
150                    #(#statuses),*
151                }
152            }
153
154            fn as_response(&self) -> ::poem_openapi_problemdetails::poem::Response {
155                use ::poem_openapi_problemdetails::poem::IntoResponse;
156
157                match &self {
158                    #(#as_response),*
159                }
160            }
161        }
162    };
163
164    Ok(stream.into())
165}
166
167#[derive(Debug, FromDeriveInput)]
168#[darling(attributes(oai_problemdetails), forward_attrs(doc))]
169struct ResponseArgs {
170    ident: Ident,
171    data: Data<ResponseItem, Ignored>,
172}
173
174#[derive(Debug, FromVariant)]
175#[darling(attributes(oai_problemdetails), forward_attrs(doc))]
176struct ResponseItem {
177    ident: Ident,
178    attrs: Vec<Attribute>,
179    fields: Fields<()>,
180    status: LitOrPath<u16>,
181    #[darling(default)]
182    title: Option<LitOrPath<String>>,
183    #[darling(default)]
184    detail: Option<LitOrPath<String>>,
185    #[darling(default)]
186    ty: Option<LitOrPath<String>>,
187}
188
189#[derive(Debug, thiserror::Error)]
190pub(crate) enum GeneratorError {
191    #[error("{0}")]
192    Syn(#[from] syn::Error),
193    #[error("{0}")]
194    Darling(#[from] darling::Error),
195}
196
197impl GeneratorError {
198    pub(crate) fn write_errors(self) -> proc_macro2::TokenStream {
199        match self {
200            GeneratorError::Syn(err) => err.to_compile_error(),
201            GeneratorError::Darling(err) => err.write_errors(),
202        }
203    }
204}
205
206#[derive(Debug)]
207pub(crate) enum LitOrPath<T> {
208    Lit(T),
209    Path(syn::Path),
210}
211
212impl<T> darling::FromMeta for LitOrPath<T>
213where
214    T: darling::FromMeta,
215{
216    fn from_nested_meta(item: &darling::ast::NestedMeta) -> darling::Result<Self> {
217        T::from_nested_meta(item)
218            .map(Self::Lit)
219            .or_else(|_| syn::Path::from_nested_meta(item).map(Self::Path))
220    }
221
222    fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
223        T::from_meta(item)
224            .map(Self::Lit)
225            .or_else(|_| syn::Path::from_meta(item).map(Self::Path))
226    }
227
228    fn from_none() -> Option<Self> {
229        T::from_none()
230            .map(Self::Lit)
231            .or_else(|| syn::Path::from_none().map(Self::Path))
232    }
233
234    fn from_word() -> darling::Result<Self> {
235        T::from_word()
236            .map(Self::Lit)
237            .or_else(|_| syn::Path::from_word().map(Self::Path))
238    }
239
240    fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result<Self> {
241        T::from_list(items)
242            .map(Self::Lit)
243            .or_else(|_| syn::Path::from_list(items).map(Self::Path))
244    }
245
246    fn from_value(value: &Lit) -> darling::Result<Self> {
247        T::from_value(value)
248            .map(Self::Lit)
249            .or_else(|_| syn::Path::from_value(value).map(Self::Path))
250    }
251
252    fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
253        T::from_expr(expr)
254            .map(Self::Lit)
255            .or_else(|_| syn::Path::from_expr(expr).map(Self::Path))
256    }
257
258    fn from_char(value: char) -> darling::Result<Self> {
259        T::from_char(value)
260            .map(Self::Lit)
261            .or_else(|_| syn::Path::from_char(value).map(Self::Path))
262    }
263
264    fn from_string(value: &str) -> darling::Result<Self> {
265        T::from_string(value)
266            .map(Self::Lit)
267            .or_else(|_| syn::Path::from_string(value).map(Self::Path))
268    }
269
270    fn from_bool(value: bool) -> darling::Result<Self> {
271        T::from_bool(value)
272            .map(Self::Lit)
273            .or_else(|_| syn::Path::from_bool(value).map(Self::Path))
274    }
275}
276
277fn get_status(
278    span: Span,
279    status: &LitOrPath<u16>,
280) -> Result<proc_macro2::TokenStream, GeneratorError> {
281    match status {
282        LitOrPath::Lit(status) => {
283            if !(100..1000).contains(status) {
284                return Err(Error::new(
285                    span,
286                    "Invalid status code, it must be greater or equal to 100 and less than 1000.",
287                )
288                .into());
289            }
290            Ok(quote::quote!(#status))
291        }
292        LitOrPath::Path(ident) => Ok(quote::quote!(#ident)),
293    }
294}
295
296pub(crate) fn get_description(attrs: &[Attribute]) -> Result<Option<String>, GeneratorError> {
297    let mut full_docs = String::new();
298    for attr in attrs {
299        if attr.path().is_ident("doc") {
300            if let Meta::NameValue(nv) = &attr.meta {
301                if let Expr::Lit(ExprLit {
302                    lit: Lit::Str(doc), ..
303                }) = &nv.value
304                {
305                    let doc = doc.value();
306                    let doc_str = doc.trim();
307                    if !full_docs.is_empty() {
308                        full_docs += "\n";
309                    }
310                    full_docs += doc_str;
311                }
312            }
313        }
314    }
315    Ok(if full_docs.is_empty() {
316        None
317    } else {
318        Some(full_docs)
319    })
320}
321
322fn get_field(field: &Option<LitOrPath<String>>) -> Option<proc_macro2::TokenStream> {
323    match field {
324        Some(LitOrPath::Lit(lit)) => Some(quote::quote!(#lit)),
325        Some(LitOrPath::Path(path)) => Some(quote::quote!(#path)),
326        None => None,
327    }
328}
329
330fn schema_string(name: &'static str, value: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
331    if value.is_empty() {
332        proc_macro2::TokenStream::default()
333    } else {
334        quote::quote! {
335            (#name, ::poem_openapi_problemdetails::poem_openapi::registry::MetaSchemaRef::Inline(
336                ::std::boxed::Box::new(::poem_openapi_problemdetails::poem_openapi::registry::MetaSchema {
337                    ty: "string",
338                    enum_items: ::std::vec![
339                        ::poem_openapi_problemdetails::serde_json::Value::String(#value.into())
340                    ],
341                    ..::poem_openapi_problemdetails::poem_openapi::registry::MetaSchema::ANY
342                })
343            )),
344        }
345    }
346}
347
348fn schema_number(name: &'static str, value: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
349    if value.is_empty() {
350        proc_macro2::TokenStream::default()
351    } else {
352        quote::quote! {
353            (#name, ::poem_openapi_problemdetails::poem_openapi::registry::MetaSchemaRef::Inline(
354                ::std::boxed::Box::new(::poem_openapi_problemdetails::poem_openapi::registry::MetaSchema {
355                    ty: "number",
356                    enum_items: ::std::vec![
357                        ::poem_openapi_problemdetails::serde_json::Value::Number(#value.into())
358                    ],
359                    ..::poem_openapi_problemdetails::poem_openapi::registry::MetaSchema::ANY
360                })
361            )),
362        }
363    }
364}