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
|
||||
}
|
||||
|
||||
// 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
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 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,16 +74,20 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
|||
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)
|
||||
.map(|i| !schemars_meta_names.contains(&i.to_string()))
|
||||
.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),*)]
|
||||
};
|
||||
|
@ -90,6 +99,9 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
|||
}
|
||||
}
|
||||
|
||||
attrs.extend(misc_attrs)
|
||||
}
|
||||
|
||||
fn to_tokens(attrs: &[Attribute]) -> impl ToTokens {
|
||||
let mut tokens = proc_macro2::TokenStream::new();
|
||||
for attr in attrs {
|
||||
|
@ -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