Refactor flatten and move it to _private, remove TempFixupForTests, regenerate test schemas

This commit is contained in:
Graham Esau 2024-05-13 22:02:30 +01:00
parent 18300c67bb
commit c4d42ec11a
45 changed files with 280 additions and 330 deletions

View file

@ -3,6 +3,7 @@ use crate::JsonSchema;
use crate::Schema;
use serde::Serialize;
use serde_json::json;
use serde_json::map::Entry;
use serde_json::Map;
use serde_json::Value;
@ -174,3 +175,62 @@ pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {
f(inner_schema);
}
}
pub fn flatten(schema: &mut Schema, other: Schema) {
if let Value::Object(obj2) = other.to_value() {
let obj1 = schema.ensure_object();
for (key, value2) in obj2 {
match obj1.entry(key) {
Entry::Vacant(vacant) => {
vacant.insert(value2);
}
Entry::Occupied(mut occupied) => {
match occupied.key().as_str() {
// This special "type" handling can probably be removed once the enum variant `with`/`schema_with` behaviour is fixed
"type" => match (occupied.get_mut(), value2) {
(Value::Array(a1), Value::Array(mut a2)) => {
a2.retain(|v2| !a1.contains(v2));
a1.extend(a2);
}
(v1, Value::Array(mut a2)) => {
if !a2.contains(v1) {
a2.push(std::mem::take(v1));
*occupied.get_mut() = Value::Array(a2);
}
}
(Value::Array(a1), v2) => {
if !a1.contains(&v2) {
a1.push(v2);
}
}
(v1, v2) => {
if v1 != &v2 {
*occupied.get_mut() =
Value::Array(vec![std::mem::take(v1), v2]);
}
}
},
"required" => {
if let Value::Array(a1) = occupied.into_mut() {
if let Value::Array(a2) = value2 {
a1.extend(a2);
}
}
}
"properties" | "patternProperties" => {
if let Value::Object(o1) = occupied.into_mut() {
if let Value::Object(o2) = value2 {
o1.extend(o2);
}
}
}
_ => {
// leave the original value as it is (don't modify `schema`)
}
};
}
}
}
}
}

View file

@ -1,71 +0,0 @@
use serde_json::map::Entry;
use serde_json::Value;
use crate::Schema;
impl Schema {
/// This function is only public for use by schemars_derive.
///
/// It should not be considered part of the public API.
#[doc(hidden)]
pub fn flatten(mut self, other: Self) -> Schema {
if let Value::Object(obj2) = other.to_value() {
let obj1 = self.ensure_object();
for (key, value2) in obj2 {
match obj1.entry(key) {
Entry::Vacant(vacant) => {
vacant.insert(value2);
}
Entry::Occupied(mut occupied) => {
match occupied.key().as_str() {
// This special "type" handling can probably be removed once the enum variant `with`/`schema_with` behaviour is fixed
"type" => match (occupied.get_mut(), value2) {
(Value::Array(a1), Value::Array(mut a2)) => {
a2.retain(|v2| !a1.contains(v2));
a1.extend(a2);
}
(v1, Value::Array(mut a2)) => {
if !a2.contains(v1) {
a2.push(std::mem::take(v1));
*occupied.get_mut() = Value::Array(a2);
}
}
(Value::Array(a1), v2) => {
if !a1.contains(&v2) {
a1.push(v2);
}
}
(v1, v2) => {
if v1 != &v2 {
*occupied.get_mut() =
Value::Array(vec![std::mem::take(v1), v2]);
}
}
},
"required" => {
if let Value::Array(a1) = occupied.into_mut() {
if let Value::Array(a2) = value2 {
a1.extend(a2);
}
}
}
"properties" | "patternProperties" => {
if let Value::Object(o1) = occupied.into_mut() {
if let Value::Object(o2) = value2 {
o1.extend(o2);
}
}
}
_ => {
// leave the original value as it is (don't modify `self`)
}
};
}
}
}
}
self
}
}

View file

@ -1,7 +1,6 @@
#![deny(unsafe_code)]
#![doc = include_str!("../README.md")]
mod flatten;
mod json_schema_impls;
mod schema;
mod ser;

