Set schema title and description from #[doc]s

Work in progress
This commit is contained in:
Graham Esau 2019-12-07 22:14:23 +00:00
parent c630264ef9
commit feefd418d4
6 changed files with 262 additions and 47 deletions

View file

@ -121,6 +121,22 @@ impl SchemaGenerator {
&self.settings
}
// TODO document/rename
#[doc(hidden)]
pub fn objectify(&self, schema: SchemaObject) -> SchemaObject {
if schema.is_ref() {
SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
all_of: Some(vec![schema.into()]),
..Default::default()
})),
..Default::default()
}
} else {
schema
}
}
/// Returns a `Schema` that matches everything, such as the empty schema `{}`.
///
/// The exact value returned depends on this generator's [`BoolSchemas`](struct.SchemaSettings.html#structfield.bool_schemas) setting.
@ -196,7 +212,7 @@ impl SchemaGenerator {
/// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
let mut schema: SchemaObject = T::json_schema(self).into();
schema.metadata().title = Some(T::schema_name());
schema.metadata().title.get_or_insert_with(T::schema_name);
RootSchema {
meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(),
@ -210,7 +226,7 @@ impl SchemaGenerator {
/// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
let mut schema: SchemaObject = T::json_schema(&mut self).into();
schema.metadata().title = Some(T::schema_name());
schema.metadata().title.get_or_insert_with(T::schema_name);
RootSchema {
meta_schema: self.settings.meta_schema,
definitions: self.definitions,

53
schemars/tests/docs.rs Normal file
View file

@ -0,0 +1,53 @@
mod util;
use schemars::JsonSchema;
use util::*;
#[derive(Debug, JsonSchema)]
/**
*
* # This is the struct's title
*
* This is the struct's description.
*
*/
pub struct MyStruct {
/// # An integer
pub my_int: i32,
pub my_undocumented_bool: bool,
/// A unit struct instance
pub my_unit: MyUnitStruct,
}
/// # A Unit
#[derive(Debug, JsonSchema)]
pub struct MyUnitStruct;
#[doc = " # This is the enum's title "]
#[doc = " This is..."]
#[derive(Debug, JsonSchema)]
#[doc = "...the enum's description. "]
pub enum MyEnum {
UndocumentedUnit,
/// This comment is not included in the generated schema :(
DocumentedUnit,
/// ## Complex variant
/// This is a struct-like variant.
Complex {
/// ### A nullable string
///
/// This field is a nullable string.
///
/// This is another line!
my_nullable_string: Option<String>,
},
}
#[test]
fn doc_comments_struct() -> TestResult {
test_default_generated_schema::<MyStruct>("doc_comments_struct")
}
#[test]
fn doc_comments_enum() -> TestResult {
test_default_generated_schema::<MyEnum>("doc_comments_enum")
}

View 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)
}

View file

@ -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);
}
});

View 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)
}
}
}

View file

@ -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)]