Derive JsonSchema_repr (#76)
This commit is contained in:
parent
7de2b2276f
commit
11d95b79e5
16 changed files with 225 additions and 18 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +1,2 @@
|
||||||
*.json text eol=lf
|
*.json text eol=lf
|
||||||
|
*.stderr text eol=lf
|
||||||
|
|
14
docs/_includes/examples/enum_repr.rs
Normal file
14
docs/_includes/examples/enum_repr.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use schemars::{schema_for, JsonSchema_repr};
|
||||||
|
|
||||||
|
#[derive(JsonSchema_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum SmallPrime {
|
||||||
|
Two = 2,
|
||||||
|
Three = 3,
|
||||||
|
Five = 5,
|
||||||
|
Seven = 7,
|
||||||
|
}
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(SmallPrime);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
11
docs/_includes/examples/enum_repr.schema.json
Normal file
11
docs/_includes/examples/enum_repr.schema.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "SmallPrime",
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
14
docs/examples/8-enum_repr.md
Normal file
14
docs/examples/8-enum_repr.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Serialize enum as number (serde_repr)
|
||||||
|
parent: Examples
|
||||||
|
nav_order: 8
|
||||||
|
summary: >-
|
||||||
|
Generating a schema for with a C-like enum compatible with serde_repr.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deriving JsonSchema with Fields Using Custom Serialization
|
||||||
|
|
||||||
|
If you use the `#[repr(...)]` attribute on an enum to give it a C-like representation, then you may also want to use the [serde_repr](https://github.com/dtolnay/serde-repr) crate to serialize the enum values as numbers. In this case, you should use the corresponding `JsonSchema_repr` derive to ensure the schema for your type reflects how serde formats your type.
|
||||||
|
|
||||||
|
{% include example.md name="enum_repr" %}
|
14
schemars/examples/enum_repr.rs
Normal file
14
schemars/examples/enum_repr.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use schemars::{schema_for, JsonSchema_repr};
|
||||||
|
|
||||||
|
#[derive(JsonSchema_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum SmallPrime {
|
||||||
|
Two = 2,
|
||||||
|
Three = 3,
|
||||||
|
Five = 5,
|
||||||
|
Seven = 7,
|
||||||
|
}
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(SmallPrime);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
11
schemars/examples/enum_repr.schema.json
Normal file
11
schemars/examples/enum_repr.schema.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "SmallPrime",
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
35
schemars/tests/enum_repr.rs
Normal file
35
schemars/tests/enum_repr.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
mod util;
|
||||||
|
use schemars::JsonSchema_repr;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
#[derive(JsonSchema_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Enum {
|
||||||
|
Zero,
|
||||||
|
One,
|
||||||
|
Five = 5,
|
||||||
|
Six,
|
||||||
|
Three = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_repr() -> TestResult {
|
||||||
|
test_default_generated_schema::<Enum>("enum-repr")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema_repr)]
|
||||||
|
#[repr(i64)]
|
||||||
|
#[serde(rename = "Renamed")]
|
||||||
|
/// Description from comment
|
||||||
|
pub enum EnumWithAttrs {
|
||||||
|
Zero,
|
||||||
|
One,
|
||||||
|
Five = 5,
|
||||||
|
Six,
|
||||||
|
Three = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_repr_with_attrs() -> TestResult {
|
||||||
|
test_default_generated_schema::<EnumWithAttrs>("enum-repr-with-attrs")
|
||||||
|
}
|
13
schemars/tests/expected/enum-repr-with-attrs.json
Normal file
13
schemars/tests/expected/enum-repr-with-attrs.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Renamed",
|
||||||
|
"description": "Description from comment",
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
12
schemars/tests/expected/enum-repr.json
Normal file
12
schemars/tests/expected/enum-repr.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Enum",
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
}
|
8
schemars/tests/ui/repr_missing.rs
Normal file
8
schemars/tests/ui/repr_missing.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use schemars::JsonSchema_repr;
|
||||||
|
|
||||||
|
#[derive(JsonSchema_repr)]
|
||||||
|
pub enum Enum {
|
||||||
|
Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
7
schemars/tests/ui/repr_missing.stderr
Normal file
7
schemars/tests/ui/repr_missing.stderr
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
error: JsonSchema_repr: missing #[repr(...)] attribute
|
||||||
|
--> $DIR/repr_missing.rs:3:10
|
||||||
|
|
|
||||||
|
3 | #[derive(JsonSchema_repr)]
|
||||||
|
| ^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
10
schemars/tests/ui/repr_non_unit_variant.rs
Normal file
10
schemars/tests/ui/repr_non_unit_variant.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use schemars::JsonSchema_repr;
|
||||||
|
|
||||||
|
#[derive(JsonSchema_repr)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Enum {
|
||||||
|
Unit,
|
||||||
|
EmptyTuple(),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
5
schemars/tests/ui/repr_non_unit_variant.stderr
Normal file
5
schemars/tests/ui/repr_non_unit_variant.stderr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
error: JsonSchema_repr: must be a unit variant
|
||||||
|
--> $DIR/repr_non_unit_variant.rs:7:5
|
||||||
|
|
|
||||||
|
7 | EmptyTuple(),
|
||||||
|
| ^^^^^^^^^^
|
|
@ -18,6 +18,7 @@ pub struct Attrs {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub deprecated: bool,
|
pub deprecated: bool,
|
||||||
pub examples: Vec<syn::Path>,
|
pub examples: Vec<syn::Path>,
|
||||||
|
pub repr: Option<syn::Type>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -33,6 +34,10 @@ impl Attrs {
|
||||||
.populate(attrs, "serde", true, errors);
|
.populate(attrs, "serde", true, errors);
|
||||||
|
|
||||||
result.deprecated = attrs.iter().any(|a| a.path.is_ident("deprecated"));
|
result.deprecated = attrs.iter().any(|a| a.path.is_ident("deprecated"));
|
||||||
|
result.repr = attrs
|
||||||
|
.iter()
|
||||||
|
.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);
|
let (doc_title, doc_description) = doc::get_title_and_desc_from_doc(attrs);
|
||||||
result.title = result.title.or(doc_title);
|
result.title = result.title.or(doc_title);
|
||||||
|
|
|
@ -17,27 +17,35 @@ use proc_macro2::TokenStream;
|
||||||
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
|
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
|
||||||
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||||
derive_json_schema(input).into()
|
derive_json_schema(input, false)
|
||||||
|
.unwrap_or_else(compile_error)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream {
|
#[proc_macro_derive(JsonSchema_repr, attributes(schemars, serde))]
|
||||||
|
pub fn derive_json_schema_repr_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||||
|
derive_json_schema(input, true)
|
||||||
|
.unwrap_or_else(compile_error)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_json_schema(
|
||||||
|
mut input: syn::DeriveInput,
|
||||||
|
repr: bool,
|
||||||
|
) -> Result<TokenStream, Vec<syn::Error>> {
|
||||||
add_trait_bounds(&mut input.generics);
|
add_trait_bounds(&mut input.generics);
|
||||||
|
|
||||||
if let Err(e) = attr::process_serde_attrs(&mut input) {
|
attr::process_serde_attrs(&mut input)?;
|
||||||
return compile_error(&e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cont = match Container::from_ast(&input) {
|
let cont = Container::from_ast(&input)?;
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => return compile_error(&e),
|
|
||||||
};
|
|
||||||
|
|
||||||
let type_name = &cont.ident;
|
let type_name = &cont.ident;
|
||||||
let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();
|
||||||
|
|
||||||
if let Some(transparent_field) = cont.transparent_field() {
|
if let Some(transparent_field) = cont.transparent_field() {
|
||||||
let (ty, type_def) = schema_exprs::type_for_schema(transparent_field, 0);
|
let (ty, type_def) = schema_exprs::type_for_schema(transparent_field, 0);
|
||||||
return quote! {
|
return Ok(quote! {
|
||||||
const _: () = {
|
const _: () = {
|
||||||
#type_def
|
#type_def
|
||||||
|
|
||||||
|
@ -70,7 +78,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut schema_base_name = cont.name();
|
let mut schema_base_name = cont.name();
|
||||||
|
@ -106,9 +114,13 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let schema_expr = schema_exprs::expr_for_container(&cont);
|
let schema_expr = if repr {
|
||||||
|
schema_exprs::expr_for_repr(&cont).map_err(|e| vec![e])?
|
||||||
|
} else {
|
||||||
|
schema_exprs::expr_for_container(&cont)
|
||||||
|
};
|
||||||
|
|
||||||
quote! {
|
Ok(quote! {
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
#[allow(unused_braces)]
|
#[allow(unused_braces)]
|
||||||
impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause {
|
impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause {
|
||||||
|
@ -120,7 +132,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream {
|
||||||
#schema_expr
|
#schema_expr
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_trait_bounds(generics: &mut syn::Generics) {
|
fn add_trait_bounds(generics: &mut syn::Generics) {
|
||||||
|
@ -131,8 +143,8 @@ fn add_trait_bounds(generics: &mut syn::Generics) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_error<'a>(errors: impl IntoIterator<Item = &'a syn::Error>) -> TokenStream {
|
fn compile_error<'a>(errors: Vec<syn::Error>) -> TokenStream {
|
||||||
let compile_errors = errors.into_iter().map(syn::Error::to_compile_error);
|
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
|
||||||
quote! {
|
quote! {
|
||||||
#(#compile_errors)*
|
#(#compile_errors)*
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{ast::*, attr::WithAttr, metadata::SchemaMetadata};
|
use crate::{ast::*, attr::WithAttr, metadata::SchemaMetadata};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::{Span, TokenStream};
|
||||||
use serde_derive_internals::ast::Style;
|
use serde_derive_internals::ast::Style;
|
||||||
use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault, TagType};
|
use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault, TagType};
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
|
@ -21,6 +21,41 @@ pub fn expr_for_container(cont: &Container) -> TokenStream {
|
||||||
doc_metadata.apply_to_schema(schema_expr)
|
doc_metadata.apply_to_schema(schema_expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
|
||||||
|
let repr_type = cont.attrs.repr.as_ref().ok_or_else(|| {
|
||||||
|
syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"JsonSchema_repr: missing #[repr(...)] attribute",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let variants = match &cont.data {
|
||||||
|
Data::Enum(variants) => variants,
|
||||||
|
_ => return Err(syn::Error::new(Span::call_site(), "oh no!")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(non_unit_error) = variants.iter().find_map(|v| match v.style {
|
||||||
|
Style::Unit => None,
|
||||||
|
_ => Some(syn::Error::new(
|
||||||
|
v.original.span(),
|
||||||
|
"JsonSchema_repr: must be a unit variant",
|
||||||
|
)),
|
||||||
|
}) {
|
||||||
|
return Err(non_unit_error);
|
||||||
|
};
|
||||||
|
|
||||||
|
let enum_ident = &cont.ident;
|
||||||
|
let variant_idents = variants.iter().map(|v| &v.ident);
|
||||||
|
|
||||||
|
let schema_expr = schema_object(quote! {
|
||||||
|
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
|
||||||
|
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
|
||||||
|
});
|
||||||
|
|
||||||
|
let doc_metadata = SchemaMetadata::from_attrs(&cont.attrs);
|
||||||
|
Ok(doc_metadata.apply_to_schema(schema_expr))
|
||||||
|
}
|
||||||
|
|
||||||
fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
|
fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
|
||||||
let (ty, type_def) = type_for_schema(field, 0);
|
let (ty, type_def) = type_for_schema(field, 0);
|
||||||
let span = field.original.span();
|
let span = field.original.span();
|
||||||
|
@ -300,7 +335,7 @@ fn expr_for_untagged_enum_variant_for_flatten(
|
||||||
) -> Option<TokenStream> {
|
) -> Option<TokenStream> {
|
||||||
if let Some(WithAttr::Type(with)) = &variant.attrs.with {
|
if let Some(WithAttr::Type(with)) = &variant.attrs.with {
|
||||||
return Some(quote_spanned! {variant.original.span()=>
|
return Some(quote_spanned! {variant.original.span()=>
|
||||||
<#with>::json_schema(gen)
|
<#with as schemars::JsonSchema>::json_schema(gen)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue