poem_openapi_problemdetails_macro/
lib.rs1use 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))]
13pub 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}