diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 967b8dec..554b0aff 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -502,7 +502,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { /// /// # Responses Attributes /// -/// * `status = ...` Is valid http status code. E.g. _`200`_ +/// * `status = ...` Is either a valid http status code integer. E.g. _`200`_ or a string value representing a range such as `"4XX"` or `"default"`. /// * `description = "..."` Define description for the response as str. /// * `body = ...` Optional response body object type. When left empty response does not expect to send any /// response body. Should be an identifier or slice. E.g _`Pet`_ or _`[Pet]`_. Where the type implments [`ToSchema`][to_schema], @@ -524,6 +524,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { /// responses( /// (status = 200, description = "success response"), /// (status = 404, description = "resource missing"), +/// (status = "5XX", description = "server error"), /// ) /// ``` /// diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs index ff6695e7..49a35142 100644 --- a/utoipa-gen/src/path/response.rs +++ b/utoipa-gen/src/path/response.rs @@ -37,7 +37,7 @@ impl Parse for Response<'_> { #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct ResponseValue<'r> { - status_code: i32, + status_code: String, description: String, response_type: Option>, content_type: Option>, @@ -48,6 +48,9 @@ pub struct ResponseValue<'r> { impl Parse for ResponseValue<'_> { fn parse(input: syn::parse::ParseStream) -> syn::Result { const EXPECTED_ATTRIBUTE_MESSAGE: &str = "unexpected attribute, expected any of: status, description, body, content_type, headers"; + const VALID_STATUS_RANGES: &[&str] = &["default", "1XX", "2XX", "3XX", "4XX", "5XX"]; + const INVALID_STATUS_RANGE_MESSAGE: &str = "Invalid status range, expected one of:"; + let mut response = ResponseValue::default(); while !input.is_empty() { @@ -62,8 +65,23 @@ impl Parse for ResponseValue<'_> { match attribute_name { "status" => { response.status_code = - parse_utils::parse_next(input, || input.parse::())? - .base10_parse()?; + parse_utils::parse_next(input, || { + let lookahead = input.lookahead1(); + if lookahead.peek(LitInt) { + input.parse::()?.base10_parse() + } else if lookahead.peek(LitStr) { + let value = input.parse::()?.value(); + if !VALID_STATUS_RANGES.contains(&value.as_str()) { + return Err(Error::new( + input.span(), + format!("{} {}", INVALID_STATUS_RANGE_MESSAGE, VALID_STATUS_RANGES.join(", ")), + )) + } + Ok(value) + } else { + Err(lookahead.error()) + } + })? } "description" => { response.description = parse_utils::parse_next_literal_str(input)?; @@ -176,7 +194,7 @@ impl ToTokens for Responses<'_> { }) } Response::Value(response) => { - let code = &response.status_code.to_string(); + let code = &response.status_code; acc.extend(quote! { .response(#code, #response) }); } } diff --git a/utoipa-gen/tests/path_response_derive_test.rs b/utoipa-gen/tests/path_response_derive_test.rs index 216f4703..fa000e4c 100644 --- a/utoipa-gen/tests/path_response_derive_test.rs +++ b/utoipa-gen/tests/path_response_derive_test.rs @@ -58,7 +58,9 @@ test_fn! { (status = 200, description = "success"), (status = 401, description = "unauthorized"), (status = 404, description = "not found"), - (status = 500, description = "server error") + (status = 500, description = "server error"), + (status = "5XX", description = "all other server errors"), + (status = "default", description = "default") ) } @@ -79,6 +81,12 @@ fn derive_path_with_multiple_simple_responses() { "responses.500.description" = r#""server error""#, "Response description" "responses.500.content" = r#"null"#, "Response content" "responses.500.headers" = r#"null"#, "Response headers" + "responses.5XX.description" = r#""all other server errors""#, "Response description" + "responses.5XX.content" = r#"null"#, "Response content" + "responses.5XX.headers" = r#"null"#, "Response headers" + "responses.default.description" = r#""default""#, "Response description" + "responses.default.content" = r#"null"#, "Response content" + "responses.default.headers" = r#"null"#, "Response headers" } }