Skip to content

Commit

Permalink
Implement JsonSchemaAs for KeyValueMap
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
swlynch99 committed Mar 8, 2024
1 parent 50febe0 commit a804257
Show file tree
Hide file tree
Showing 5 changed files with 524 additions and 3 deletions.
234 changes: 232 additions & 2 deletions serde_with/src/schemars_0_8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -570,6 +574,183 @@ where
}
}

impl<T, TA> WrapSchema<Vec<T>, KeyValueMap<TA>>
where
TA: JsonSchemaAs<T>,
{
/// Transform a schema from the entry type of a `KeyValueMap<T>` 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<String>,
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<T, TA> JsonSchemaAs<Vec<T>> for KeyValueMap<TA>
where
TA: JsonSchemaAs<T>,
{
fn schema_name() -> String {
std::format!("KeyValueMap<{}>", <WrapSchema<T, TA>>::schema_name())
}

fn schema_id() -> Cow<'static, str> {
std::format!(
"serde_with::KeyValueMap<{}>",
<WrapSchema<T, TA>>::schema_id()
)
.into()
}

fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut value = <WrapSchema<T, TA>>::json_schema(gen);
<WrapSchema<Vec<T>, KeyValueMap<TA>>>::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<K, V, KA, VA, const N: usize> JsonSchemaAs<[(K, V); N]> for Map<KA, VA>
where
VA: JsonSchemaAs<V>,
Expand Down Expand Up @@ -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<T, F: FnOnce(T)> {
value: ManuallyDrop<T>,
guard: Option<F>,
}

impl<T, F: FnOnce(T)> DropGuard<T, F> {
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<T, F: FnOnce(T)> Deref for DropGuard<T, F> {
type Target = T;

fn deref(&self) -> &Self::Target {
&self.value
}
}

impl<T, F: FnOnce(T)> DerefMut for DropGuard<T, F> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}

impl<T, F: FnOnce(T)> Drop for DropGuard<T, F> {
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);
}
}
}
Loading

0 comments on commit a804257

Please sign in to comment.