View file

@ -8,7 +8,7 @@
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
"minimum": 0
}
},
{
@ -16,7 +16,7 @@
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
"minimum": 0
}
}
],

View file

@ -33,10 +33,10 @@
}
},
"required": [
"weekday",
"date_time",
"naive_date",
"naive_date_time",
"naive_time",
"weekday"
"naive_time"
]
}

View file

@ -13,7 +13,7 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
}

View file

@ -30,8 +30,8 @@
}
},
"required": [
"deprecated_field",
"foo"
"foo",
"deprecated_field"
]
}
},

View file

@ -14,7 +14,7 @@
},
"deprecated": true,
"required": [
"deprecated_field",
"foo"
"foo",
"deprecated_field"
]
}

View file

@ -21,17 +21,17 @@
"nanos": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
"minimum": 0
},
"secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
"minimum": 0
}
},
"required": [
"nanos",
"secs"
"secs",
"nanos"
]
},
"SystemTime": {
@ -40,17 +40,17 @@
"nanos_since_epoch": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
"minimum": 0
},
"secs_since_epoch": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
"minimum": 0
}
},
"required": [
"nanos_since_epoch",
"secs_since_epoch"
"secs_since_epoch",
"nanos_since_epoch"
]
}
}

View file

@ -35,8 +35,8 @@
},
"additionalProperties": false,
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -54,8 +54,8 @@
},
"additionalProperties": false,
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -73,8 +73,8 @@
},
"additionalProperties": false,
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -93,8 +93,8 @@
},
"additionalProperties": false,
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"t": {
@ -106,8 +106,8 @@
},
"additionalProperties": false,
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -136,8 +136,8 @@
},
"additionalProperties": false,
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -171,8 +171,8 @@
},
"additionalProperties": false,
"required": [
"c",
"t"
"t",
"c"
]
}
],
@ -189,8 +189,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"UnitStruct": {

View file

@ -33,8 +33,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -51,8 +51,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -69,8 +69,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -88,8 +88,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"t": {
@ -100,8 +100,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -129,8 +129,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -162,8 +162,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
}
],
@ -180,8 +180,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"UnitStruct": {

View file

@ -64,8 +64,8 @@
},
"additionalProperties": false,
"required": [
"bar",
"foo"
"foo",
"bar"
]
}
},
@ -124,8 +124,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"UnitStruct": {

View file

@ -63,8 +63,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
}
},
@ -123,8 +123,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"UnitStruct": {

View file

@ -59,9 +59,9 @@
}
},
"required": [
"bar",
"typeProperty",
"foo",
"typeProperty"
"bar"
]
},
{
@ -81,9 +81,9 @@
},
"additionalProperties": false,
"required": [
"bar",
"typeProperty",
"foo",
"typeProperty"
"bar"
]
},
{

View file

@ -57,9 +57,9 @@
}
},
"required": [
"bar",
"typeProperty",
"foo",
"typeProperty"
"bar"
]
},
{
@ -78,9 +78,9 @@
}
},
"required": [
"bar",
"typeProperty",
"foo",
"typeProperty"
"bar"
]
},
{

View file

@ -30,8 +30,8 @@
},
"additionalProperties": false,
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
{
@ -66,8 +66,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"UnitStruct": {

View file

@ -29,8 +29,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
{
@ -65,8 +65,8 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
},
"UnitStruct": {

View file

@ -33,7 +33,7 @@
null
],
"required": [
"bar",
"foo"
"foo",
"bar"
]
}

View file

@ -26,8 +26,8 @@
}
},
"required": [
"b",
"f",
"b",
"s",
"v"
]

View file

@ -9,7 +9,7 @@
"replicas": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
"minimum": 0
}
},
"required": [

View file

@ -10,11 +10,11 @@
"x": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
"minimum": 0
}
},
"required": [
"v",
"x"
"x",
"v"
]
}

View file

