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
|
@ -121,6 +121,22 @@ impl SchemaGenerator {
|
||||||
&self.settings
|
&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 `{}`.
|
/// 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.
|
/// 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)
|
/// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
|
||||||
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
|
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
|
||||||
let mut schema: SchemaObject = T::json_schema(self).into();
|
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 {
|
RootSchema {
|
||||||
meta_schema: self.settings.meta_schema.clone(),
|
meta_schema: self.settings.meta_schema.clone(),
|
||||||
definitions: self.definitions.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)
|
/// 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 {
|
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
|
||||||
let mut schema: SchemaObject = T::json_schema(&mut self).into();
|
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 {
|
RootSchema {
|
||||||
meta_schema: self.settings.meta_schema,
|
meta_schema: self.settings.meta_schema,
|
||||||
definitions: self.definitions,
|
definitions: self.definitions,
|
||||||
|
|
53
schemars/tests/docs.rs
Normal file
53
schemars/tests/docs.rs
Normal 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")
|
||||||
|
}
|
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 syn;
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
mod doc_attrs;
|
||||||
|
mod metadata;
|
||||||
mod preprocess;
|
mod preprocess;
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
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 serde_derive_internals::{Ctxt, Derive};
|
||||||
use syn::spanned::Spanned;
|
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 {
|
pub fn derive_json_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let mut input = parse_macro_input!(input as syn::DeriveInput);
|
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 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::Unit, _) => schema_for_unit_struct(),
|
||||||
Data::Struct(Style::Newtype, ref fields) => schema_for_newtype_struct(&fields[0]),
|
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::Tuple, ref fields) => schema_for_tuple_struct(fields),
|
||||||
Data::Struct(Style::Struct, ref fields) => schema_for_struct(fields, &cont.attrs),
|
Data::Struct(Style::Struct, ref fields) => schema_for_struct(fields, &cont.attrs),
|
||||||
Data::Enum(ref variants) => schema_for_enum(variants, &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_name = cont.ident;
|
||||||
let type_params: Vec<_> = cont.generics.type_params().map(|ty| &ty.ident).collect();
|
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 {
|
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()) {
|
if !container_has_default && !has_default(field.attrs.default()) {
|
||||||
required.push(name.clone());
|
required.push(name.clone());
|
||||||
}
|
}
|
||||||
let ty = get_json_schema_type(field);
|
|
||||||
|
|
||||||
if field.attrs.skip_deserializing() {
|
let ty = get_json_schema_type(field);
|
||||||
quote_spanned! {field.original.span()=>
|
let span = field.original.span();
|
||||||
let mut schema: schemars::schema::SchemaObject = gen.subschema_for::<#ty>().into();
|
let schema_expr = quote_spanned! {span=>
|
||||||
schema.metadata().read_only = true;
|
gen.subschema_for::<#ty>()
|
||||||
props.insert(#name.to_owned(), schema.into());
|
};
|
||||||
}
|
|
||||||
} else if field.attrs.skip_serializing() {
|
let metadata = metadata::SchemaMetadata {
|
||||||
quote_spanned! {field.original.span()=>
|
read_only: field.attrs.skip_deserializing(),
|
||||||
let mut schema: schemars::schema::SchemaObject = gen.subschema_for::<#ty>().into();
|
write_only: field.attrs.skip_serializing(),
|
||||||
schema.metadata().write_only = true;
|
..metadata::get_metadata_from_docs(&field.original.attrs)
|
||||||
props.insert(#name.to_owned(), schema.into());
|
};
|
||||||
}
|
let schema_expr = metadata::set_metadata_on_schema(schema_expr, &metadata);
|
||||||
} else {
|
|
||||||
quote_spanned! {field.original.span()=>
|
quote_spanned! {span=>
|
||||||
props.insert(#name.to_owned(), gen.subschema_for::<#ty>());
|
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>) {
|
fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
||||||
let mut serde_meta: Vec<NestedMeta> = attrs
|
let mut schemars_attrs = Vec::<Attribute>::new();
|
||||||
.iter()
|
let mut serde_attrs = Vec::<Attribute>::new();
|
||||||
.filter(|a| a.path.is_ident("serde"))
|
let mut misc_attrs = Vec::<Attribute>::new();
|
||||||
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
|
||||||
.flatten()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
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;
|
let schemars_ident = attr.path.segments.pop().unwrap().into_value().ident;
|
||||||
attr.path
|
attr.path
|
||||||
.segments
|
.segments
|
||||||
.push(Ident::new("serde", schemars_ident.span()).into());
|
.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()
|
.iter()
|
||||||
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
||||||
.flatten()
|
.flatten()
|
||||||
|
@ -69,16 +74,20 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
||||||
schemars_meta_names.insert("deserialize_with".to_string());
|
schemars_meta_names.insert("deserialize_with".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_meta.retain(|m| {
|
let mut serde_meta = serde_attrs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|attr| get_meta_items(&ctxt, attr))
|
||||||
|
.flatten()
|
||||||
|
.filter(|m| {
|
||||||
get_meta_ident(&ctxt, m)
|
get_meta_ident(&ctxt, m)
|
||||||
.map(|i| !schemars_meta_names.contains(&i.to_string()))
|
.map(|i| !schemars_meta_names.contains(&i))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
});
|
})
|
||||||
|
.peekable();
|
||||||
|
|
||||||
if serde_meta.is_empty() {
|
*attrs = schemars_attrs;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if serde_meta.peek().is_some() {
|
||||||
let new_serde_attr = quote! {
|
let new_serde_attr = quote! {
|
||||||
#[serde(#(#serde_meta),*)]
|
#[serde(#(#serde_meta),*)]
|
||||||
};
|
};
|
||||||
|
@ -90,6 +99,9 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attrs.extend(misc_attrs)
|
||||||
|
}
|
||||||
|
|
||||||
fn to_tokens(attrs: &[Attribute]) -> impl ToTokens {
|
fn to_tokens(attrs: &[Attribute]) -> impl ToTokens {
|
||||||
let mut tokens = proc_macro2::TokenStream::new();
|
let mut tokens = proc_macro2::TokenStream::new();
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
|
@ -142,6 +154,7 @@ mod tests {
|
||||||
#[schemars(container2 = "overridden", container4)]
|
#[schemars(container2 = "overridden", container4)]
|
||||||
#[misc]
|
#[misc]
|
||||||
struct MyStruct {
|
struct MyStruct {
|
||||||
|
/// blah blah blah
|
||||||
#[serde(field, field2)]
|
#[serde(field, field2)]
|
||||||
field1: i32,
|
field1: i32,
|
||||||
#[serde(field, field2, serialize_with = "se", deserialize_with = "de")]
|
#[serde(field, field2, serialize_with = "se", deserialize_with = "de")]
|
||||||
|
@ -154,8 +167,10 @@ mod tests {
|
||||||
let expected: DeriveInput = parse_quote! {
|
let expected: DeriveInput = parse_quote! {
|
||||||
#[serde(container2 = "overridden", container4)]
|
#[serde(container2 = "overridden", container4)]
|
||||||
#[serde(container, container3(foo, bar))]
|
#[serde(container, container3(foo, bar))]
|
||||||
|
#[misc]
|
||||||
struct MyStruct {
|
struct MyStruct {
|
||||||
#[serde(field, field2)]
|
#[serde(field, field2)]
|
||||||
|
#[doc = r" blah blah blah"]
|
||||||
field1: i32,
|
field1: i32,
|
||||||
#[serde(field = "overridden", with = "with")]
|
#[serde(field = "overridden", with = "with")]
|
||||||
#[serde(field2)]
|
#[serde(field2)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue