From e9fb1fd184b56c2df002e7645d71dd45a242be6f Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Tue, 17 Dec 2024 23:15:00 +0100 Subject: [PATCH 1/7] Backport OpenApiResponder macro from personal project --- .gitignore | 3 +- rocket-okapi-codegen/src/lib.rs | 12 ++ .../src/responder_derive/attribute.rs | 73 ++++++++++++ .../src/responder_derive/mod.rs | 106 ++++++++++++++++++ .../src/responder_derive/utils.rs | 43 +++++++ rocket-okapi/src/util.rs | 9 ++ 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 rocket-okapi-codegen/src/responder_derive/attribute.rs create mode 100644 rocket-okapi-codegen/src/responder_derive/mod.rs create mode 100644 rocket-okapi-codegen/src/responder_derive/utils.rs diff --git a/.gitignore b/.gitignore index 66046e11..9befdf41 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/*.rs.bk Cargo.lock /.idea -.vscode \ No newline at end of file +.vscode +.DS_Store diff --git a/rocket-okapi-codegen/src/lib.rs b/rocket-okapi-codegen/src/lib.rs index 31a3b7e9..128ea348 100644 --- a/rocket-okapi-codegen/src/lib.rs +++ b/rocket-okapi-codegen/src/lib.rs @@ -12,6 +12,7 @@ mod openapi_attr; mod openapi_spec; mod parse_routes; +mod responder_derive; use proc_macro::TokenStream; use quote::quote; @@ -136,6 +137,17 @@ pub fn open_api_from_request_derive(input: TokenStream) -> TokenStream { gen.into() } +/// TODO +#[proc_macro_derive(OpenApiResponder, attributes(responder))] +pub fn open_api_responder_derive(input: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + + match responder_derive::derive(ast) { + Ok(v) => v.into(), + Err(err) => err.to_compile_error().into(), + } +} + fn get_add_operation_fn_name(route_fn_name: &Ident) -> Ident { Ident::new( &format!("okapi_add_operation_for_{}_", route_fn_name), diff --git a/rocket-okapi-codegen/src/responder_derive/attribute.rs b/rocket-okapi-codegen/src/responder_derive/attribute.rs new file mode 100644 index 00000000..fe1c7364 --- /dev/null +++ b/rocket-okapi-codegen/src/responder_derive/attribute.rs @@ -0,0 +1,73 @@ +use quote::ToTokens; +use syn::{parse::Parse, Error, LitInt, LitStr, Token}; + +mod kw { + syn::custom_keyword!(ignore); + syn::custom_keyword!(status); + syn::custom_keyword!(content_type); +} + +#[derive(Default)] +pub(crate) struct ResponseAttribute { + pub ignore: bool, + pub status: Status, + pub content_type: Option, +} + +#[derive(Default)] +#[repr(transparent)] +pub(crate) struct Status(pub rocket_http::Status); + +impl ToTokens for Status { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.code.to_tokens(tokens); + } +} + +#[derive(Default)] +#[repr(transparent)] +pub(crate) struct ContentType(pub rocket_http::ContentType); + +impl ToTokens for ContentType { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_string().to_tokens(tokens); + } +} + +impl Parse for ResponseAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut out = Self::default(); + loop { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::ignore) { + input.parse::()?; + out.ignore = true; + } else if lookahead.peek(kw::status) { + input.parse::()?; + input.parse::()?; + let code = input.parse::()?.base10_parse::()?; + out.status = Status(rocket_http::Status::new(code)); + } else if lookahead.peek(kw::content_type) { + input.parse::()?; + input.parse::()?; + let value = input.parse::()?; + let content_type = value + .value() + .parse() + .map_err(|err| Error::new(value.span(), err))?; + out.content_type = Some(ContentType(content_type)); + } else if input.is_empty() { + break; + } else { + return Err(lookahead.error()); + } + + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + Ok(out) + } +} diff --git a/rocket-okapi-codegen/src/responder_derive/mod.rs b/rocket-okapi-codegen/src/responder_derive/mod.rs new file mode 100644 index 00000000..53a54519 --- /dev/null +++ b/rocket-okapi-codegen/src/responder_derive/mod.rs @@ -0,0 +1,106 @@ +mod attribute; +mod utils; + +use attribute::ResponseAttribute; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{spanned::Spanned, Attribute, DeriveInput, Error, Field, Fields, Result}; +use utils::{parse_attribute, parse_doc}; + +pub(crate) fn derive(input: DeriveInput) -> Result { + let variants: Vec<(Vec, Fields)> = match input.data { + syn::Data::Struct(s) => vec![(input.attrs.clone(), s.fields)], + syn::Data::Enum(e) => e + .variants + .into_iter() + .map(|v| (v.attrs, v.fields)) + .collect(), + syn::Data::Union(_) => { + return Err(Error::new(input.span(), "unions are not supported")); + } + }; + + let attrs = input.attrs; + let ident = input.ident; + let generics = input.generics; + + let responses = variants + .into_iter() + .filter_map(|v| variant_to_responses(&attrs, v).transpose()) + .collect::>>()?; + + Ok(quote! { + impl<#generics> ::rocket_okapi::response::OpenApiResponderInner for #ident { + fn responses( + gen: &mut ::rocket_okapi::gen::OpenApiGenerator, + ) -> ::rocket_okapi::Result<::rocket_okapi::okapi::openapi3::Responses> { + let mut responses = ::rocket_okapi::okapi::openapi3::Responses::default(); + #( + responses.responses.extend({ #responses }.responses); + )* + Ok(responses) + } + } + }) +} + +fn variant_to_responses( + global_attrs: &[Attribute], + (attrs, fields): (Vec, Fields), +) -> Result> { + let ResponseAttribute { + ignore, + status, + content_type, + } = parse_response_attribute(&attrs)?; + + if ignore { + return Ok(None); + } + + let Some(Field { + attrs: field_attrs, + ty: field_type, + .. + }) = first_field(fields)? + else { + return Ok(None); + }; + + let set_status = (status.0 != rocket_http::Status::Ok) + .then(|| quote! { ::rocket_okapi::util::set_status_code(&mut r, #status)?; }); + + let set_content_type = + content_type.map(|ct| quote! { ::rocket_okapi::util::set_content_type(&mut r, #ct)?; }); + + let set_description = parse_doc(&field_attrs) + .or_else(|| parse_doc(&attrs)) + .or_else(|| parse_doc(global_attrs)) + .map(|doc| quote! { ::rocket_okapi::util::set_description(&mut r, #doc)?; }); + + let responses = quote! { + let mut r = <#field_type as ::rocket_okapi::response::OpenApiResponderInner>::responses(gen)?; + #set_status + #set_content_type + #set_description + r + }; + + Ok(Some(responses)) +} + +fn first_field(fields: Fields) -> Result> { + for field in fields { + let ResponseAttribute { ignore, .. } = parse_response_attribute(&field.attrs)?; + if !ignore { + return Ok(Some(field)); + } + } + + Ok(None) +} + +#[inline(always)] +fn parse_response_attribute(attrs: &[Attribute]) -> Result { + parse_attribute(&attrs, "response").map(Option::unwrap_or_default) +} diff --git a/rocket-okapi-codegen/src/responder_derive/utils.rs b/rocket-okapi-codegen/src/responder_derive/utils.rs new file mode 100644 index 00000000..599e594e --- /dev/null +++ b/rocket-okapi-codegen/src/responder_derive/utils.rs @@ -0,0 +1,43 @@ +use syn::{parse::Parse, spanned::Spanned, Attribute, Error, Expr, Ident, Lit, Meta, Result}; + +pub(crate) fn parse_doc(attrs: &[Attribute]) -> Option { + let doc = attrs + .iter() + .filter(|a| a.path.is_ident("doc")) + .filter_map(|a| { + if let Meta::NameValue(nv) = &a.parse_meta().ok()? { + if let Lit::Str(str) = &nv.lit { + return Some(str.value().trim().to_owned()); + } + } + None + }) + .collect::>() + .join(" "); + + (!doc.is_empty()).then_some(doc) +} + +pub(crate) fn parse_attribute(attrs: &[Attribute], ident: I) -> Result> +where + Ident: PartialEq, +{ + let attrs = attrs + .iter() + .filter(|a| a.path.is_ident(&ident)) + .collect::>(); + + if attrs.len() > 1 { + return Err(Error::new( + attrs[1].span(), + "this attribute may be specified only once", + )); + } else if attrs.is_empty() { + return Ok(None); + } + + let attr = attrs[0]; + let attr = attr.parse_args::()?; + + Ok(Some(attr)) +} diff --git a/rocket-okapi/src/util.rs b/rocket-okapi/src/util.rs index b19a8c9b..e6e3396f 100644 --- a/rocket-okapi/src/util.rs +++ b/rocket-okapi/src/util.rs @@ -123,6 +123,15 @@ pub fn set_content_type(responses: &mut Responses, content_type: impl ToString) Ok(()) } +/// Replaces the description for all responses. +pub fn set_description(responses: &mut Responses, description: impl ToString) -> Result<()> { + for ref mut resp_refor in responses.responses.values_mut() { + let response = ensure_not_ref(resp_refor)?; + response.description = description.to_string(); + } + Ok(()) +} + /// Adds a `Response` to a `Responses` object with the given status code, Content-Type and `SchemaObject`. pub fn add_schema_response( responses: &mut Responses, From 7b7cabd1550d772762c8bcca9072925f3557ea11 Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Thu, 19 Dec 2024 16:13:33 +0100 Subject: [PATCH 2/7] Move doc_attr.rs from openapi_attr to crate root --- rocket-okapi-codegen/src/{openapi_attr => }/doc_attr.rs | 2 +- rocket-okapi-codegen/src/openapi_attr/mod.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename rocket-okapi-codegen/src/{openapi_attr => }/doc_attr.rs (96%) diff --git a/rocket-okapi-codegen/src/openapi_attr/doc_attr.rs b/rocket-okapi-codegen/src/doc_attr.rs similarity index 96% rename from rocket-okapi-codegen/src/openapi_attr/doc_attr.rs rename to rocket-okapi-codegen/src/doc_attr.rs index bfd6f202..676c390c 100644 --- a/rocket-okapi-codegen/src/openapi_attr/doc_attr.rs +++ b/rocket-okapi-codegen/src/doc_attr.rs @@ -31,7 +31,7 @@ fn merge_description_lines(doc: &str) -> Option { none_if_empty(desc) } -fn get_doc(attrs: &[Attribute]) -> Option { +pub fn get_doc(attrs: &[Attribute]) -> Option { let doc = attrs .iter() .filter_map(|attr| { diff --git a/rocket-okapi-codegen/src/openapi_attr/mod.rs b/rocket-okapi-codegen/src/openapi_attr/mod.rs index 67d88fe0..8a4e4685 100644 --- a/rocket-okapi-codegen/src/openapi_attr/mod.rs +++ b/rocket-okapi-codegen/src/openapi_attr/mod.rs @@ -1,7 +1,6 @@ -mod doc_attr; mod route_attr; -use crate::get_add_operation_fn_name; +use crate::{doc_attr::get_title_and_desc_from_doc, get_add_operation_fn_name}; use darling::FromMeta; use proc_macro::TokenStream; use proc_macro2::Span; @@ -295,7 +294,7 @@ fn create_route_operation_fn( .replace("..>", "}") .replace('>', "}"); let method = Ident::new(&to_pascal_case_string(route.method), Span::call_site()); - let (title, desc) = doc_attr::get_title_and_desc_from_doc(&route_fn.attrs); + let (title, desc) = get_title_and_desc_from_doc(&route_fn.attrs); let title = match title { Some(x) => quote!(Some(#x.to_owned())), None => quote!(None), From e1215dc0f7af4ba9bc64d60b73fa68d9afbc5ebd Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Thu, 19 Dec 2024 16:16:15 +0100 Subject: [PATCH 3/7] Rewrite OpenApiResponder using darling and fix minor bugs Fix generics passthrough and #[response(ignore)] behavior to match rocket --- rocket-okapi-codegen/src/lib.rs | 3 +- .../src/responder_derive/attribute.rs | 73 ------------ .../src/responder_derive/mod.rs | 111 +++++++++--------- .../src/responder_derive/response_attr.rs | 61 ++++++++++ .../src/responder_derive/utils.rs | 43 ------- 5 files changed, 116 insertions(+), 175 deletions(-) delete mode 100644 rocket-okapi-codegen/src/responder_derive/attribute.rs create mode 100644 rocket-okapi-codegen/src/responder_derive/response_attr.rs delete mode 100644 rocket-okapi-codegen/src/responder_derive/utils.rs diff --git a/rocket-okapi-codegen/src/lib.rs b/rocket-okapi-codegen/src/lib.rs index 128ea348..0a823d38 100644 --- a/rocket-okapi-codegen/src/lib.rs +++ b/rocket-okapi-codegen/src/lib.rs @@ -9,6 +9,7 @@ //! - `#[derive(OpenApiFromRequest)]`: Implement `OpenApiFromRequest` trait for a given struct. //! +mod doc_attr; mod openapi_attr; mod openapi_spec; mod parse_routes; @@ -144,7 +145,7 @@ pub fn open_api_responder_derive(input: TokenStream) -> TokenStream { match responder_derive::derive(ast) { Ok(v) => v.into(), - Err(err) => err.to_compile_error().into(), + Err(err) => err.write_errors().into(), } } diff --git a/rocket-okapi-codegen/src/responder_derive/attribute.rs b/rocket-okapi-codegen/src/responder_derive/attribute.rs deleted file mode 100644 index fe1c7364..00000000 --- a/rocket-okapi-codegen/src/responder_derive/attribute.rs +++ /dev/null @@ -1,73 +0,0 @@ -use quote::ToTokens; -use syn::{parse::Parse, Error, LitInt, LitStr, Token}; - -mod kw { - syn::custom_keyword!(ignore); - syn::custom_keyword!(status); - syn::custom_keyword!(content_type); -} - -#[derive(Default)] -pub(crate) struct ResponseAttribute { - pub ignore: bool, - pub status: Status, - pub content_type: Option, -} - -#[derive(Default)] -#[repr(transparent)] -pub(crate) struct Status(pub rocket_http::Status); - -impl ToTokens for Status { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.0.code.to_tokens(tokens); - } -} - -#[derive(Default)] -#[repr(transparent)] -pub(crate) struct ContentType(pub rocket_http::ContentType); - -impl ToTokens for ContentType { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.0.to_string().to_tokens(tokens); - } -} - -impl Parse for ResponseAttribute { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut out = Self::default(); - loop { - let lookahead = input.lookahead1(); - if lookahead.peek(kw::ignore) { - input.parse::()?; - out.ignore = true; - } else if lookahead.peek(kw::status) { - input.parse::()?; - input.parse::()?; - let code = input.parse::()?.base10_parse::()?; - out.status = Status(rocket_http::Status::new(code)); - } else if lookahead.peek(kw::content_type) { - input.parse::()?; - input.parse::()?; - let value = input.parse::()?; - let content_type = value - .value() - .parse() - .map_err(|err| Error::new(value.span(), err))?; - out.content_type = Some(ContentType(content_type)); - } else if input.is_empty() { - break; - } else { - return Err(lookahead.error()); - } - - if input.peek(Token![,]) { - input.parse::()?; - } else { - break; - } - } - Ok(out) - } -} diff --git a/rocket-okapi-codegen/src/responder_derive/mod.rs b/rocket-okapi-codegen/src/responder_derive/mod.rs index 53a54519..be5447d9 100644 --- a/rocket-okapi-codegen/src/responder_derive/mod.rs +++ b/rocket-okapi-codegen/src/responder_derive/mod.rs @@ -1,22 +1,22 @@ -mod attribute; -mod utils; +mod response_attr; -use attribute::ResponseAttribute; +use crate::doc_attr::get_doc; +use darling::{FromMeta, Result}; use proc_macro2::TokenStream; use quote::quote; -use syn::{spanned::Spanned, Attribute, DeriveInput, Error, Field, Fields, Result}; -use utils::{parse_attribute, parse_doc}; +use response_attr::ResponseAttribute; +use syn::{Attribute, DeriveInput, Fields}; -pub(crate) fn derive(input: DeriveInput) -> Result { - let variants: Vec<(Vec, Fields)> = match input.data { - syn::Data::Struct(s) => vec![(input.attrs.clone(), s.fields)], - syn::Data::Enum(e) => e - .variants - .into_iter() - .map(|v| (v.attrs, v.fields)) - .collect(), +pub fn derive(input: DeriveInput) -> Result { + let responses_variants: Vec<(Vec, Fields)> = match input.data { + syn::Data::Struct(syn::DataStruct { fields, .. }) => { + [(input.attrs.clone(), fields)].to_vec() + } + syn::Data::Enum(syn::DataEnum { variants, .. }) => { + variants.into_iter().map(|v| (v.attrs, v.fields)).collect() + } syn::Data::Union(_) => { - return Err(Error::new(input.span(), "unions are not supported")); + return Err(darling::Error::custom("unions are not supported").with_span(&input)); } }; @@ -24,13 +24,15 @@ pub(crate) fn derive(input: DeriveInput) -> Result { let ident = input.ident; let generics = input.generics; - let responses = variants + let responses = responses_variants .into_iter() - .filter_map(|v| variant_to_responses(&attrs, v).transpose()) + .filter_map(|(variant_attrs, variant_fields)| { + variant_to_responses(&attrs, variant_attrs, variant_fields).transpose() + }) .collect::>>()?; Ok(quote! { - impl<#generics> ::rocket_okapi::response::OpenApiResponderInner for #ident { + impl #generics ::rocket_okapi::response::OpenApiResponderInner for #ident #generics { fn responses( gen: &mut ::rocket_okapi::gen::OpenApiGenerator, ) -> ::rocket_okapi::Result<::rocket_okapi::okapi::openapi3::Responses> { @@ -45,62 +47,55 @@ pub(crate) fn derive(input: DeriveInput) -> Result { } fn variant_to_responses( - global_attrs: &[Attribute], - (attrs, fields): (Vec, Fields), + entity_attrs: &[Attribute], + attrs: Vec, + fields: Fields, ) -> Result> { - let ResponseAttribute { - ignore, - status, - content_type, - } = parse_response_attribute(&attrs)?; - - if ignore { - return Ok(None); - } + let response_attribute = get_response_attr_or_default(&attrs)?; - let Some(Field { - attrs: field_attrs, - ty: field_type, - .. - }) = first_field(fields)? - else { - return Ok(None); - }; + let field = fields + .into_iter() + .next() + .ok_or(darling::Error::custom("need at least one field"))?; + let field_type = field.ty; - let set_status = (status.0 != rocket_http::Status::Ok) - .then(|| quote! { ::rocket_okapi::util::set_status_code(&mut r, #status)?; }); + let status = response_attribute.status; + let set_status = + status.map(|status| quote! { ::rocket_okapi::util::set_status_code(&mut r, #status)?; }); + let content_type = response_attribute.content_type; let set_content_type = content_type.map(|ct| quote! { ::rocket_okapi::util::set_content_type(&mut r, #ct)?; }); - let set_description = parse_doc(&field_attrs) - .or_else(|| parse_doc(&attrs)) - .or_else(|| parse_doc(global_attrs)) - .map(|doc| quote! { ::rocket_okapi::util::set_description(&mut r, #doc)?; }); + let description = get_doc(&field.attrs) + .or_else(|| get_doc(&attrs)) + .or_else(|| get_doc(entity_attrs)); + let set_description = + description.map(|doc| quote! { ::rocket_okapi::util::set_description(&mut r, #doc)?; }); - let responses = quote! { + Ok(Some(quote! { let mut r = <#field_type as ::rocket_okapi::response::OpenApiResponderInner>::responses(gen)?; #set_status #set_content_type #set_description r - }; - - Ok(Some(responses)) + })) } -fn first_field(fields: Fields) -> Result> { - for field in fields { - let ResponseAttribute { ignore, .. } = parse_response_attribute(&field.attrs)?; - if !ignore { - return Ok(Some(field)); - } - } +fn get_response_attr_or_default(attrs: &[Attribute]) -> Result { + let mut attrs = attrs.iter().filter(|a| a.path.is_ident("response")); - Ok(None) -} + let Some(attr) = attrs.next() else { + return Ok(ResponseAttribute::default()); + }; + + if let Some(second_attr) = attrs.next() { + return Err( + darling::Error::custom("`response` attribute may be specified only once") + .with_span(second_attr), + ); + } -#[inline(always)] -fn parse_response_attribute(attrs: &[Attribute]) -> Result { - parse_attribute(&attrs, "response").map(Option::unwrap_or_default) + let meta = attr.parse_meta()?; + ResponseAttribute::from_meta(&meta) } diff --git a/rocket-okapi-codegen/src/responder_derive/response_attr.rs b/rocket-okapi-codegen/src/responder_derive/response_attr.rs new file mode 100644 index 00000000..bea7fbeb --- /dev/null +++ b/rocket-okapi-codegen/src/responder_derive/response_attr.rs @@ -0,0 +1,61 @@ +use darling::FromMeta; +use quote::ToTokens; + +/// This structure documents all the properties that can be used in +/// the `#[response]` attribute. For example: `#[response(status = 404)]` +#[derive(Default, FromMeta)] +#[darling(default)] +pub struct ResponseAttribute { + /// Status code of the response. By default, depends on the field type + pub status: Option, + + /// Content type of the response. By default, depends on the field type + pub content_type: Option, +} + +/// Derive macro wrapper for the Rocket's Status type. +/// Implements [`darling::FromMeta`] and [`quote::ToTokens`]. +pub struct Status(pub rocket_http::Status); + +impl Default for Status { + fn default() -> Self { + Self(rocket_http::Status::Ok) + } +} + +impl FromMeta for Status { + fn from_value(value: &syn::Lit) -> darling::Result { + if let syn::Lit::Int(int) = value { + let code = int.base10_parse::()?; + Ok(Self(rocket_http::Status::new(code))) + } else { + Err(darling::Error::unexpected_lit_type(value)) + } + } +} + +impl ToTokens for Status { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.code.to_tokens(tokens); + } +} + +/// Derive macro wrapper for the Rocket's ContentType type. +/// Implements [`darling::FromMeta`] and [`quote::ToTokens`]. +pub struct ContentType(pub rocket_http::ContentType); + +impl FromMeta for ContentType { + fn from_string(value: &str) -> darling::Result { + Ok(Self( + value + .parse() + .map_err(|_| darling::Error::unsupported_format(value))?, + )) + } +} + +impl ToTokens for ContentType { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_string().to_tokens(tokens); + } +} diff --git a/rocket-okapi-codegen/src/responder_derive/utils.rs b/rocket-okapi-codegen/src/responder_derive/utils.rs deleted file mode 100644 index 599e594e..00000000 --- a/rocket-okapi-codegen/src/responder_derive/utils.rs +++ /dev/null @@ -1,43 +0,0 @@ -use syn::{parse::Parse, spanned::Spanned, Attribute, Error, Expr, Ident, Lit, Meta, Result}; - -pub(crate) fn parse_doc(attrs: &[Attribute]) -> Option { - let doc = attrs - .iter() - .filter(|a| a.path.is_ident("doc")) - .filter_map(|a| { - if let Meta::NameValue(nv) = &a.parse_meta().ok()? { - if let Lit::Str(str) = &nv.lit { - return Some(str.value().trim().to_owned()); - } - } - None - }) - .collect::>() - .join(" "); - - (!doc.is_empty()).then_some(doc) -} - -pub(crate) fn parse_attribute(attrs: &[Attribute], ident: I) -> Result> -where - Ident: PartialEq, -{ - let attrs = attrs - .iter() - .filter(|a| a.path.is_ident(&ident)) - .collect::>(); - - if attrs.len() > 1 { - return Err(Error::new( - attrs[1].span(), - "this attribute may be specified only once", - )); - } else if attrs.is_empty() { - return Ok(None); - } - - let attr = attrs[0]; - let attr = attr.parse_args::()?; - - Ok(Some(attr)) -} From ee9efea0c11f568a0188b4722cceb893aed0b6fe Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Thu, 19 Dec 2024 16:59:18 +0100 Subject: [PATCH 4/7] Add OpenApiResponderInner trait bound for generics in OpenApiResponder --- .../src/responder_derive/mod.rs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/rocket-okapi-codegen/src/responder_derive/mod.rs b/rocket-okapi-codegen/src/responder_derive/mod.rs index be5447d9..74fb5588 100644 --- a/rocket-okapi-codegen/src/responder_derive/mod.rs +++ b/rocket-okapi-codegen/src/responder_derive/mod.rs @@ -5,7 +5,7 @@ use darling::{FromMeta, Result}; use proc_macro2::TokenStream; use quote::quote; use response_attr::ResponseAttribute; -use syn::{Attribute, DeriveInput, Fields}; +use syn::{parse_quote, Attribute, DeriveInput, Field, Fields, GenericParam, Generics, Type}; pub fn derive(input: DeriveInput) -> Result { let responses_variants: Vec<(Vec, Fields)> = match input.data { @@ -20,19 +20,20 @@ pub fn derive(input: DeriveInput) -> Result { } }; - let attrs = input.attrs; - let ident = input.ident; - let generics = input.generics; + let name = input.ident; + + let generics = add_trait_bound(input.generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let responses = responses_variants .into_iter() .filter_map(|(variant_attrs, variant_fields)| { - variant_to_responses(&attrs, variant_attrs, variant_fields).transpose() + variant_to_responses(&input.attrs, variant_attrs, variant_fields).transpose() }) .collect::>>()?; Ok(quote! { - impl #generics ::rocket_okapi::response::OpenApiResponderInner for #ident #generics { + impl #impl_generics ::rocket_okapi::response::OpenApiResponderInner for #name #ty_generics #where_clause { fn responses( gen: &mut ::rocket_okapi::gen::OpenApiGenerator, ) -> ::rocket_okapi::Result<::rocket_okapi::okapi::openapi3::Responses> { @@ -46,6 +47,17 @@ pub fn derive(input: DeriveInput) -> Result { }) } +fn add_trait_bound(mut generics: Generics) -> Generics { + for param in generics.params.iter_mut() { + if let GenericParam::Type(param) = param { + param.bounds.push(parse_quote!( + ::rocket_okapi::response::OpenApiResponderInner + )); + } + } + generics +} + fn variant_to_responses( entity_attrs: &[Attribute], attrs: Vec, @@ -57,7 +69,7 @@ fn variant_to_responses( .into_iter() .next() .ok_or(darling::Error::custom("need at least one field"))?; - let field_type = field.ty; + let response_type = &field.ty; let status = response_attribute.status; let set_status = @@ -74,7 +86,7 @@ fn variant_to_responses( description.map(|doc| quote! { ::rocket_okapi::util::set_description(&mut r, #doc)?; }); Ok(Some(quote! { - let mut r = <#field_type as ::rocket_okapi::response::OpenApiResponderInner>::responses(gen)?; + let mut r = <#response_type as ::rocket_okapi::response::OpenApiResponderInner>::responses(gen)?; #set_status #set_content_type #set_description From d45baf591396fb08a7290e8ec3891f9276677367 Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Thu, 19 Dec 2024 17:07:18 +0100 Subject: [PATCH 5/7] Add status code validation in OpenApiResponder For compatibility with Rocket's implementation --- .../src/responder_derive/response_attr.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rocket-okapi-codegen/src/responder_derive/response_attr.rs b/rocket-okapi-codegen/src/responder_derive/response_attr.rs index bea7fbeb..8a21a302 100644 --- a/rocket-okapi-codegen/src/responder_derive/response_attr.rs +++ b/rocket-okapi-codegen/src/responder_derive/response_attr.rs @@ -25,12 +25,16 @@ impl Default for Status { impl FromMeta for Status { fn from_value(value: &syn::Lit) -> darling::Result { - if let syn::Lit::Int(int) = value { - let code = int.base10_parse::()?; - Ok(Self(rocket_http::Status::new(code))) - } else { - Err(darling::Error::unexpected_lit_type(value)) + let syn::Lit::Int(int) = value else { + return Err(darling::Error::unexpected_lit_type(value)); + }; + + let code = int.base10_parse::()?; + if code < 100 || code >= 600 { + return Err(darling::Error::custom("status must be in range [100, 599]").with_span(int)); } + + Ok(Self(rocket_http::Status::new(code))) } } From 567eab9aa352ca5ce5b03fedc152883a7f3181ce Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Thu, 19 Dec 2024 17:07:59 +0100 Subject: [PATCH 6/7] Use flexible parse for ContentType in OpenApiResponder For compatibility with Rocket's implementation --- rocket-okapi-codegen/src/responder_derive/mod.rs | 2 +- .../src/responder_derive/response_attr.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/rocket-okapi-codegen/src/responder_derive/mod.rs b/rocket-okapi-codegen/src/responder_derive/mod.rs index 74fb5588..961f2ad0 100644 --- a/rocket-okapi-codegen/src/responder_derive/mod.rs +++ b/rocket-okapi-codegen/src/responder_derive/mod.rs @@ -5,7 +5,7 @@ use darling::{FromMeta, Result}; use proc_macro2::TokenStream; use quote::quote; use response_attr::ResponseAttribute; -use syn::{parse_quote, Attribute, DeriveInput, Field, Fields, GenericParam, Generics, Type}; +use syn::{parse_quote, Attribute, DeriveInput, Fields, GenericParam, Generics}; pub fn derive(input: DeriveInput) -> Result { let responses_variants: Vec<(Vec, Fields)> = match input.data { diff --git a/rocket-okapi-codegen/src/responder_derive/response_attr.rs b/rocket-okapi-codegen/src/responder_derive/response_attr.rs index 8a21a302..0174b98d 100644 --- a/rocket-okapi-codegen/src/responder_derive/response_attr.rs +++ b/rocket-okapi-codegen/src/responder_derive/response_attr.rs @@ -50,11 +50,9 @@ pub struct ContentType(pub rocket_http::ContentType); impl FromMeta for ContentType { fn from_string(value: &str) -> darling::Result { - Ok(Self( - value - .parse() - .map_err(|_| darling::Error::unsupported_format(value))?, - )) + rocket_http::ContentType::parse_flexible(value) + .map(Self) + .ok_or_else(|| darling::Error::unsupported_format(value)) } } From 9c60b8be1c973efe8f5c765a23c92e5044404b29 Mon Sep 17 00:00:00 2001 From: Valery Klachkov Date: Thu, 19 Dec 2024 17:41:00 +0100 Subject: [PATCH 7/7] Add doc comment for OpenApiResponder --- rocket-okapi-codegen/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rocket-okapi-codegen/src/lib.rs b/rocket-okapi-codegen/src/lib.rs index 0a823d38..cc49523c 100644 --- a/rocket-okapi-codegen/src/lib.rs +++ b/rocket-okapi-codegen/src/lib.rs @@ -87,7 +87,7 @@ pub fn openapi_spec(input: TokenStream) -> TokenStream { .into() } -/// Derive marco for the `OpenApiFromRequest` trait. +/// Derive macro for the `OpenApiFromRequest` trait. /// /// This derive trait is a very simple implementation for anything that does not /// require any other special headers or parameters to be validated. @@ -138,7 +138,11 @@ pub fn open_api_from_request_derive(input: TokenStream) -> TokenStream { gen.into() } -/// TODO +/// Derive for the [`OpenApiResponderInner`](rocket_okapi::response::OpenApiResponderInner) trait. +/// +/// Derive is fully compatible with the syntax of the +/// [`#[response]`](https://api.rocket.rs/v0.5/rocket/derive.Responder#field-attribute) attribute +/// from Rocket and does not require any code changes. #[proc_macro_derive(OpenApiResponder, attributes(responder))] pub fn open_api_responder_derive(input: TokenStream) -> TokenStream { let ast: syn::DeriveInput = syn::parse(input).unwrap();