Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive JsonSchema_repr #76

Merged
merged 5 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
*.json text eol=lf
*.stderr text eol=lf
14 changes: 14 additions & 0 deletions docs/_includes/examples/enum_repr.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions docs/_includes/examples/enum_repr.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SmallPrime",
"type": "integer",
"enum": [
2,
3,
5,
7
]
}
14 changes: 14 additions & 0 deletions docs/examples/8-enum_repr.md
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions schemars/examples/enum_repr.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions schemars/examples/enum_repr.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SmallPrime",
"type": "integer",
"enum": [
2,
3,
5,
7
]
}
35 changes: 35 additions & 0 deletions schemars/tests/enum_repr.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions schemars/tests/expected/enum-repr-with-attrs.json
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions schemars/tests/expected/enum-repr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Enum",
"type": "integer",
"enum": [
0,
1,
5,
6,
3
]
}
8 changes: 8 additions & 0 deletions schemars/tests/ui/repr_missing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use schemars::JsonSchema_repr;

#[derive(JsonSchema_repr)]
pub enum Enum {
Unit,
}

fn main() {}
7 changes: 7 additions & 0 deletions schemars/tests/ui/repr_missing.stderr
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions schemars/tests/ui/repr_non_unit_variant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use schemars::JsonSchema_repr;

#[derive(JsonSchema_repr)]
#[repr(u8)]
pub enum Enum {
Unit,
EmptyTuple(),
}

fn main() {}
5 changes: 5 additions & 0 deletions schemars/tests/ui/repr_non_unit_variant.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: JsonSchema_repr: must be a unit variant
--> $DIR/repr_non_unit_variant.rs:7:5
|
7 | EmptyTuple(),
| ^^^^^^^^^^
5 changes: 5 additions & 0 deletions schemars_derive/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Attrs {
pub description: Option<String>,
pub deprecated: bool,
pub examples: Vec<syn::Path>,
pub repr: Option<syn::Type>,
}

#[derive(Debug)]
Expand All @@ -33,6 +34,10 @@ impl Attrs {
.populate(attrs, "serde", true, errors);

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);
result.title = result.title.or(doc_title);
Expand Down
44 changes: 28 additions & 16 deletions schemars_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,35 @@ use proc_macro2::TokenStream;
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
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);

if let Err(e) = attr::process_serde_attrs(&mut input) {
return compile_error(&e);
}
attr::process_serde_attrs(&mut input)?;

let cont = match Container::from_ast(&input) {
Ok(c) => c,
Err(e) => return compile_error(&e),
};
let cont = Container::from_ast(&input)?;

let type_name = &cont.ident;
let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();

if let Some(transparent_field) = cont.transparent_field() {
let (ty, type_def) = schema_exprs::type_for_schema(transparent_field, 0);
return quote! {
return Ok(quote! {
const _: () = {
#type_def

Expand Down Expand Up @@ -70,7 +78,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream {
}
};
};
};
});
}

let mut schema_base_name = cont.name();
Expand Down Expand Up @@ -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]
#[allow(unused_braces)]
impl #impl_generics schemars::JsonSchema for #type_name #ty_generics #where_clause {
Expand All @@ -120,7 +132,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream {
#schema_expr
}
};
}
})
}

fn add_trait_bounds(generics: &mut syn::Generics) {
Expand All @@ -131,8 +143,8 @@ fn add_trait_bounds(generics: &mut syn::Generics) {
}
}

fn compile_error<'a>(errors: impl IntoIterator<Item = &'a syn::Error>) -> TokenStream {
let compile_errors = errors.into_iter().map(syn::Error::to_compile_error);
fn compile_error<'a>(errors: Vec<syn::Error>) -> TokenStream {
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
quote! {
#(#compile_errors)*
}
Expand Down
39 changes: 37 additions & 2 deletions schemars_derive/src/schema_exprs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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::attr::{self as serde_attr, Default as SerdeDefault, TagType};
use syn::spanned::Spanned;
Expand All @@ -21,6 +21,41 @@ pub fn expr_for_container(cont: &Container) -> TokenStream {
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 {
let (ty, type_def) = type_for_schema(field, 0);
let span = field.original.span();
Expand Down Expand Up @@ -300,7 +335,7 @@ fn expr_for_untagged_enum_variant_for_flatten(
) -> Option<TokenStream> {
if let Some(WithAttr::Type(with)) = &variant.attrs.with {
return Some(quote_spanned! {variant.original.span()=>
<#with>::json_schema(gen)
<#with as schemars::JsonSchema>::json_schema(gen)
});
}

Expand Down