Add strongly-typed validation fields

This commit is contained in:
Graham Esau 2019-09-12 22:49:38 +01:00
parent 5de6bcfdef
commit 88a8e0a706
9 changed files with 597 additions and 99 deletions

View file

@ -1,7 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::{JsonSchema, Map, Result};
use serde_json::json;
use crate::{JsonSchema, Result};
// Does not require T: JsonSchema.
impl<T> JsonSchema for [T; 0] {
@ -12,11 +11,12 @@ impl<T> JsonSchema for [T; 0] {
}
fn json_schema(_: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new();
extensions.insert("maxItems".to_owned(), json!(0));
Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()),
extensions,
array: ArrayValidation {
max_items: Some(0),
..Default::default()
},
..Default::default()
}
.into())
@ -34,13 +34,14 @@ macro_rules! array_impls {
}
fn json_schema(gen: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new();
extensions.insert("minItems".to_owned(), json!($len));
extensions.insert("maxItems".to_owned(), json!($len));
Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()),
items: Some(gen.subschema_for::<T>()?.into()),
extensions,
array: ArrayValidation {
items: Some(gen.subschema_for::<T>()?.into()),
max_items: Some($len),
min_items: Some($len),
..Default::default()
},
..Default::default()
}.into())
}
@ -69,9 +70,12 @@ mod tests {
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Array))
);
assert_eq!(schema.extensions.get("minItems"), Some(&json!(8)));
assert_eq!(schema.extensions.get("maxItems"), Some(&json!(8)));
assert_eq!(schema.items, Some(SingleOrVec::from(schema_for::<i32>())));
assert_eq!(
schema.array.items,
Some(SingleOrVec::from(schema_for::<i32>()))
);
assert_eq!(schema.array.max_items, Some(8));
assert_eq!(schema.array.min_items, Some(8));
}
// SomeStruct does not implement JsonSchema
@ -84,6 +88,6 @@ mod tests {
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Array))
);
assert_eq!(schema.extensions.get("maxItems"), Some(&json!(0)));
assert_eq!(schema.array.max_items, Some(0));
}
}

View file

@ -1,7 +1,6 @@
use crate::gen::{BoolSchemas, SchemaGenerator};
use crate::schema::*;
use crate::{JsonSchema, Map, Result};
use serde_json::json;
use crate::{JsonSchema, Result};
macro_rules! map_impl {
($($desc:tt)+) => {
@ -20,18 +19,18 @@ macro_rules! map_impl {
let subschema = gen.subschema_for::<V>()?;
let json_schema_bool = gen.settings().bool_schemas == BoolSchemas::AdditionalPropertiesOnly
&& subschema == gen.schema_for_any();
let mut extensions = Map::new();
extensions.insert(
"additionalProperties".to_owned(),
let additional_properties =
if json_schema_bool {
json!(true)
true.into()
} else {
json!(subschema)
}
);
subschema.into()
};
Ok(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
extensions,
object: ObjectValidation {
additional_properties: Some(Box::new(additional_properties)),
..Default::default()
},
..Default::default()
}.into())
}
@ -62,10 +61,10 @@ mod tests {
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Object))
);
assert_eq!(
schema.extensions.get("additionalProperties"),
Some(&json!(true))
);
let additional_properties = schema.object
.additional_properties
.expect("additionalProperties field present");
assert_eq!(*additional_properties, Schema::Bool(true));
}
}
@ -80,10 +79,10 @@ mod tests {
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Object))
);
assert_eq!(
schema.extensions.get("additionalProperties"),
Some(&json!(Schema::Object(Default::default())))
);
let additional_properties = schema.object
.additional_properties
.expect("additionalProperties field present");
assert_eq!(*additional_properties, Schema::Object(Default::default()));
}
#[test]
@ -102,10 +101,10 @@ mod tests {
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Object))
);
assert_eq!(
schema.extensions.get("additionalProperties"),
Some(&json!(schema_for::<i32>()))
);
let additional_properties = schema.object
.additional_properties
.expect("additionalProperties field present");
assert_eq!(*additional_properties, schema_for::<i32>());
}
}
}

View file

