Set schema title and description from #[doc]s
Work in progress
This commit is contained in:
parent
c630264ef9
commit
feefd418d4
6 changed files with 262 additions and 47 deletions
63
schemars_derive/src/doc_attrs.rs
Normal file
63
schemars_derive/src/doc_attrs.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use syn::{Attribute, Lit::Str, Meta::NameValue, MetaNameValue};
|
||||
|
||||
pub fn get_title_and_desc_from_docs(attrs: &[Attribute]) -> (Option<String>, Option<String>) {
|
||||
let docs = match get_docs(attrs) {
|
||||
None => return (None, None),
|
||||
Some(docs) => docs,
|
||||
};
|
||||
|
||||
if docs.starts_with('#') {
|
||||
let mut split = docs.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, none_if_empty(docs))
|
||||
}
|
||||
}
|
||||
|
||||
fn none_if_empty(s: String) -> Option<String> {
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_docs(attrs: &[Attribute]) -> Option<String> {
|
||||
let doc_attrs = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
if !attr.path.is_ident("doc") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let meta = attr.parse_meta().ok()?;
|
||||
if let NameValue(MetaNameValue { lit: Str(s), .. }) = meta {
|
||||
return Some(s.value());
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if doc_attrs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut docs = doc_attrs
|
||||
.iter()
|
||||
.flat_map(|a| a.split('\n'))
|
||||
.map(str::trim)
|
||||
.skip_while(|s| *s == "")
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
docs.truncate(docs.trim_end().len());
|
||||
Some(docs)
|
||||
}
|
|
@ -4,6 +4,8 @@ extern crate quote;
|
|||
extern crate syn;
|
||||
extern crate proc_macro;
|
||||
|
||||
mod doc_attrs;
|
||||
mod metadata;
|
||||
mod preprocess;
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
|
@ -13,7 +15,7 @@ use serde_derive_internals::attr::{self, Default as SerdeDefault, TagType};
|
|||
use serde_derive_internals::{Ctxt, Derive};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
|
||||
#[proc_macro_derive(JsonSchema, attributes(schemars, serde, doc))]
|
||||
pub fn derive_json_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let mut input = parse_macro_input!(input as syn::DeriveInput);
|
||||
|
||||
|
@ -29,13 +31,14 @@ pub fn derive_json_schema(input: proc_macro::TokenStream) -> proc_macro::TokenSt
|
|||
}
|
||||
let cont = cont.expect("from_ast set no errors on Ctxt, so should have returned Some");
|
||||
|
||||
let schema = match cont.data {
|
||||
let schema_expr = match cont.data {
|
||||
Data::Struct(Style::Unit, _) => schema_for_unit_struct(),
|
||||
Data::Struct(Style::Newtype, ref fields) => schema_for_newtype_struct(&fields[0]),
|
||||
Data::Struct(Style::Tuple, ref fields) => schema_for_tuple_struct(fields),
|
||||
Data::Struct(Style::Struct, ref fields) => schema_for_struct(fields, &cont.attrs),
|
||||
Data::Enum(ref variants) => schema_for_enum(variants, &cont.attrs),
|
||||
};
|
||||
let schema_expr = metadata::set_metadata_on_schema_from_docs(schema_expr, &cont.original.attrs);
|
||||
|
||||
let type_name = cont.ident;
|
||||
let type_params: Vec<_> = cont.generics.type_params().map(|ty| &ty.ident).collect();
|
||||
|
@ -72,7 +75,7 @@ pub fn derive_json_schema(input: proc_macro::TokenStream) -> proc_macro::TokenSt
|
|||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
#schema
|
||||
#schema_expr
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -277,24 +280,22 @@ fn schema_for_struct(fields: &[Field], cattrs: &attr::Container) -> TokenStream
|
|||
if !container_has_default && !has_default(field.attrs.default()) {
|
||||
required.push(name.clone());
|
||||
}
|
||||
let ty = get_json_schema_type(field);
|
||||
|
||||
if field.attrs.skip_deserializing() {
|
||||
quote_spanned! {field.original.span()=>
|
||||
let mut schema: schemars::schema::SchemaObject = gen.subschema_for::<#ty>().into();
|
||||
schema.metadata().read_only = true;
|
||||
props.insert(#name.to_owned(), schema.into());
|
||||
}
|
||||
} else if field.attrs.skip_serializing() {
|
||||
quote_spanned! {field.original.span()=>
|
||||
let mut schema: schemars::schema::SchemaObject = gen.subschema_for::<#ty>().into();
|
||||
schema.metadata().write_only = true;
|
||||
props.insert(#name.to_owned(), schema.into());
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {field.original.span()=>
|
||||
props.insert(#name.to_owned(), gen.subschema_for::<#ty>());
|
||||
}
|
||||
let ty = get_json_schema_type(field);
|
||||
let span = field.original.span();
|
||||
let schema_expr = quote_spanned! {span=>
|
||||
gen.subschema_for::<#ty>()
|
||||
};
|
||||
|
||||
let metadata = metadata::SchemaMetadata {
|
||||
read_only: field.attrs.skip_deserializing(),
|
||||
write_only: field.attrs.skip_serializing(),
|
||||
..metadata::get_metadata_from_docs(&field.original.attrs)
|
||||
};
|
||||
let schema_expr = metadata::set_metadata_on_schema(schema_expr, &metadata);
|
||||
|
||||
quote_spanned! {span=>
|
||||
props.insert(#name.to_owned(), #schema_expr);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
67
schemars_derive/src/metadata.rs
Normal file
67
schemars_derive/src/metadata.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use crate::doc_attrs;
|
||||
use proc_macro2::TokenStream;
|
||||
use syn::Attribute;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct SchemaMetadata {
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub read_only: bool,
|
||||
pub write_only: bool,
|
||||
}
|
||||
|
||||
pub fn set_metadata_on_schema_from_docs(
|
||||
schema_expr: TokenStream,
|
||||
attrs: &[Attribute],
|
||||
) -> TokenStream {
|
||||
let metadata = get_metadata_from_docs(attrs);
|
||||
set_metadata_on_schema(schema_expr, &metadata)
|
||||
}
|
||||
|
||||
pub fn get_metadata_from_docs(attrs: &[Attribute]) -> SchemaMetadata {
|
||||
let (title, description) = doc_attrs::get_title_and_desc_from_docs(attrs);
|
||||
SchemaMetadata {
|
||||
title,
|
||||
description,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_metadata_on_schema(schema_expr: TokenStream, metadata: &SchemaMetadata) -> TokenStream {
|
||||
let mut setters = Vec::<TokenStream>::new();
|
||||
|
||||
if let Some(title) = &metadata.title {
|
||||
setters.push(quote! {
|
||||
metadata.title = Some(#title.to_owned());
|
||||
})
|
||||
}
|
||||
if let Some(description) = &metadata.description {
|
||||
setters.push(quote! {
|
||||
metadata.description = Some(#description.to_owned());
|
||||
})
|
||||
}
|
||||
if metadata.read_only {
|
||||
setters.push(quote! {
|
||||
metadata.read_only = true;
|
||||
})
|
||||
}
|
||||
if metadata.write_only {
|
||||
setters.push(quote! {
|
||||
metadata.write_only = true;
|
||||
})
|
||||
}
|
||||
|
||||
if setters.is_empty() {
|
||||
return schema_expr;
|
||||
}
|
||||
|
||||
quote! {
|
||||
{
|
||||
let schema = #schema_expr.into();
|
||||
let mut schema_obj = gen.objectify(schema);
|
||||
let mut metadata = schema_obj.metadata();
|
||||
#(#setters)*
|
||||
schemars::schema::Schema::Object(schema_obj)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,23 +42,28 @@ fn process_serde_field_attrs<'a>(ctxt: &Ctxt, fields: impl Iterator<Item = &'a m
|
|||
}
|
||||
|
||||
fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
||||
let mut serde_meta: Vec<NestedMeta> = attrs
|
||||
.iter()
|
||||
.filter(|a| a.path.is_ident("serde"))
|
||||
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut schemars_attrs = Vec::<Attribute>::new();
|
||||
let mut serde_attrs = Vec::<Attribute>::new();
|
||||
let mut misc_attrs = Vec::<Attribute>::new();
|
||||
|
||||
attrs.retain(|a| a.path.is_ident("schemars"));
|
||||
for attr in attrs.drain(..) {
|
||||
if attr.path.is_ident("schemars") {
|
||||
schemars_attrs.push(attr)
|
||||
} else if attr.path.is_ident("serde") {
|
||||
serde_attrs.push(attr)
|
||||
} else {
|
||||
misc_attrs.push(attr)
|
||||
}
|
||||
}
|
||||
|
||||
for attr in attrs.iter_mut() {
|
||||
for attr in schemars_attrs.iter_mut() {
|
||||
let schemars_ident = attr.path.segments.pop().unwrap().into_value().ident;
|
||||
attr.path
|
||||
.segments
|
||||
.push(Ident::new("serde", schemars_ident.span()).into());
|
||||
}
|
||||
|
||||
let mut schemars_meta_names: BTreeSet<String> = attrs
|
||||
let mut schemars_meta_names: BTreeSet<String> = schemars_attrs
|
||||
.iter()
|
||||
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
||||
.flatten()
|
||||
|
@ -69,25 +74,32 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
|||
schemars_meta_names.insert("deserialize_with".to_string());
|
||||
}
|
||||
|
||||
serde_meta.retain(|m| {
|
||||
get_meta_ident(&ctxt, m)
|
||||
.map(|i| !schemars_meta_names.contains(&i.to_string()))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let mut serde_meta = serde_attrs
|
||||
.iter()
|
||||
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
||||
.flatten()
|
||||
.filter(|m| {
|
||||
get_meta_ident(&ctxt, m)
|
||||
.map(|i| !schemars_meta_names.contains(&i))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if serde_meta.is_empty() {
|
||||
return;
|
||||
*attrs = schemars_attrs;
|
||||
|
||||
if serde_meta.peek().is_some() {
|
||||
let new_serde_attr = quote! {
|
||||
#[serde(#(#serde_meta),*)]
|
||||
};
|
||||
|
||||
let parser = Attribute::parse_outer;
|
||||
match parser.parse2(new_serde_attr) {
|
||||
Ok(ref mut parsed) => attrs.append(parsed),
|
||||
Err(e) => ctxt.error_spanned_by(to_tokens(attrs), e),
|
||||
}
|
||||
}
|
||||
|
||||
let new_serde_attr = quote! {
|
||||
#[serde(#(#serde_meta),*)]
|
||||
};
|
||||
|
||||
let parser = Attribute::parse_outer;
|
||||
match parser.parse2(new_serde_attr) {
|
||||
Ok(ref mut parsed) => attrs.append(parsed),
|
||||
Err(e) => ctxt.error_spanned_by(to_tokens(attrs), e),
|
||||
}
|
||||
attrs.extend(misc_attrs)
|
||||
}
|
||||
|
||||
fn to_tokens(attrs: &[Attribute]) -> impl ToTokens {
|
||||
|
@ -142,6 +154,7 @@ mod tests {
|
|||
#[schemars(container2 = "overridden", container4)]
|
||||
#[misc]
|
||||
struct MyStruct {
|
||||
/// blah blah blah
|
||||
#[serde(field, field2)]
|
||||
field1: i32,
|
||||
#[serde(field, field2, serialize_with = "se", deserialize_with = "de")]
|
||||
|
@ -154,8 +167,10 @@ mod tests {
|
|||
let expected: DeriveInput = parse_quote! {
|
||||
#[serde(container2 = "overridden", container4)]
|
||||
#[serde(container, container3(foo, bar))]
|
||||
#[misc]
|
||||
struct MyStruct {
|
||||
#[serde(field, field2)]
|
||||
#[doc = r" blah blah blah"]
|
||||
field1: i32,
|
||||
#[serde(field = "overridden", with = "with")]
|
||||
#[serde(field2)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue