Allow arbitrary expressions in doc/title/description attributes (#327)

This commit is contained in:
Graham Esau 2024-08-24 14:35:30 +01:00 committed by GitHub
parent 5547e77bcd
commit df06fc5f66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 206 additions and 159 deletions

View file

@ -10,7 +10,7 @@ license = "MIT"
readme = "README.md"
keywords = ["rust", "json-schema", "serde"]
categories = ["encoding", "no-std"]
rust-version = "1.60"
rust-version = "1.65"
[lib]
proc-macro = true

View file

@ -26,7 +26,6 @@ impl<'a> FromSerde for Container<'a> {
serde_attrs: serde.attrs,
data: Data::from_serde(errors, serde.data)?,
generics: serde.generics.clone(),
original: serde.original,
// FIXME this allows with/schema_with attribute on containers
attrs: Attrs::new(&serde.original.attrs, errors),
})

View file

@ -10,7 +10,6 @@ pub struct Container<'a> {
pub serde_attrs: serde_derive_internals::attr::Container,
pub data: Data<'a>,
pub generics: syn::Generics,
pub original: &'a syn::DeriveInput,
pub attrs: Attrs,
}

View file

@ -1,54 +1,25 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::Attribute;
pub fn get_title_and_desc_from_doc(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
let doc = match get_doc(attrs) {
None => return (None, None),
Some(doc) => doc,
};
pub fn get_doc(attrs: &[Attribute]) -> Option<syn::Expr> {
let joiner = quote! {, "\n",};
let mut macro_args: TokenStream = TokenStream::new();
if doc.starts_with('#') {
let mut split = doc.splitn(2, '\n');
let title = split
.next()
.unwrap()
.trim_start_matches('#')
.trim()
.to_owned();
let maybe_desc = split.next().map(|s| s.trim().to_owned());
(none_if_empty(title), maybe_desc)
} else {
(None, Some(doc))
}
}
fn get_doc(attrs: &[Attribute]) -> Option<String> {
let lines = attrs
for nv in attrs
.iter()
.filter_map(|attr| {
if !attr.path().is_ident("doc") {
return None;
}
.filter(|a| a.path().is_ident("doc"))
.filter_map(|a| a.meta.require_name_value().ok())
{
if !macro_args.is_empty() {
macro_args.extend(joiner.clone());
}
macro_args.extend(nv.value.to_token_stream());
}
let meta = attr.meta.require_name_value().ok()?;
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = &meta.value
{
return Some(lit_str.value());
}
None
})
.collect::<Vec<_>>();
none_if_empty(lines.join("\n").trim().to_owned())
}
fn none_if_empty(s: String) -> Option<String> {
if s.is_empty() {
if macro_args.is_empty() {
None
} else {
Some(s)
Some(parse_quote!(::core::concat!(#macro_args)))
}
}

View file

@ -10,7 +10,7 @@ use proc_macro2::{Group, Span, TokenStream, TokenTree};
use quote::ToTokens;
use serde_derive_internals::Ctxt;
use syn::parse::{self, Parse};
use syn::{LitStr, Meta, MetaNameValue};
use syn::{Expr, LitStr, Meta, MetaNameValue};
// FIXME using the same struct for containers+variants+fields means that
// with/schema_with are accepted (but ignored) on containers, and
@ -19,15 +19,16 @@ use syn::{LitStr, Meta, MetaNameValue};
#[derive(Debug, Default)]
pub struct Attrs {
pub with: Option<WithAttr>,
pub title: Option<String>,
pub description: Option<String>,
pub title: Option<Expr>,
pub description: Option<Expr>,
pub doc: Option<Expr>,
pub deprecated: bool,
pub examples: Vec<syn::Path>,
pub repr: Option<syn::Type>,
pub crate_name: Option<syn::Path>,
pub is_renamed: bool,
pub extensions: Vec<(String, TokenStream)>,
pub transforms: Vec<syn::Expr>,
pub transforms: Vec<Expr>,
}
#[derive(Debug)]
@ -48,26 +49,15 @@ impl Attrs {
.find(|a| a.path().is_ident("repr"))
.and_then(|a| a.parse_args().ok());
let (doc_title, doc_description) = doc::get_title_and_desc_from_doc(attrs);
result.title = result.title.or(doc_title);
result.description = result.description.or(doc_description);
result.doc = doc::get_doc(attrs);
result
}
pub fn as_metadata(&self) -> SchemaMetadata<'_> {
#[allow(clippy::ptr_arg)]
fn none_if_empty(s: &String) -> Option<&str> {
if s.is_empty() {
None
} else {
Some(s)
}
}
SchemaMetadata {
title: self.title.as_ref().and_then(none_if_empty),
description: self.description.as_ref().and_then(none_if_empty),
doc: self.doc.as_ref(),
title: self.title.as_ref(),
description: self.description.as_ref(),
deprecated: self.deprecated,
examples: &self.examples,
extensions: &self.extensions,
@ -128,25 +118,15 @@ impl Attrs {
}
}
Meta::NameValue(m) if m.path.is_ident("title") => {
if let Ok(title) = expr_as_lit_str(errors, attr_type, "title", &m.value) {
match self.title {
Some(_) => duplicate_error(m),
None => self.title = Some(title.value()),
}
}
}
Meta::NameValue(m) if m.path.is_ident("title") => match self.title {
Some(_) => duplicate_error(m),
None => self.title = Some(m.value.clone()),
},
Meta::NameValue(m) if m.path.is_ident("description") => {
if let Ok(description) =
expr_as_lit_str(errors, attr_type, "description", &m.value)
{
match self.description {
Some(_) => duplicate_error(m),
None => self.description = Some(description.value()),
}
}
}
Meta::NameValue(m) if m.path.is_ident("description") => match self.description {
Some(_) => duplicate_error(m),
None => self.description = Some(m.value.clone()),
},
Meta::NameValue(m) if m.path.is_ident("example") => {
if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.value) {
@ -239,6 +219,7 @@ impl Attrs {
with: None,
title: None,
description: None,
doc: None,
deprecated: false,
examples,
repr: None,

View file

@ -1,17 +1,18 @@
use proc_macro2::TokenStream;
use syn::spanned::Spanned;
use syn::{spanned::Spanned, Expr};
#[derive(Debug, Clone)]
pub struct SchemaMetadata<'a> {
pub title: Option<&'a str>,
pub description: Option<&'a str>,
pub title: Option<&'a Expr>,
pub description: Option<&'a Expr>,
pub doc: Option<&'a Expr>,
pub deprecated: bool,
pub read_only: bool,
pub write_only: bool,
pub examples: &'a [syn::Path],
pub default: Option<TokenStream>,
pub extensions: &'a [(String, TokenStream)],
pub transforms: &'a [syn::Expr],
pub transforms: &'a [Expr],
}
impl<'a> SchemaMetadata<'a> {
@ -35,14 +36,29 @@ impl<'a> SchemaMetadata<'a> {
fn make_setters(&self) -> Vec<TokenStream> {
let mut setters = Vec::<TokenStream>::new();
if let Some(doc) = &self.doc {
if self.title.is_none() || self.description.is_none() {
setters.push(quote!{
const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc);
});
}
}
if let Some(title) = &self.title {
setters.push(quote! {
schemars::_private::insert_metadata_property(&mut schema, "title", #title);
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", #title);
});
} else if self.doc.is_some() {
setters.push(quote! {
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", title_and_description.0);
});
}
if let Some(description) = &self.description {
setters.push(quote! {
schemars::_private::insert_metadata_property(&mut schema, "description", #description);
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", #description);
});
} else if self.doc.is_some() {
setters.push(quote! {
schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", title_and_description.1);
});
}