schema_with attribute

This commit is contained in:
Graham Esau 2020-05-15 17:11:28 +01:00
parent 9d951b34ce
commit 3fd316063a
15 changed files with 538 additions and 51 deletions

View file

@ -1,8 +1,12 @@
# Changelog
## In-dev - version TBC
### Added:
- `#[schemars(schema_with = "...")]` attribute can be set on variants and fields. This allows you to specify another function which returns the schema you want, which is particularly useful on fields of types that don't implement the JsonSchema trait (https://github.com/GREsau/schemars/issues/15)
### Fixed
- `#[serde(with = "...")]`/`#[schemars(with = "...")]` attributes on enum variants are now respected
- Some compiler errors generated by schemars_derive should now have more accurate spans
## [0.7.2] - 2020-04-30
### Added:

View file

@ -2,6 +2,9 @@ mod util;
use schemars::{JsonSchema, Map};
use util::*;
// Ensure that schemars_derive uses the full path to std::string::String
pub struct String;
#[derive(Debug, JsonSchema)]
pub struct UnitStruct;
@ -15,7 +18,7 @@ pub struct Struct {
#[schemars(rename_all = "camelCase")]
pub enum External {
UnitOne,
StringMap(Map<String, String>),
StringMap(Map<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -37,7 +40,7 @@ fn enum_external_tag() -> TestResult {
#[schemars(tag = "typeProperty")]
pub enum Internal {
UnitOne,
StringMap(Map<String, String>),
StringMap(Map<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -58,7 +61,7 @@ fn enum_internal_tag() -> TestResult {
#[schemars(untagged)]
pub enum Untagged {
UnitOne,
StringMap(Map<String, String>),
StringMap(Map<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -79,7 +82,7 @@ fn enum_untagged() -> TestResult {
#[schemars(tag = "t", content = "c")]
pub enum Adjacent {
UnitOne,
StringMap(Map<String, String>),
StringMap(Map<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {

View file

@ -0,0 +1,79 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Adjacent",
"anyOf": [
{
"type": "object",
"required": [
"c",
"t"
],
"properties": {
"c": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "boolean"
}
}
},
"t": {
"type": "string",
"enum": [
"Struct"
]
}
}
},
{
"type": "object",
"required": [
"c",
"t"
],
"properties": {
"c": {
"type": "boolean"
},
"t": {
"type": "string",
"enum": [
"NewType"
]
}
}
},
{
"type": "object",
"required": [
"c",
"t"
],
"properties": {
"c": {
"type": "array",
"items": [
{
"type": "boolean"
},
{
"type": "integer",
"format": "int32"
}
],
"maxItems": 2,
"minItems": 2
},
"t": {
"type": "string",
"enum": [
"Tuple"
]
}
}
}
]
}

View file

@ -0,0 +1,58 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "External",
"anyOf": [
{
"type": "object",
"required": [
"struct"
],
"properties": {
"struct": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "boolean"
}
}
}
}
},
{
"type": "object",
"required": [
"newType"
],
"properties": {
"newType": {
"type": "boolean"
}
}
},
{
"type": "object",
"required": [
"tuple"
],
"properties": {
"tuple": {
"type": "array",
"items": [
{
"type": "boolean"
},
{
"type": "integer",
"format": "int32"
}
],
"maxItems": 2,
"minItems": 2
}
}
}
]
}

View file

@ -0,0 +1,41 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Internal",
"anyOf": [
{
"type": "object",
"required": [
"foo",
"typeProperty"
],
"properties": {
"foo": {
"type": "boolean"
},
"typeProperty": {
"type": "string",
"enum": [
"Struct"
]
}
}
},
{
"type": [
"boolean",
"object"
],
"required": [
"typeProperty"
],
"properties": {
"typeProperty": {
"type": "string",
"enum": [
"NewType"
]
}
}
}
]
}

View file

@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Untagged",
"anyOf": [
{
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "boolean"
}
}
},
{
"type": "boolean"
},
{
"type": "array",
"items": [
{
"type": "boolean"
},
{
"type": "integer",
"format": "int32"
}
],
"maxItems": 2,
"minItems": 2
}
]
}

View file

@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Newtype",
"type": "boolean"
}