@ -1,7 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::{JsonSchema, Map, Result};
use serde_json::json;
use crate::{JsonSchema, Result};
macro_rules! simple_impl {
($type:tt => $instance_type:ident) => {
@ -57,12 +56,13 @@ impl JsonSchema for char {
}
fn json_schema(_: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new();
extensions.insert("minLength".to_owned(), json!(1));
extensions.insert("maxLength".to_owned(), json!(1));
Ok(SchemaObject {
instance_type: Some(InstanceType::String.into()),
extensions,
string: StringValidation {
min_length: Some(1),
max_length: Some(1),
..Default::default()
},
..Default::default()
}
.into())

View file

@ -17,7 +17,10 @@ macro_rules! seq_impl {
fn json_schema(gen: &mut SchemaGenerator) -> Result {
Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()),
items: Some(gen.subschema_for::<T>()?.into()),
array: ArrayValidation {
items: Some(gen.subschema_for::<T>()?.into()),
..Default::default()
},
..Default::default()
}.into())
}

View file

@ -1,7 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::{JsonSchema, Map, Result};
use serde_json::json;
use crate::{JsonSchema, Result};
macro_rules! tuple_impls {
($($len:expr => ($($name:ident)+))+) => {
@ -14,16 +13,17 @@ macro_rules! tuple_impls {
}
fn json_schema(gen: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new();
extensions.insert("minItems".to_owned(), json!($len));
extensions.insert("maxItems".to_owned(), json!($len));
let items = vec![
$(gen.subschema_for::<$name>()?),+
];
Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()),
items: Some(items.into()),
extensions,
array: ArrayValidation {
items: Some(items.into()),
max_items: Some($len),
min_items: Some($len),
..Default::default()
},
..Default::default()
}.into())
}
@ -64,14 +64,14 @@ mod tests {
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Array))
);
assert_eq!(schema.extensions.get("minItems"), Some(&json!(2)));
assert_eq!(schema.extensions.get("maxItems"), Some(&json!(2)));
assert_eq!(
schema.items,
schema.array.items,
Some(SingleOrVec::Vec(vec![
schema_for::<i32>(),
schema_for::<bool>()
]))
);
assert_eq!(schema.array.max_items, Some(2));
assert_eq!(schema.array.min_items, Some(2));
}
}

View file

