Allow arbitrary expressions in doc/title/description attributes (#327)
This commit is contained in:
parent
5547e77bcd
commit
df06fc5f66
17 changed files with 206 additions and 159 deletions
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue