From f7750fc809fc4bf0f3e5bbbf0e20b41819888044 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Fri, 24 May 2024 13:01:17 +0300 Subject: [PATCH] Add support for description and summary overriding (#948) Add `description = ...` and `summary = ...` attributes for `#[utoipa::path(...)]` attribute macro to allow description and summary overriding. When these attributes are defined the values are not resolved from the doc comment above the function. The value can be either literal string or expression e.g. reference to a `const` variable or `include_str!(...)` statement. Relates to #802 Resolves #439 Resolves #781 --- utoipa-gen/src/path.rs | 110 ++++++++++++++---- utoipa-gen/tests/path_derive.rs | 83 +++++++++++++ .../testdata/path_derive_description_override | 1 + 3 files changed, 169 insertions(+), 25 deletions(-) create mode 100644 utoipa-gen/tests/testdata/path_derive_description_override diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index e3ca62d1..3a3ba238 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -40,6 +40,8 @@ pub struct PathAttr<'p> { security: Option>, context_path: Option, impl_for: Option, + description: Option, + summary: Option, } impl<'p> PathAttr<'p> { @@ -95,7 +97,7 @@ impl<'p> PathAttr<'p> { impl Parse for PathAttr<'_> { fn parse(input: syn::parse::ParseStream) -> syn::Result { - const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected identifier, expected any of: operation_id, path, get, post, put, delete, options, head, patch, trace, connect, request_body, responses, params, tag, security, context_path"; + const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected identifier, expected any of: operation_id, path, get, post, put, delete, options, head, patch, trace, connect, request_body, responses, params, tag, security, context_path, description, summary"; let mut path_attr = PathAttr::default(); while !input.is_empty() { @@ -158,6 +160,13 @@ impl Parse for PathAttr<'_> { path_attr.impl_for = Some(parse_utils::parse_next(input, || input.parse::())?); } + "description" => { + path_attr.description = + Some(parse_utils::parse_next_literal_str_or_expr(input)?) + } + "summary" => { + path_attr.summary = Some(parse_utils::parse_next_literal_str_or_expr(input)?) + } _ => { // any other case it is expected to be path operation if let Some(path_operation) = @@ -401,19 +410,33 @@ impl<'p> ToTokensDiagnostics for Path<'p> { (summary, description) }); + let summary = self + .path_attr + .summary + .as_ref() + .map(Summary::Value) + .or_else(|| { + split_comment + .as_ref() + .map(|(summary, _)| Summary::Str(summary)) + }); + + let description = self + .path_attr + .description + .as_ref() + .map(Description::Value) + .or_else(|| { + split_comment + .as_ref() + .map(|(_, description)| Description::Vec(description)) + }); + let operation: Operation = Operation { deprecated: &self.deprecated, operation_id, - summary: split_comment.as_ref().and_then(|(summary, _)| { - if summary.is_empty() { - None - } else { - Some(summary) - } - }), - description: split_comment - .as_ref() - .map(|(_, description)| description.as_ref()), + summary, + description, parameters: self.path_attr.params.as_ref(), request_body: self.path_attr.request_body.as_ref(), responses: self.path_attr.responses.as_ref(), @@ -471,8 +494,8 @@ impl<'p> ToTokensDiagnostics for Path<'p> { #[cfg_attr(feature = "debug", derive(Debug))] struct Operation<'a> { operation_id: Expr, - summary: Option<&'a String>, - description: Option<&'a [String]>, + summary: Option>, + description: Option>, deprecated: &'a Option, parameters: &'a Vec>, request_body: Option<&'a RequestBody<'a>>, @@ -510,20 +533,12 @@ impl ToTokensDiagnostics for Operation<'_> { tokens.extend(quote!( .deprecated(Some(#deprecated)))) } - if let Some(summary) = self.summary { - tokens.extend(quote! { - .summary(Some(#summary)) - }) + if let Some(summary) = &self.summary { + summary.to_tokens(tokens); } - if let Some(description) = self.description { - let description = description.join("\n\n"); - - if !description.is_empty() { - tokens.extend(quote! { - .description(Some(#description)) - }) - } + if let Some(description) = &self.description { + description.to_tokens(tokens); } for parameter in self.parameters { @@ -534,6 +549,51 @@ impl ToTokensDiagnostics for Operation<'_> { } } +#[cfg_attr(feature = "debug", derive(Debug))] +enum Description<'a> { + Value(&'a parse_utils::Value), + Vec(&'a [String]), +} + +impl ToTokens for Description<'_> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + Self::Value(value) => tokens.extend(quote! { + .description(Some(#value)) + }), + Self::Vec(vec) => { + let description = vec.join("\n\n"); + + if !description.is_empty() { + tokens.extend(quote! { + .description(Some(#description)) + }) + } + } + } + } +} + +#[cfg_attr(feature = "debug", derive(Debug))] +enum Summary<'a> { + Value(&'a parse_utils::Value), + Str(&'a str), +} + +impl ToTokens for Summary<'_> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + Self::Value(value) => tokens.extend(quote! { + .summary(Some(#value)) + }), + Self::Str(str) if !str.is_empty() => tokens.extend(quote! { + .summary(Some(#str)) + }), + _ => (), + } + } +} + /// Represents either `ref("...")` or `Type` that can be optionally inlined with `inline(Type)`. #[cfg_attr(feature = "debug", derive(Debug))] enum PathType<'p> { diff --git a/utoipa-gen/tests/path_derive.rs b/utoipa-gen/tests/path_derive.rs index d9293d75..2415f601 100644 --- a/utoipa-gen/tests/path_derive.rs +++ b/utoipa-gen/tests/path_derive.rs @@ -2086,3 +2086,86 @@ fn derive_path_with_multiple_tags() { }) ); } + +#[test] +fn derive_path_with_description_and_summary_override() { + const SUMMARY: &str = "This is summary override that is +split to multiple lines"; + /// This is long summary + /// split to multiple lines + /// + /// This is description + /// split to multiple lines + #[allow(dead_code)] + #[utoipa::path( + get, + path = "/test-description", + summary = SUMMARY, + description = "This is description override", + responses( + (status = 200, description = "success response") + ), + )] + #[allow(unused)] + fn test_description_summary() -> &'static str { + "" + } + + let operation = test_api_fn_doc! { + test_description_summary, + operation: get, + path: "/test-description" + }; + + assert_json_eq!( + &operation, + json!({ + "description": "This is description override", + "operationId": "test_description_summary", + "responses": { + "200": { + "description": "success response", + }, + }, + "summary": "This is summary override that is\nsplit to multiple lines", + "tags": ["crate"] + }) + ); +} + +#[test] +fn derive_path_include_str_description() { + #[allow(dead_code)] + #[utoipa::path( + get, + path = "/test-description", + description = include_str!("./testdata/path_derive_description_override"), + responses( + (status = 200, description = "success response") + ), + )] + #[allow(unused)] + fn test_description_summary() -> &'static str { + "" + } + + let operation = test_api_fn_doc! { + test_description_summary, + operation: get, + path: "/test-description" + }; + + assert_json_eq!( + &operation, + json!({ + "description": "This is description from include_str!\n", + "operationId": "test_description_summary", + "responses": { + "200": { + "description": "success response", + }, + }, + "tags": ["crate"] + }) + ); +} diff --git a/utoipa-gen/tests/testdata/path_derive_description_override b/utoipa-gen/tests/testdata/path_derive_description_override new file mode 100644 index 00000000..2e263bf4 --- /dev/null +++ b/utoipa-gen/tests/testdata/path_derive_description_override @@ -0,0 +1 @@ +This is description from include_str!