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!