@ -13,7 +13,7 @@
"nonzero_unsigned": {
"type": "integer",
"format": "uint32",
"minimum": 1.0
"minimum": 1
},
"signed": {
"type": "integer",
@ -22,13 +22,13 @@
"unsigned": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
"minimum": 0
}
},
"required": [
"nonzero_signed",
"unsigned",
"nonzero_unsigned",
"signed",
"unsigned"
"nonzero_signed"
]
}

View file

@ -11,8 +11,8 @@
}
},
"required": [
"borrowed",
"owned"
"owned",
"borrowed"
],
"definitions": {
"OsString": {
@ -25,7 +25,7 @@
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
"minimum": 0
}
}
},
@ -41,7 +41,7 @@
"items": {
"type": "integer",
"format": "uint16",
"minimum": 0.0
"minimum": 0
}
}
},

View file

@ -14,9 +14,9 @@
}
},
"required": [
"bound",
"range",
"inclusive",
"range"
"bound"
],
"definitions": {
"Bound_of_string": {
@ -62,8 +62,8 @@
}
},
"required": [
"end",
"start"
"start",
"end"
]
},
"Range_of_uint": {
@ -72,17 +72,17 @@
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
"minimum": 0
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
"minimum": 0
}
},
"required": [
"end",
"start"
"start",
"end"
]
}
}

View file

@ -47,8 +47,8 @@
}
},
"required": [
"nanos",
"secs"
"secs",
"nanos"
]
}
}

View file

@ -25,9 +25,9 @@
},
"required": [
"byte_or_bool2",
"fake_map",
"unit_or_t2",
"s",
"unit_or_t2"
"fake_map"
],
"definitions": {
"Or_for_null_and_int32": {
@ -46,7 +46,7 @@
{
"type": "integer",
"format": "uint8",
"minimum": 0.0
"minimum": 0
},
{
"type": "boolean"

View file

@ -24,11 +24,11 @@
}
},
"required": [
"inner",
"t",
"u",
"v",
"w"
"w",
"inner"
],
"definitions": {
"another-new-name": {

View file

@ -24,11 +24,11 @@
}
},
"required": [
"inner",
"t",
"u",
"v",
"w"
"w",
"inner"
],
"definitions": {
"MySimpleStruct": {

View file

@ -12,8 +12,8 @@
}
},
"required": [
"foo",
"generic"
"generic",
"foo"
],
"definitions": {
"MySimpleStruct": {
@ -52,11 +52,11 @@
}
},
"required": [
"inner",
"t",
"u",
"v",
"w"
"w",
"inner"
]
}
}

View file

@ -29,8 +29,8 @@
},
"required": [
"int",
"value",
"values"
"values",
"value"
],
"definitions": {
"Inner": {

View file

@ -24,8 +24,8 @@
},
"required": [
"int",
"value",
"values"
"values",
"value"
],
"definitions": {
"Inner": {

View file

@ -29,8 +29,8 @@
},
"required": [
"int",
"value",
"values"
"values",
"value"
],
"definitions": {
"Inner": {

View file

@ -24,8 +24,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -42,8 +42,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -71,8 +71,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
},
{
@ -89,8 +89,8 @@
}
},
"required": [
"c",
"t"
"t",
"c"
]
}
]

View file

@ -14,8 +14,8 @@
}
},
"required": [
"foo",
"typeProperty"
"typeProperty",
"foo"
]
},
{

View file

@ -15,8 +15,8 @@
}
},
"required": [
"foo",
"bar",
"baz",
"foo"
"baz"
]
}

View file

@ -18,7 +18,7 @@
}
},
"required": [
"included",
"writable"
"writable",
"included"
]
}

View file

@ -19,7 +19,7 @@
},
"additionalProperties": false,
"required": [
"bar",
"foo"
"foo",
"bar"
]
}

View file

@ -18,7 +18,7 @@
}
},
"required": [
"bar",
"foo"
"foo",
"bar"
]
}

View file

