Skip to content

Commit

Permalink
Add support for description and summary overriding (#948)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
juhaku authored May 24, 2024
1 parent 403d716 commit f7750fc
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 25 deletions.
110 changes: 85 additions & 25 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub struct PathAttr<'p> {
security: Option<Array<'p, SecurityRequirementsAttr>>,
context_path: Option<parse_utils::Value>,
impl_for: Option<Ident>,
description: Option<parse_utils::Value>,
summary: Option<parse_utils::Value>,
}

impl<'p> PathAttr<'p> {
Expand Down Expand Up @@ -95,7 +97,7 @@ impl<'p> PathAttr<'p> {

impl Parse for PathAttr<'_> {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
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() {
Expand Down Expand Up @@ -158,6 +160,13 @@ impl Parse for PathAttr<'_> {
path_attr.impl_for =
Some(parse_utils::parse_next(input, || input.parse::<Ident>())?);
}
"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) =
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<Summary<'a>>,
description: Option<Description<'a>>,
deprecated: &'a Option<bool>,
parameters: &'a Vec<Parameter<'a>>,
request_body: Option<&'a RequestBody<'a>>,
Expand Down Expand Up @@ -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 {
Expand All @@ -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> {
Expand Down
83 changes: 83 additions & 0 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
})
);
}
1 change: 1 addition & 0 deletions utoipa-gen/tests/testdata/path_derive_description_override
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is description from include_str!

0 comments on commit f7750fc

Please sign in to comment.