From a80425783f43c89d0dd612e5c4a65aa05871733d Mon Sep 17 00:00:00 2001 From: Sean Lynch <42618346+swlynch99@users.noreply.github.com> Date: Wed, 6 Dec 2023 23:21:14 -0800 Subject: [PATCH] Implement JsonSchemaAs for KeyValueMap This one is probably the most complicated of all the schema adaptors. We have to process the inner schemas until we find the one with the `$key$` field, remove that field, and then recreate it as an enum. --- serde_with/src/schemars_0_8.rs | 234 +++++++++++++++++- serde_with/tests/schemars_0_8.rs | 126 +++++++++- .../schemars_0_8/snapshots/key_value_map.json | 44 ++++ .../snapshots/key_value_map_enum.json | 57 +++++ .../snapshots/key_value_map_flatten.json | 66 +++++ 5 files changed, 524 insertions(+), 3 deletions(-) create mode 100644 serde_with/tests/schemars_0_8/snapshots/key_value_map.json create mode 100644 serde_with/tests/schemars_0_8/snapshots/key_value_map_enum.json create mode 100644 serde_with/tests/schemars_0_8/snapshots/key_value_map_flatten.json diff --git a/serde_with/src/schemars_0_8.rs b/serde_with/src/schemars_0_8.rs index 55bb4a36..a41daf92 100644 --- a/serde_with/src/schemars_0_8.rs +++ b/serde_with/src/schemars_0_8.rs @@ -12,11 +12,15 @@ use crate::{ use ::schemars_0_8::{ gen::SchemaGenerator, schema::{ - ArrayValidation, InstanceType, Metadata, NumberValidation, Schema, SchemaObject, - SubschemaValidation, + ArrayValidation, InstanceType, Metadata, NumberValidation, ObjectValidation, Schema, + SchemaObject, SingleOrVec, SubschemaValidation, }, JsonSchema, }; +use core::{ + mem::ManuallyDrop, + ops::{Deref, DerefMut}, +}; //=================================================================== // Trait Definition @@ -570,6 +574,183 @@ where } } +impl WrapSchema, KeyValueMap> +where + TA: JsonSchemaAs, +{ + /// Transform a schema from the entry type of a `KeyValueMap` to the + /// resulting field type. + /// + /// This usually means doing one of two things: + /// 1. removing the `$key$` property from an object, or, + /// 2. removing the first item from an array. + /// + /// We also need to adjust any fields that control the number of items or + /// properties allowed such as `(max|min)_properties` or `(max|min)_items`. + /// + /// This is mostly straightforward. Where things get hairy is when dealing + /// with subschemas. JSON schemas allow you to build the schema for an + /// object by combining multiple subschemas: + /// - You can match exactly one of a set of subschemas (`one_of`). + /// - You can match any of a set of subschemas (`any_of`). + /// - You can match all of a set of subschemas (`all_of`). + /// + /// Unfortunately for us, we need to handle all of these options by recursing + /// into the subschemas and applying the same transformations as above. + fn kvmap_transform_schema(gen: &mut SchemaGenerator, schema: &mut Schema) { + let mut parents = Vec::new(); + + Self::kvmap_transform_schema_impl(gen, schema, &mut parents, 0); + } + + fn kvmap_transform_schema_impl( + gen: &mut SchemaGenerator, + schema: &mut Schema, + parents: &mut Vec, + depth: u32, + ) { + if depth > 8 { + return; + } + + let mut done = false; + let schema = match schema { + Schema::Object(schema) => schema, + _ => return, + }; + + // The schema is a reference to a schema defined elsewhere. + // + // If possible we replace it with its definition but if that is not + // available then we give up and leave it as-is. + let mut parents = if let Some(reference) = &schema.reference { + let name = match reference.strip_prefix(&gen.settings().definitions_path) { + Some(name) => name, + // Reference is defined elsewhere, nothing we can do. + None => return, + }; + + // We are in a recursive reference loop. No point in continuing. + if parents.iter().any(|parent| parent == name) { + return; + } + + let name = name.to_owned(); + *schema = match gen.definitions().get(&name) { + Some(Schema::Object(schema)) => schema.clone(), + _ => return, + }; + + parents.push(name); + DropGuard::new(parents, |parents| drop(parents.pop())) + } else { + DropGuard::unguarded(parents) + }; + + if let Some(object) = &mut schema.object { + // For objects KeyValueMap uses the $key$ property so we need to remove it from + // the inner schema. + + done |= object.properties.remove("$key$").is_some(); + done |= object.required.remove("$key$"); + + if let Some(max) = &mut object.max_properties { + *max = max.saturating_sub(1); + } + + if let Some(min) = &mut object.max_properties { + *min = min.saturating_sub(1); + } + } + + if let Some(array) = &mut schema.array { + // For arrays KeyValueMap uses the first array element so we need to remove it + // from the inner schema. + + if let Some(SingleOrVec::Vec(items)) = &mut array.items { + // If the array is empty then the leading element may be following the + // additionalItem schema. In that case we do nothing. + if !items.is_empty() { + items.remove(0); + done = true; + } + } + + if let Some(max) = &mut array.max_items { + *max = max.saturating_sub(1); + } + + if let Some(min) = &mut array.min_items { + *min = min.saturating_sub(1); + } + } + + // We've already modified the schema so there's no need to do more work. + if done { + return; + } + + let subschemas = match &mut schema.subschemas { + Some(subschemas) => subschemas, + None => return, + }; + + if let Some(one_of) = &mut subschemas.one_of { + for subschema in one_of { + Self::kvmap_transform_schema_impl(gen, subschema, &mut parents, depth + 1); + } + } + + if let Some(any_of) = &mut subschemas.any_of { + for subschema in any_of { + Self::kvmap_transform_schema_impl(gen, subschema, &mut parents, depth + 1); + } + } + + if let Some(all_of) = &mut subschemas.all_of { + for subschema in all_of { + Self::kvmap_transform_schema_impl(gen, subschema, &mut parents, depth + 1); + } + } + } +} + +impl JsonSchemaAs> for KeyValueMap +where + TA: JsonSchemaAs, +{ + fn schema_name() -> String { + std::format!("KeyValueMap<{}>", >::schema_name()) + } + + fn schema_id() -> Cow<'static, str> { + std::format!( + "serde_with::KeyValueMap<{}>", + >::schema_id() + ) + .into() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let mut value = >::json_schema(gen); + , KeyValueMap>>::kvmap_transform_schema(gen, &mut value); + + SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + additional_properties: Some(Box::new(value)), + ..Default::default() + })), + ..Default::default() + } + .into() + } + + fn is_referenceable() -> bool { + true + } +} + impl JsonSchemaAs<[(K, V); N]> for Map where VA: JsonSchemaAs, @@ -895,3 +1076,52 @@ forward_duration_schema!(TimestampSecondsWithFrac); forward_duration_schema!(TimestampMilliSecondsWithFrac); forward_duration_schema!(TimestampMicroSecondsWithFrac); forward_duration_schema!(TimestampNanoSecondsWithFrac); + +//=================================================================== +// Extra internal helper structs + +struct DropGuard { + value: ManuallyDrop, + guard: Option, +} + +impl DropGuard { + pub fn new(value: T, guard: F) -> Self { + Self { + value: ManuallyDrop::new(value), + guard: Some(guard), + } + } + + pub fn unguarded(value: T) -> Self { + Self { + value: ManuallyDrop::new(value), + guard: None, + } + } +} + +impl Deref for DropGuard { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl DerefMut for DropGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +impl Drop for DropGuard { + fn drop(&mut self) { + // SAFETY: value is known to be initialized since we only ever remove it here. + let value = unsafe { ManuallyDrop::take(&mut self.value) }; + + if let Some(guard) = self.guard.take() { + guard(value); + } + } +} diff --git a/serde_with/tests/schemars_0_8.rs b/serde_with/tests/schemars_0_8.rs index 602c6324..5d0cfc59 100644 --- a/serde_with/tests/schemars_0_8.rs +++ b/serde_with/tests/schemars_0_8.rs @@ -1,6 +1,6 @@ use crate::utils::{check_matches_schema, check_valid_json_schema}; -use ::schemars_0_8::JsonSchema; use expect_test::expect_file; +use schemars::JsonSchema; use serde::Serialize; use serde_json::json; use serde_with::*; @@ -222,6 +222,33 @@ mod snapshots { C { c: i32, b: Option }, } + #[derive(JsonSchema, Serialize)] + struct KvMapData { + #[serde(rename = "$key$")] + key: String, + + a: u32, + b: String, + c: f32, + d: bool, + } + + #[allow(dead_code, variant_size_differences)] + #[derive(JsonSchema, Serialize)] + #[serde(tag = "$key$")] + enum KvMapEnum { + TypeA { a: u32 }, + TypeB { b: String }, + TypeC { c: bool }, + } + + #[derive(JsonSchema, Serialize)] + struct KvMapFlatten { + #[serde(flatten)] + data: KvMapEnum, + extra: bool, + } + declare_snapshot_test! { bytes { struct Test { @@ -304,6 +331,27 @@ mod snapshots { data: Vec, } } + + key_value_map { + struct Test { + #[serde_as(as = "KeyValueMap<_>")] + data: Vec, + } + } + + key_value_map_enum { + struct Test { + #[serde_as(as = "KeyValueMap<_>")] + data: Vec, + } + } + + key_value_map_flatten { + struct Test { + #[serde_as(as = "KeyValueMap<_>")] + data: Vec, + } + } } } @@ -733,3 +781,79 @@ fn test_set_prevent_duplicates_with_duplicates() { "set": [ 1, 1 ] })); } + +mod key_value_map { + use super::*; + use std::collections::BTreeMap; + + #[serde_as] + #[derive(Clone, Debug, JsonSchema, Serialize)] + #[serde(transparent)] + struct KVMap( + #[serde_as(as = "KeyValueMap<_>")] + #[serde(bound(serialize = "E: Serialize", deserialize = "E: Deserialize<'de>"))] + Vec, + ); + + #[derive(Clone, Debug, JsonSchema, Serialize)] + #[serde(untagged)] + enum UntaggedEnum { + A { + #[serde(rename = "$key$")] + key: String, + field1: String, + }, + B(String, i32), + } + + #[test] + fn test_untagged_enum() { + let value = KVMap(vec![ + UntaggedEnum::A { + key: "v1".into(), + field1: "field".into(), + }, + UntaggedEnum::B("v2".into(), 7), + ]); + + check_valid_json_schema(&value); + } + + #[derive(Clone, Debug, JsonSchema, Serialize)] + #[serde(untagged)] + enum UntaggedNestedEnum { + Nested(UntaggedEnum), + C { + #[serde(rename = "$key$")] + key: String, + field2: i32, + }, + } + + #[test] + fn test_untagged_nested_enum() { + let value = KVMap(vec![ + UntaggedNestedEnum::Nested(UntaggedEnum::A { + key: "v1".into(), + field1: "field".into(), + }), + UntaggedNestedEnum::Nested(UntaggedEnum::B("v2".into(), 7)), + UntaggedNestedEnum::C { + key: "v2".into(), + field2: 222, + }, + ]); + + check_valid_json_schema(&value); + } + + #[test] + fn test_btreemap() { + let value = KVMap(vec![ + BTreeMap::from_iter([("$key$", "a"), ("value", "b")]), + BTreeMap::from_iter([("$key$", "b"), ("value", "d")]), + ]); + + check_valid_json_schema(&value); + } +} diff --git a/serde_with/tests/schemars_0_8/snapshots/key_value_map.json b/serde_with/tests/schemars_0_8/snapshots/key_value_map.json new file mode 100644 index 00000000..e232adc5 --- /dev/null +++ b/serde_with/tests/schemars_0_8/snapshots/key_value_map.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Test", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "$ref": "#/definitions/KeyValueMap" + } + }, + "definitions": { + "KeyValueMap": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "a", + "b", + "c", + "d" + ], + "properties": { + "a": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "b": { + "type": "string" + }, + "c": { + "type": "number", + "format": "float" + }, + "d": { + "type": "boolean" + } + } + } + } + } +} diff --git a/serde_with/tests/schemars_0_8/snapshots/key_value_map_enum.json b/serde_with/tests/schemars_0_8/snapshots/key_value_map_enum.json new file mode 100644 index 00000000..d4a60070 --- /dev/null +++ b/serde_with/tests/schemars_0_8/snapshots/key_value_map_enum.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Test", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "$ref": "#/definitions/KeyValueMap" + } + }, + "definitions": { + "KeyValueMap": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + { + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "c" + ], + "properties": { + "c": { + "type": "boolean" + } + } + } + ] + } + } + } +} diff --git a/serde_with/tests/schemars_0_8/snapshots/key_value_map_flatten.json b/serde_with/tests/schemars_0_8/snapshots/key_value_map_flatten.json new file mode 100644 index 00000000..a94120d9 --- /dev/null +++ b/serde_with/tests/schemars_0_8/snapshots/key_value_map_flatten.json @@ -0,0 +1,66 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Test", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "$ref": "#/definitions/KeyValueMap" + } + }, + "definitions": { + "KeyValueMap": { + "type": "object", + "additionalProperties": { + "type": "object", + "oneOf": [ + { + "type": "object", + "required": [ + "a" + ], + "properties": { + "a": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + { + "type": "object", + "required": [ + "b" + ], + "properties": { + "b": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "c" + ], + "properties": { + "c": { + "type": "boolean" + } + } + } + ], + "required": [ + "extra" + ], + "properties": { + "extra": { + "type": "boolean" + } + } + } + } + } +}