@ -31,14 +31,14 @@
"min_max": {
"type": "number",
"format": "float",
"maximum": 100.0,
"maximum": 100,
"minimum": 0.01
},
"min_max2": {
"type": "number",
"format": "float",
"maximum": 1000.0,
"minimum": 1.0
"maximum": 1000,
"minimum": 1
},
"non_empty_str": {
"type": "string",
@ -84,21 +84,21 @@
}
},
"required": [
"contains_str1",
"contains_str2",
"email_address",
"homepage",
"map_contains",
"min_max",
"min_max2",
"non_empty_str",
"non_empty_str2",
"pair",
"regex_str1",
"regex_str2",
"regex_str3",
"required_option",
"contains_str1",
"contains_str2",
"email_address",
"tel",
"homepage",
"non_empty_str",
"non_empty_str2",
"pair",
"map_contains",
"required_option",
"x"
]
}

View file

@ -25,8 +25,8 @@
"items": {
"type": "integer",
"format": "int32",
"maximum": 10.0,
"minimum": -10.0
"maximum": 10,
"minimum": -10
}
},
"vec_str_length": {
@ -65,10 +65,10 @@
"required": [
"array_str_length",
"slice_str_contains",
"vec_i32_range",
"vec_str_regex",
"vec_str_length",
"vec_str_length2",
"vec_str_regex",
"vec_str_url"
"vec_str_url",
"vec_i32_range"
]
}

View file

@ -3,6 +3,6 @@
"title": "NewType",
"type": "integer",
"format": "uint8",
"maximum": 10.0,
"minimum": 0.0
"maximum": 10,
"minimum": 0
}

View file

@ -31,14 +31,14 @@
"min_max": {
"type": "number",
"format": "float",
"maximum": 100.0,
"maximum": 100,
"minimum": 0.01
},
"min_max2": {
"type": "number",
"format": "float",
"maximum": 1000.0,
"minimum": 1.0
"maximum": 1000,
"minimum": 1
},
"non_empty_str": {
"type": "string",
@ -84,21 +84,21 @@
}
},
"required": [
"contains_str1",
"contains_str2",
"email_address",
"homepage",
"map_contains",
"min_max",
"min_max2",
"non_empty_str",
"non_empty_str2",
"pair",
"regex_str1",
"regex_str2",
"regex_str3",
"required_option",
"contains_str1",
"contains_str2",
"email_address",
"tel",
"homepage",
"non_empty_str",
"non_empty_str2",
"pair",
"map_contains",
"required_option",
"x"
]
}

View file