View file

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Struct",
"type": "object",
"required": [
"bar",
"baz",
"foo"
],
"properties": {
"bar": {
"type": "integer",
"format": "int32"
},
"baz": {
"type": "boolean"
},
"foo": {
"type": "boolean"
}
}
}

View file

@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Tuple",
"type": "array",
"items": [
{
"type": "boolean"
},
{
"type": "integer",
"format": "int32"
},
{
"type": "boolean"
}
],
"maxItems": 3,
"minItems": 3
}

View file

@ -0,0 +1,92 @@
mod util;
use schemars::JsonSchema;
use util::*;
// FIXME determine whether schema_with should be allowed on unit variants
fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<bool>::json_schema(gen)
}
#[derive(Debug)]
pub struct DoesntImplementJsonSchema;
#[derive(Debug, JsonSchema)]
#[schemars(rename_all = "camelCase")]
pub enum External {
Struct {
#[schemars(schema_with = "schema_fn")]
foo: DoesntImplementJsonSchema,
},
NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
Tuple(
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
),
// #[schemars(schema_with = "schema_fn")]
// Unit,
}
#[test]
fn enum_external_tag() -> TestResult {
test_default_generated_schema::<External>("schema_with-enum-external")
}
#[derive(Debug, JsonSchema)]
#[schemars(tag = "typeProperty")]
pub enum Internal {
Struct {
#[schemars(schema_with = "schema_fn")]
foo: DoesntImplementJsonSchema,
},
NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
// #[schemars(schema_with = "schema_fn")]
// Unit,
}
#[test]
fn enum_internal_tag() -> TestResult {
test_default_generated_schema::<Internal>("schema_with-enum-internal")
}
#[derive(Debug, JsonSchema)]
#[schemars(untagged)]
pub enum Untagged {
Struct {
#[schemars(schema_with = "schema_fn")]
foo: DoesntImplementJsonSchema,
},
NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
Tuple(
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
),
// #[schemars(schema_with = "schema_fn")]
// Unit,
}
#[test]
fn enum_untagged() -> TestResult {
test_default_generated_schema::<Untagged>("schema_with-enum-untagged")
}
#[derive(Debug, JsonSchema)]
#[schemars(tag = "t", content = "c")]
pub enum Adjacent {
Struct {
#[schemars(schema_with = "schema_fn")]
foo: DoesntImplementJsonSchema,
},
NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
Tuple(
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
),
// #[schemars(schema_with = "schema_fn")]
// Unit,
}
#[test]
fn enum_adjacent_tagged() -> TestResult {
test_default_generated_schema::<Adjacent>("schema_with-enum-adjacent-tagged")
}

View file

@ -0,0 +1,44 @@
mod util;
use schemars::JsonSchema;
use util::*;
fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<bool>::json_schema(gen)
}
#[derive(Debug)]
struct DoesntImplementJsonSchema;
#[derive(Debug, JsonSchema)]
pub struct Struct {
#[schemars(schema_with = "schema_fn")]
foo: DoesntImplementJsonSchema,
bar: i32,
#[schemars(schema_with = "schema_fn")]
baz: DoesntImplementJsonSchema,
}
#[test]
fn struct_normal() -> TestResult {
test_default_generated_schema::<Struct>("schema_with-struct")
}
#[derive(Debug, JsonSchema)]
pub struct Tuple(
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
);
#[test]
fn struct_tuple() -> TestResult {
test_default_generated_schema::<Tuple>("schema_with-tuple")
}
#[derive(Debug, JsonSchema)]
pub struct Newtype(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema);
#[test]
fn struct_newtype() -> TestResult {
test_default_generated_schema::<Newtype>("schema_with-newtype")
}

View file