@ -43,9 +43,6 @@ impl Schema {
id: s1.id.or(s2.id),
title: s1.title.or(s2.title),
description: s1.description.or(s2.description),
items: s1.items.or(s2.items),
properties: extend(s1.properties, s2.properties),
required: extend(s1.required, s2.required),
definitions: extend(s1.definitions, s2.definitions),
extensions: extend(s1.extensions, s2.extensions),
// TODO do the following make sense?
@ -56,6 +53,38 @@ impl Schema {
any_of: s1.any_of.or(s2.any_of),
one_of: s1.one_of.or(s2.one_of),
not: s1.not.or(s2.not),
if_schema: s1.if_schema.or(s2.if_schema),
then_schema: s1.then_schema.or(s2.then_schema),
else_schema: s1.else_schema.or(s2.else_schema),
number: NumberValidation {
multiple_of: s1.number.multiple_of.or(s2.number.multiple_of),
maximum: s1.number.maximum.or(s2.number.maximum),
exclusive_maximum: s1.number.exclusive_maximum.or(s2.number.exclusive_maximum),
minimum: s1.number.minimum.or(s2.number.minimum),
exclusive_minimum: s1.number.exclusive_minimum.or(s2.number.exclusive_minimum),
},
string: StringValidation {
max_length: s1.string.max_length.or(s2.string.max_length),
min_length: s1.string.min_length.or(s2.string.min_length),
pattern: s1.string.pattern.or(s2.string.pattern),
},
array: ArrayValidation {
items: s1.array.items.or(s2.array.items),
additional_items: s1.array.additional_items.or(s2.array.additional_items),
max_items: s1.array.max_items.or(s2.array.max_items),
min_items: s1.array.min_items.or(s2.array.min_items),
unique_items: s1.array.unique_items.or(s2.array.unique_items),
contains: s1.array.contains.or(s2.array.contains),
},
object: ObjectValidation {
max_properties: s1.object.max_properties.or(s2.object.max_properties),
min_properties: s1.object.min_properties.or(s2.object.min_properties),
required: extend(s1.object.required, s2.object.required),
properties: extend(s1.object.properties, s2.object.properties),
pattern_properties: extend(s1.object.pattern_properties, s2.object.pattern_properties),
additional_properties: s1.object.additional_properties.or(s2.object.additional_properties),
property_names: s1.object.property_names.or(s2.object.property_names),
},
}))
}
@ -111,12 +140,6 @@ pub struct SchemaObject {
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<SingleOrVec<Schema>>,
#[serde(skip_serializing_if = "Map::is_empty")]
pub properties: Map<String, Schema>,
#[serde(skip_serializing_if = "Set::is_empty")]
pub required: Set<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub any_of: Option<Vec<Schema>>,
@ -124,12 +147,88 @@ pub struct SchemaObject {
pub one_of: Option<Vec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Box<Schema>>,
#[serde(rename = "if", skip_serializing_if = "Option::is_none")]
pub if_schema: Option<Box<Schema>>,
#[serde(rename = "then", skip_serializing_if = "Option::is_none")]
pub then_schema: Option<Box<Schema>>,
#[serde(rename = "else", skip_serializing_if = "Option::is_none")]
pub else_schema: Option<Box<Schema>>,
#[serde(skip_serializing_if = "Map::is_empty")]
pub definitions: Map<String, Schema>,
#[serde(flatten)]
pub number: NumberValidation,
#[serde(flatten)]
pub string: StringValidation,
#[serde(flatten)]
pub array: ArrayValidation,
#[serde(flatten)]
pub object: ObjectValidation,
#[serde(flatten)]
pub extensions: Map<String, Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct NumberValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct StringValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct ArrayValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<SingleOrVec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_items: Option<Box<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contains: Option<Box<Schema>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
pub struct ObjectValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<u32>,
#[serde(skip_serializing_if = "Set::is_empty")]
pub required: Set<String>,
#[serde(skip_serializing_if = "Map::is_empty")]
pub properties: Map<String, Schema>,
#[serde(skip_serializing_if = "Map::is_empty")]
pub pattern_properties: Map<String, Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Box<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<Box<Schema>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum InstanceType {

View file

@ -26,14 +26,14 @@
},
"Ref": {
"type": "object",
"required": [
"$ref"
],
"properties": {
"$ref": {
"type": "string"
}
},
"required": [
"$ref"
]
}
},
"Schema": {
"anyOf": [
@ -59,6 +59,34 @@
"type": "string",
"nullable": true
},
"additionalItems": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"additionalProperties": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"allOf": {
"type": "array",
"items": {
@ -73,6 +101,20 @@
},
"nullable": true
},
"contains": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"definitions": {
"type": "object",
"additionalProperties": {
@ -83,15 +125,53 @@
"type": "string",
"nullable": true
},
"else": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"enum": {
"type": "array",
"items": {},
"nullable": true
},
"exclusiveMaximum": {
"type": "number",
"format": "double",
"nullable": true
},
"exclusiveMinimum": {
"type": "number",
"format": "double",
"nullable": true
},
"format": {
"type": "string",
"nullable": true
},
"if": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"items": {
"anyOf": [
{
@ -106,6 +186,51 @@
],
"nullable": true
},
"maxItems": {
"type": "integer",
"format": "uint32",
"nullable": true
},
"maxLength": {
"type": "integer",
"format": "uint32",
"nullable": true
},
"maxProperties": {
"type": "integer",
"format": "uint32",
"nullable": true
},
"maximum": {
"type": "number",
"format": "double",
"nullable": true
},
"minItems": {
"type": "integer",
"format": "uint32",
"nullable": true
},
"minLength": {
"type": "integer",
"format": "uint32",
"nullable": true
},
"minProperties": {
"type": "integer",
"format": "uint32",
"nullable": true
},
"minimum": {
"type": "number",
"format": "double",
"nullable": true
},
"multipleOf": {
"type": "number",
"format": "double",
"nullable": true
},
"not": {
"anyOf": [
{
@ -127,18 +252,56 @@
},
"nullable": true
},
"pattern": {
"type": "string",
"nullable": true
},
"patternProperties": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Schema"
}
},
"properties": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Schema"
}
},
"propertyNames": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"required": {
"type": "array",
"items": {
"type": "string"
}
},
"then": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/components/schemas/Ref"
},
{
"$ref": "#/components/schemas/SchemaObject"
}
],
"nullable": true
},
"title": {
"type": "string",
"nullable": true
@ -156,6 +319,10 @@
}
],
"nullable": true
},
"uniqueItems": {
"type": "boolean",
"nullable": true
}
},
"additionalProperties": true

View file