@ -6,8 +6,8 @@
{
"type": "integer",
"format": "uint8",
"maximum": 10.0,
"minimum": 0.0
"maximum": 10,
"minimum": 0
},
{
"type": "boolean"

View file

@ -1,5 +1,4 @@
use pretty_assertions::assert_eq;
use schemars::visit::Visitor;
use schemars::{gen::SchemaSettings, schema_for, JsonSchema, Schema};
use std::error::Error;
use std::fs;
@ -19,15 +18,6 @@ pub fn test_default_generated_schema<T: JsonSchema>(file: &str) -> TestResult {
}
pub fn test_schema(actual: &Schema, file: &str) -> TestResult {
// TEMP for easier comparison of schemas handling changes that don't actually affect a schema:
// - `required` ordering has changed
// - previously `f64` properties may now be integers
let actual = &{
let mut actual = actual.clone();
TempFixupForTests.visit_schema(&mut actual);
actual
};
let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) {
Ok(j) => j,
Err(e) => {
@ -50,25 +40,3 @@ fn write_actual_to_file(schema: &Schema, file: &str) -> TestResult {
fs::write(format!("tests/actual/{}.json", file), actual_json)?;
Ok(())
}
struct TempFixupForTests;
impl schemars::visit::Visitor for TempFixupForTests {
fn visit_schema(&mut self, schema: &mut Schema) {
schemars::visit::visit_schema(self, schema);
if let Some(object) = schema.as_object_mut() {
if let Some(serde_json::Value::Array(required)) = object.get_mut("required") {
required.sort_unstable_by(|a, b| a.as_str().cmp(&b.as_str()));
}
for (key, value) in object {
if key == "multipleOf" || key.ends_with("aximum") || key.ends_with("inimum") {
if let Some(f) = value.as_f64() {
*value = f.into();
}
}
}
}
}
}

View file

@ -434,73 +434,68 @@ fn expr_for_struct(
default: &SerdeDefault,
deny_unknown_fields: bool,
) -> TokenStream {
let (flattened_fields, property_fields): (Vec<_>, Vec<_>) = fields
.iter()
.filter(|f| !f.serde_attrs.skip_deserializing() || !f.serde_attrs.skip_serializing())
.partition(|f| f.serde_attrs.flatten());
let set_container_default = match default {
SerdeDefault::None => None,
SerdeDefault::Default => Some(quote!(let container_default = Self::default();)),
SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)),
};
let properties: Vec<_> = property_fields
.into_iter()
let properties: Vec<_> = fields
.iter()
.filter(|f| !f.serde_attrs.skip_deserializing() || !f.serde_attrs.skip_serializing())
.map(|field| {
let name = field.name();
let default = field_default_expr(field, set_container_default.is_some());
if field.serde_attrs.flatten() {
let (ty, type_def) = type_for_field_schema(field);
let (ty, type_def) = type_for_field_schema(field);
let required = field.validation_attrs.required();
let has_default = default.is_some();
let required = field.validation_attrs.required();
let args = quote!(gen, #required);
let mut schema_expr = quote_spanned! {ty.span()=>
schemars::_private::json_schema_for_flatten::<#ty>(#args)
};
let metadata = SchemaMetadata {
read_only: field.serde_attrs.skip_deserializing(),
write_only: field.serde_attrs.skip_serializing(),
default,
..field.attrs.as_metadata()
};
prepend_type_def(type_def, &mut schema_expr);
let gen = quote!(gen);
let mut schema_expr = if field.validation_attrs.required() {
quote_spanned! {ty.span()=>
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen)
quote! {
schemars::_private::flatten(&mut schema, #schema_expr);
}
} else {
quote_spanned! {ty.span()=>
#gen.subschema_for::<#ty>()
}
};
let name = field.name();
let default = field_default_expr(field, set_container_default.is_some());
metadata.apply_to_schema(&mut schema_expr);
field.validation_attrs.apply_to_schema(&mut schema_expr);
let (ty, type_def) = type_for_field_schema(field);
quote! {
{
#type_def
schemars::_private::insert_object_property::<#ty>(&mut schema, #name, #has_default, #required, #schema_expr);
}
}
})
.collect();
let has_default = default.is_some();
let required = field.validation_attrs.required();
let flattens: Vec<_> = flattened_fields
.into_iter()
.map(|field| {
let (ty, type_def) = type_for_field_schema(field);
let metadata = SchemaMetadata {
read_only: field.serde_attrs.skip_deserializing(),
write_only: field.serde_attrs.skip_serializing(),
default,
..field.attrs.as_metadata()
};
let required = field.validation_attrs.required();
let gen = quote!(gen);
let mut schema_expr = if field.validation_attrs.required() {
quote_spanned! {ty.span()=>
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen)
}
} else {
quote_spanned! {ty.span()=>
#gen.subschema_for::<#ty>()
}
};
let args = quote!(gen, #required);
let mut schema_expr = quote_spanned! {ty.span()=>
schemars::_private::json_schema_for_flatten::<#ty>(#args)
};
metadata.apply_to_schema(&mut schema_expr);
field.validation_attrs.apply_to_schema(&mut schema_expr);
prepend_type_def(type_def, &mut schema_expr);
schema_expr
})
quote! {
{
#type_def
schemars::_private::insert_object_property::<#ty>(&mut schema, #name, #has_default, #required, #schema_expr);
}
}}
})
.collect();
let set_additional_properties = if deny_unknown_fields {
@ -510,17 +505,16 @@ fn expr_for_struct(
} else {
TokenStream::new()
};
quote! {
{
#set_container_default
let mut schema = schemars::json_schema!({
"type": "object",
#set_additional_properties
});
#(#properties)*
schema #(.flatten(#flattens))*
}
}
quote! ({
#set_container_default
let mut schema = schemars::json_schema!({
"type": "object",
#set_additional_properties
});
#(#properties)*
schema
})
}
fn field_default_expr(field: &Field, container_has_default: bool) -> Option<TokenStream> {