@ -2,11 +2,14 @@ mod util;
use schemars::JsonSchema;
use util::*;
// Ensure that schemars_derive uses the full path to std::string::String
pub struct String;
#[derive(Debug, JsonSchema)]
pub struct Struct {
foo: i32,
bar: bool,
baz: Option<String>,
baz: Option<&'static str>,
}
#[test]
@ -15,7 +18,7 @@ fn struct_normal() -> TestResult {
}
#[derive(Debug, JsonSchema)]
pub struct Tuple(i32, bool, Option<String>);
pub struct Tuple(i32, bool, Option<&'static str>);
#[test]
fn struct_tuple() -> TestResult {

View file

@ -1,6 +1,6 @@
mod from_serde;
use crate::attr::{Attrs, WithAttr};
use crate::attr::Attrs;
use from_serde::FromSerde;
use serde_derive_internals::ast as serde_ast;
use serde_derive_internals::{Ctxt, Derive};
@ -68,12 +68,4 @@ impl<'a> Field<'a> {
pub fn name(&self) -> String {
self.serde_attrs.name().deserialize_name()
}
pub fn type_for_schema(&self) -> &syn::Type {
match &self.attrs.with {
None => self.ty,
Some(WithAttr::Type(ty)) => ty,
Some(WithAttr::_Function(_)) => unimplemented!(), // TODO
}
}
}

View file

@ -22,7 +22,7 @@ pub struct Attrs {
#[derive(Debug)]
pub enum WithAttr {
Type(syn::Type),
_Function(syn::Path),
Function(syn::Path),
}
impl Attrs {
@ -74,14 +74,22 @@ impl Attrs {
if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) {
match self.with {
Some(WithAttr::Type(_)) => duplicate_error(m),
Some(WithAttr::_Function(_)) => {
mutual_exclusive_error(m, "schema_with")
}
Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"),
None => self.with = Some(WithAttr::Type(ty)),
}
}
}
Meta(NameValue(m)) if m.path.is_ident("schema_with") => {
if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.lit) {
match self.with {
Some(WithAttr::Function(_)) => duplicate_error(m),
Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"),
None => self.with = Some(WithAttr::Function(fun)),
}
}
}
Meta(_meta_item) => {
// TODO uncomment this for 0.8.0 (breaking change)
// https://github.com/GREsau/schemars/issues/18
@ -148,8 +156,8 @@ fn get_lit_str<'a>(
cx.error_spanned_by(
lit,
format!(
"expected {} attribute to be a string: `{} = \"...\"`",
attr_type, meta_item_name
"expected {} {} attribute to be a string: `{} = \"...\"`",
attr_type, meta_item_name, meta_item_name
),
);
Err(())
@ -167,7 +175,31 @@ fn parse_lit_into_ty(
parse_lit_str(string).map_err(|_| {
cx.error_spanned_by(
lit,
format!("failed to parse type: {} = {:?}", attr_type, string.value()),
format!(
"failed to parse type: `{} = {:?}`",
meta_item_name,
string.value()
),
)
})
}
fn parse_lit_into_path(
cx: &Ctxt,
attr_type: &'static str,
meta_item_name: &'static str,
lit: &syn::Lit,
) -> Result<syn::Path, ()> {
let string = get_lit_str(cx, attr_type, meta_item_name, lit)?;
parse_lit_str(string).map_err(|_| {
cx.error_spanned_by(
lit,
format!(
"failed to parse path: `{} = {:?}`",
meta_item_name,
string.value()
),
)
})
}

View file

@ -17,6 +17,58 @@ pub fn expr_for_container(cont: &Container) -> TokenStream {
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();
if allow_ref {
quote_spanned! {span=>
{
#type_def
gen.subschema_for::<#ty>()
}
}
} else {
quote_spanned! {span=>
{
#type_def
<#ty as schemars::JsonSchema>::json_schema(gen)
}
}
}
}
fn type_for_schema(field: &Field, local_id: usize) -> (syn::Type, Option<TokenStream>) {
match &field.attrs.with {
None => (field.ty.to_owned(), None),
Some(WithAttr::Type(ty)) => (ty.to_owned(), None),
Some(WithAttr::Function(fun)) => {
let ty_name = format_ident!("_SchemarsSchemaWithFunction{}", local_id);
let fn_name = fun.segments.last().unwrap().ident.to_string();
let type_def = quote_spanned! {fun.span()=>
struct #ty_name;
impl schemars::JsonSchema for #ty_name {
fn is_referenceable() -> bool {
false
}
fn schema_name() -> std::string::String {
#fn_name.to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
#fun(gen)
}
}
};
(parse_quote!(#ty_name), Some(type_def))
}
}
}
fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenStream {
let variants = variants
.iter()
@ -208,7 +260,7 @@ fn expr_for_untagged_enum_variant(variant: &Variant) -> TokenStream {
match variant.style {
Style::Unit => expr_for_unit_struct(),
Style::Newtype => expr_for_newtype_struct(&variant.fields[0]),
Style::Newtype => expr_for_field(&variant.fields[0], true),
Style::Tuple => expr_for_tuple_struct(&variant.fields),
Style::Struct => expr_for_struct(&variant.fields, None),
}
@ -223,13 +275,7 @@ fn expr_for_untagged_enum_variant_for_flatten(variant: &Variant) -> Option<Token
Some(match variant.style {
Style::Unit => return None,
Style::Newtype => {
let field = &variant.fields[0];
let ty = field.type_for_schema();
quote_spanned! {field.original.span()=>
<#ty>::json_schema(gen)
}
}
Style::Newtype => expr_for_field(&variant.fields[0], false),
Style::Tuple => expr_for_tuple_struct(&variant.fields),
Style::Struct => expr_for_struct(&variant.fields, None),
})
@ -242,21 +288,23 @@ fn expr_for_unit_struct() -> TokenStream {
}
fn expr_for_newtype_struct(field: &Field) -> TokenStream {
let ty = field.type_for_schema();
quote_spanned! {field.original.span()=>
gen.subschema_for::<#ty>()
}
expr_for_field(field, true)
}
fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
let types = fields
let (types, type_defs): (Vec<_>, Vec<_>) = fields
.iter()
.filter(|f| !f.serde_attrs.skip_deserializing())
.map(Field::type_for_schema);
.enumerate()
.map(|(i, f)| type_for_schema(f, i))
.unzip();
quote! {
{
#(#type_defs)*
gen.subschema_for::<(#(#types),*)>()
}
}
}
fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) -> TokenStream {
let (flattened_fields, property_fields): (Vec<_>, Vec<_>) = fields
@ -270,7 +318,9 @@ fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) ->
SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)),
});
let properties = property_fields.iter().map(|field| {
let mut type_defs = Vec::new();
let properties: Vec<_> = property_fields.into_iter().map(|field| {
let name = field.name();
let default = field_default_expr(field, set_container_default.is_some());
@ -286,25 +336,34 @@ fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) ->
..SchemaMetadata::from_doc_attrs(&field.original.attrs)
};
let ty = field.type_for_schema();
let span = field.original.span();
quote_spanned! {span=>
<#ty>::add_schema_as_property(gen, &mut schema_object, #name.to_owned(), #metadata, #required);
let (ty, type_def) = type_for_schema(field, type_defs.len());
if let Some(type_def) = type_def {
type_defs.push(type_def);
}
});
let flattens = flattened_fields.iter().map(|field| {
let ty = field.type_for_schema();
let span = field.original.span();
quote_spanned! {span=>
.flatten(<#ty>::json_schema_for_flatten(gen))
quote_spanned! {ty.span()=>
<#ty as schemars::JsonSchema>::add_schema_as_property(gen, &mut schema_object, #name.to_owned(), #metadata, #required);
}
});
}).collect();
let flattens: Vec<_> = flattened_fields
.into_iter()
.map(|field| {
let (ty, type_def) = type_for_schema(field, type_defs.len());
if let Some(type_def) = type_def {
type_defs.push(type_def);
}
quote_spanned! {ty.span()=>
.flatten(<#ty as schemars::JsonSchema>::json_schema_for_flatten(gen))
}
})
.collect();
quote! {
{
#(#type_defs)*
#set_container_default
let mut schema_object = schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Object.into()),