@ -26,14 +26,14 @@
},
"Ref": {
"type": "object",
"required": [
"$ref"
],
"properties": {
"$ref": {
"type": "string"
}
},
"required": [
"$ref"
]
}
},
"Schema": {
"anyOf": [
@ -71,6 +71,26 @@
}
]
},
"additionalItems": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"additionalProperties": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"allOf": {
"anyOf": [
{
@ -97,6 +117,16 @@
}
]
},
"contains": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"definitions": {
"type": "object",
"additionalProperties": {
@ -113,6 +143,16 @@
}
]
},
"else": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"enum": {
"anyOf": [
{
@ -124,6 +164,28 @@
}
]
},
"exclusiveMaximum": {
"anyOf": [
{
"type": "number",
"format": "double"
},
{
"type": "null"
}
]
},
"exclusiveMinimum": {
"anyOf": [
{
"type": "number",
"format": "double"
},
{
"type": "null"
}
]
},
"format": {
"anyOf": [
{
@ -134,6 +196,16 @@
}
]
},
"if": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"items": {
"anyOf": [
{
@ -144,6 +216,105 @@
}
]
},
"maxItems": {
"anyOf": [
{
"type": "integer",
"format": "uint32"
},
{
"type": "null"
}
]
},
"maxLength": {
"anyOf": [
{
"type": "integer",
"format": "uint32"
},
{
"type": "null"
}
]
},
"maxProperties": {
"anyOf": [
{
"type": "integer",
"format": "uint32"
},
{
"type": "null"
}
]
},
"maximum": {
"anyOf": [
{
"type": "number",
"format": "double"
},
{
"type": "null"
}
]
},
"minItems": {
"anyOf": [
{
"type": "integer",
"format": "uint32"
},
{
"type": "null"
}
]
},
"minLength": {
"anyOf": [
{
"type": "integer",
"format": "uint32"
},
{
"type": "null"
}
]
},
"minProperties": {
"anyOf": [
{
"type": "integer",
"format": "uint32"
},
{
"type": "null"
}
]
},
"minimum": {
"anyOf": [
{
"type": "number",
"format": "double"
},
{
"type": "null"
}
]
},
"multipleOf": {
"anyOf": [
{
"type": "number",
"format": "double"
},
{
"type": "null"
}
]
},
"not": {
"anyOf": [
{
@ -167,18 +338,54 @@
}
]
},
"pattern": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"patternProperties": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Schema"
}
},
"properties": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/Schema"
}
},
"propertyNames": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"required": {
"type": "array",
"items": {
"type": "string"
}
},
"then": {
"anyOf": [
{
"$ref": "#/definitions/Schema"
},
{
"type": "null"
}
]
},
"title": {
"anyOf": [
{
@ -198,6 +405,16 @@
"type": "null"
}
]
},
"uniqueItems": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"additionalProperties": true

View file

@ -142,15 +142,18 @@ fn schema_for_external_tagged_enum<'a>(
let sub_schema = schema_for_untagged_enum_variant(variant, cattrs);
wrap_schema_fields(quote! {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
properties: {
let mut props = schemars::Map::new();
props.insert(#name.to_owned(), #sub_schema);
props
},
required: {
let mut required = schemars::Set::new();
required.insert(#name.to_owned());
required
object: schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
props.insert(#name.to_owned(), #sub_schema);
props
},
required: {
let mut required = schemars::Set::new();
required.insert(#name.to_owned());
required
},
..Default::default()
},
})
}));
@ -173,15 +176,18 @@ fn schema_for_internal_tagged_enum<'a>(
});
let schema = wrap_schema_fields(quote! {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
properties: {
let mut props = schemars::Map::new();
props.insert(#tag_name.to_owned(), #type_schema);
props
},
required: {
let mut required = schemars::Set::new();
required.insert(#tag_name.to_owned());
required
object: schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
props.insert(#tag_name.to_owned(), #type_schema);
props
},
required: {
let mut required = schemars::Set::new();
required.insert(#tag_name.to_owned());
required
},
..Default::default()
},
});
if is_unit_variant(&variant) {
@ -262,15 +268,18 @@ fn schema_for_struct(fields: &[Field], cattrs: &attr::Container) -> TokenStream
let schema = wrap_schema_fields(quote! {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
properties: {
let mut props = schemars::Map::new();
#(#recurse)*
props
},
required: {
let mut required = schemars::Set::new();
#(required.insert(#required.to_owned());)*
required
object: schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
#(#recurse)*
props
},
required: {
let mut required = schemars::Set::new();
#(required.insert(#required.to_owned());)*
required
},
..Default::default()
},
});
@ -300,10 +309,10 @@ fn get_json_schema_type(field: &Field) -> Box<dyn ToTokens> {
let se_with_segments = without_last_element(field.attrs.serialize_with(), "serialize");
if de_with_segments == se_with_segments {
if let Some(expr_path) = de_with_segments {
return Box::from(expr_path);
return Box::new(expr_path);
}
}
Box::from(field.ty.clone())
Box::new(field.ty.clone())
}
fn without_last_element(path: Option<&syn::ExprPath>, last: &str) -> Option<syn::ExprPath> {