diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index 7d5c48b2..db0deac4 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -27,7 +27,7 @@ utoipa = { path = "../utoipa", features = ["debug", "uuid"], default-features = serde_json = "1" serde = "1" actix-web = { version = "4", features = ["macros"], default-features = false } -axum = { version = "0.7", default-features = false } +axum = { version = "0.7", default-features = false, features = ["json", "query"] } paste = "1" rocket = { version = "0.5", features = ["json"] } smallvec = { version = "1.10", features = ["serde"] } diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 8cd578b4..23250391 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -2776,7 +2776,7 @@ mod parse_utils { use std::fmt::Display; use proc_macro2::{Group, Ident, TokenStream}; - use quote::ToTokens; + use quote::{quote, ToTokens}; use syn::{ parenthesized, parse::{Parse, ParseStream}, @@ -2844,6 +2844,10 @@ mod parse_utils { Ok(parse_next(input, || input.parse::())?.value()) } + pub fn parse_next_literal_str_or_include_str(input: ParseStream) -> syn::Result { + Ok(parse_next(input, || input.parse::())?) + } + pub fn parse_next_literal_str_or_expr(input: ParseStream) -> syn::Result { parse_next(input, || Value::parse(input)).map_err(|error| { syn::Error::new( @@ -2911,4 +2915,40 @@ mod parse_utils { )) } } + + #[derive(Clone)] + #[cfg_attr(feature = "debug", derive(Debug))] + pub(super) enum Str { + String(String), + IncludeStr(TokenStream), + } + + impl Parse for Str { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(LitStr) { + Ok(Self::String(input.parse::()?.value())) + } else { + let include_str = input.parse::()?; + let bang = input.parse::>()?; + if include_str != "include_str" || bang.is_none() { + return Err(Error::new( + include_str.span(), + "unexpected token, expected either literal string or include_str!(...)", + )); + } + Ok(Self::IncludeStr(input.parse::()?.stream())) + } + } + } + + impl ToTokens for Str { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::String(str) => str.to_tokens(tokens), + Self::IncludeStr(include_str) => { + tokens.extend(quote! { include_str!(#include_str) }) + } + } + } + } } diff --git a/utoipa-gen/src/openapi.rs b/utoipa-gen/src/openapi.rs index ceb5928d..958067f3 100644 --- a/utoipa-gen/src/openapi.rs +++ b/utoipa-gen/src/openapi.rs @@ -11,6 +11,7 @@ use syn::{ use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; +use crate::parse_utils::Str; use crate::{ parse_utils, path::PATH_STRUCT_PREFIX, security_requirement::SecurityRequirementsAttr, Array, ExternalDocs, ResultExt, @@ -178,7 +179,7 @@ impl Parse for Modifier { #[cfg_attr(feature = "debug", derive(Debug))] struct Tag { name: String, - description: Option, + description: Option, external_docs: Option, } @@ -198,7 +199,8 @@ impl Parse for Tag { match attribute_name { "name" => tag.name = parse_utils::parse_next_literal_str(input)?, "description" => { - tag.description = Some(parse_utils::parse_next_literal_str(input)?) + tag.description = + Some(parse_utils::parse_next_literal_str_or_include_str(input)?) } "external_docs" => { let content; diff --git a/utoipa-gen/src/openapi/info.rs b/utoipa-gen/src/openapi/info.rs index 01508998..c5c94aaf 100644 --- a/utoipa-gen/src/openapi/info.rs +++ b/utoipa-gen/src/openapi/info.rs @@ -1,47 +1,14 @@ use std::borrow::Cow; use std::io; -use proc_macro2::{Group, Ident, TokenStream as TokenStream2}; +use proc_macro2::{Ident, TokenStream as TokenStream2}; use quote::{quote, ToTokens}; use syn::parse::Parse; use syn::token::Comma; -use syn::{parenthesized, Error, LitStr, Token}; +use syn::{parenthesized, Error, LitStr}; use crate::parse_utils; - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub(super) enum Str { - String(String), - IncludeStr(TokenStream2), -} - -impl Parse for Str { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - if input.peek(LitStr) { - Ok(Self::String(input.parse::()?.value())) - } else { - let include_str = input.parse::()?; - let bang = input.parse::>()?; - if include_str != "include_str" || bang.is_none() { - return Err(Error::new( - include_str.span(), - "unexpected token, expected either literal string or include_str!(...)", - )); - } - Ok(Self::IncludeStr(input.parse::()?.stream())) - } - } -} - -impl ToTokens for Str { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self { - Self::String(str) => str.to_tokens(tokens), - Self::IncludeStr(include_str) => tokens.extend(quote! { include_str!(#include_str) }), - } - } -} +use crate::parse_utils::Str; #[derive(Default, Clone)] #[cfg_attr(feature = "debug", derive(Debug))] diff --git a/utoipa-gen/tests/openapi_derive.rs b/utoipa-gen/tests/openapi_derive.rs index 5c6b3004..25b4edac 100644 --- a/utoipa-gen/tests/openapi_derive.rs +++ b/utoipa-gen/tests/openapi_derive.rs @@ -58,6 +58,22 @@ fn derive_openapi_tags() { } } +#[test] +fn derive_openapi_tags_include_str() { + #[derive(OpenApi)] + #[openapi(tags( + (name = "random::api", description = include_str!("testdata/openapi-derive-info-description")), + ))] + struct ApiDoc; + + let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + + assert_value! {doc=> + "tags.[0].name" = r###""random::api""###, "Tags random_api name" + "tags.[0].description" = r###""this is include description\n""###, "Tags random_api description" + } +} + #[test] fn derive_openapi_with_external_docs() { #[derive(OpenApi)]