Skip to content

Commit

Permalink
Implement include_str! for tags (#893)
Browse files Browse the repository at this point in the history
  • Loading branch information
junlarsen authored May 1, 2024
1 parent 4d798bc commit 365469f
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 40 deletions.
2 changes: 1 addition & 1 deletion utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
42 changes: 41 additions & 1 deletion utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -2844,6 +2844,10 @@ mod parse_utils {
Ok(parse_next(input, || input.parse::<LitStr>())?.value())
}

pub fn parse_next_literal_str_or_include_str(input: ParseStream) -> syn::Result<Str> {
Ok(parse_next(input, || input.parse::<Str>())?)
}

pub fn parse_next_literal_str_or_expr(input: ParseStream) -> syn::Result<Value> {
parse_next(input, || Value::parse(input)).map_err(|error| {
syn::Error::new(
Expand Down Expand Up @@ -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<Self> {
if input.peek(LitStr) {
Ok(Self::String(input.parse::<LitStr>()?.value()))
} else {
let include_str = input.parse::<Ident>()?;
let bang = input.parse::<Option<Token![!]>>()?;
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::<Group>()?.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) })
}
}
}
}
}
6 changes: 4 additions & 2 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -178,7 +179,7 @@ impl Parse for Modifier {
#[cfg_attr(feature = "debug", derive(Debug))]
struct Tag {
name: String,
description: Option<String>,
description: Option<Str>,
external_docs: Option<ExternalDocs>,
}

Expand All @@ -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;
Expand Down
39 changes: 3 additions & 36 deletions utoipa-gen/src/openapi/info.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
if input.peek(LitStr) {
Ok(Self::String(input.parse::<LitStr>()?.value()))
} else {
let include_str = input.parse::<Ident>()?;
let bang = input.parse::<Option<Token![!]>>()?;
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::<Group>()?.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))]
Expand Down
16 changes: 16 additions & 0 deletions utoipa-gen/tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down

0 comments on commit 365469f

Please sign in to comment.