diff --git a/.gitattributes b/.gitattributes index 4ee0147..ae67b84 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.json text eol=lf +*.stderr text eol=lf diff --git a/docs/_includes/examples/enum_repr.rs b/docs/_includes/examples/enum_repr.rs new file mode 100644 index 0000000..671bc46 --- /dev/null +++ b/docs/_includes/examples/enum_repr.rs @@ -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()); +} diff --git a/docs/_includes/examples/enum_repr.schema.json b/docs/_includes/examples/enum_repr.schema.json new file mode 100644 index 0000000..04841b7 --- /dev/null +++ b/docs/_includes/examples/enum_repr.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SmallPrime", + "type": "integer", + "enum": [ + 2, + 3, + 5, + 7 + ] +} diff --git a/docs/examples/8-enum_repr.md b/docs/examples/8-enum_repr.md new file mode 100644 index 0000000..8b43ce0 --- /dev/null +++ b/docs/examples/8-enum_repr.md @@ -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" %} diff --git a/schemars/examples/enum_repr.rs b/schemars/examples/enum_repr.rs new file mode 100644 index 0000000..671bc46 --- /dev/null +++ b/schemars/examples/enum_repr.rs @@ -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()); +} diff --git a/schemars/examples/enum_repr.schema.json b/schemars/examples/enum_repr.schema.json new file mode 100644 index 0000000..04841b7 --- /dev/null +++ b/schemars/examples/enum_repr.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SmallPrime", + "type": "integer", + "enum": [ + 2, + 3, + 5, + 7 + ] +} diff --git a/schemars/tests/enum_repr.rs b/schemars/tests/enum_repr.rs new file mode 100644 index 0000000..d318347 --- /dev/null +++ b/schemars/tests/enum_repr.rs @@ -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-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::("enum-repr-with-attrs") +} diff --git a/schemars/tests/expected/enum-repr-with-attrs.json b/schemars/tests/expected/enum-repr-with-attrs.json new file mode 100644 index 0000000..7070de8 --- /dev/null +++ b/schemars/tests/expected/enum-repr-with-attrs.json @@ -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 + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/enum-repr.json b/schemars/tests/expected/enum-repr.json new file mode 100644 index 0000000..92d6f3a --- /dev/null +++ b/schemars/tests/expected/enum-repr.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Enum", + "type": "integer", + "enum": [ + 0, + 1, + 5, + 6, + 3 + ] +} \ No newline at end of file diff --git a/schemars/tests/ui/repr_missing.rs b/schemars/tests/ui/repr_missing.rs new file mode 100644 index 0000000..b69ea9a --- /dev/null +++ b/schemars/tests/ui/repr_missing.rs @@ -0,0 +1,8 @@ +use schemars::JsonSchema_repr; + +#[derive(JsonSchema_repr)] +pub enum Enum { + Unit, +} + +fn main() {} diff --git a/schemars/tests/ui/repr_missing.stderr b/schemars/tests/ui/repr_missing.stderr new file mode 100644 index 0000000..495c177 --- /dev/null +++ b/schemars/tests/ui/repr_missing.stderr @@ -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) diff --git a/schemars/tests/ui/repr_non_unit_variant.rs b/schemars/tests/ui/repr_non_unit_variant.rs new file mode 100644 index 0000000..c4e7fee --- /dev/null +++ b/schemars/tests/ui/repr_non_unit_variant.rs @@ -0,0 +1,10 @@ +use schemars::JsonSchema_repr; + +#[derive(JsonSchema_repr)] +#[repr(u8)] +pub enum Enum { + Unit, + EmptyTuple(), +} + +fn main() {} diff --git a/schemars/tests/ui/repr_non_unit_variant.stderr b/schemars/tests/ui/repr_non_unit_variant.stderr new file mode 100644 index 0000000..eb34039 --- /dev/null +++ b/schemars/tests/ui/repr_non_unit_variant.stderr @@ -0,0 +1,5 @@ +error: JsonSchema_repr: must be a unit variant + --> $DIR/repr_non_unit_variant.rs:7:5 + | +7 | EmptyTuple(), + | ^^^^^^^^^^ diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 37919b3..65667d9 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -18,6 +18,7 @@ pub struct Attrs { pub description: Option, pub deprecated: bool, pub examples: Vec, + pub repr: Option, } #[derive(Debug)] @@ -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); diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 3746771..fbe69d4 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -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> { 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 @@ -70,7 +78,7 @@ fn derive_json_schema(mut input: syn::DeriveInput) -> TokenStream { } }; }; - }; + }); } 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] #[allow(unused_braces)] 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 } }; - } + }) } 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) -> TokenStream { - let compile_errors = errors.into_iter().map(syn::Error::to_compile_error); +fn compile_error<'a>(errors: Vec) -> TokenStream { + let compile_errors = errors.iter().map(syn::Error::to_compile_error); quote! { #(#compile_errors)* } diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 905f67a..a5cf2ca 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -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; @@ -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 { + 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(); @@ -300,7 +335,7 @@ fn expr_for_untagged_enum_variant_for_flatten( ) -> Option { 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) }); }