From 6a6166f3c8c279c27ffe4c41ec32e884274bc032 Mon Sep 17 00:00:00 2001 From: cakemanny Date: Mon, 17 Feb 2025 09:04:24 +0100 Subject: [PATCH 1/3] feat: support custom ids and $defs from 2020-12 --- src/diff_walker.rs | 22 +++---- src/lib.rs | 1 + src/resolver.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 src/resolver.rs diff --git a/src/diff_walker.rs b/src/diff_walker.rs index ca67eb3..c887d8e 100644 --- a/src/diff_walker.rs +++ b/src/diff_walker.rs @@ -7,20 +7,27 @@ use schemars::schema::{ }; use serde_json::Value; +use crate::resolver::Resolver; use crate::{Change, ChangeKind, Error, JsonSchemaType, Range}; pub struct DiffWalker { pub cb: F, pub lhs_root: RootSchema, pub rhs_root: RootSchema, + lhs_resolver: Resolver, + rhs_resolver: Resolver, } impl DiffWalker { pub fn new(cb: F, lhs_root: RootSchema, rhs_root: RootSchema) -> Self { + let lhs_resolver = Resolver::for_schema(&lhs_root); + let rhs_resolver = Resolver::for_schema(&rhs_root); Self { cb, lhs_root, rhs_root, + lhs_resolver, + rhs_resolver, } } @@ -349,28 +356,19 @@ impl DiffWalker { Ok(()) } - fn resolve_ref<'a>(root_schema: &'a RootSchema, reference: &str) -> Option<&'a Schema> { - if let Some(definition_name) = reference.strip_prefix("#/definitions/") { - let schema_object = root_schema.definitions.get(definition_name)?; - Some(schema_object) - } else { - None - } - } - fn resolve_references( - &mut self, + &self, lhs: &mut SchemaObject, rhs: &mut SchemaObject, ) -> Result<(), Error> { if let Some(ref reference) = lhs.reference { - if let Some(lhs_inner) = Self::resolve_ref(&self.lhs_root, reference) { + if let Some(lhs_inner) = self.lhs_resolver.resolve(&self.lhs_root, reference) { *lhs = lhs_inner.clone().into_object(); } } if let Some(ref reference) = rhs.reference { - if let Some(rhs_inner) = Self::resolve_ref(&self.rhs_root, reference) { + if let Some(rhs_inner) = self.rhs_resolver.resolve(&self.rhs_root, reference) { *rhs = rhs_inner.clone().into_object(); } } diff --git a/src/lib.rs b/src/lib.rs index 4239f0b..85d2986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use serde_json::Value; use thiserror::Error; mod diff_walker; +mod resolver; mod types; pub use types::*; diff --git a/src/resolver.rs b/src/resolver.rs new file mode 100644 index 0000000..0bf495f --- /dev/null +++ b/src/resolver.rs @@ -0,0 +1,147 @@ +use std::collections::BTreeMap; + +use schemars::schema::{RootSchema, Schema, SchemaObject}; + +pub struct Resolver { + ref_lookup: BTreeMap, +} + +impl Resolver { + pub fn for_schema(root: &RootSchema) -> Self { + let mut ref_lookup = BTreeMap::new(); + + for (key, schema) in &root.definitions { + if let Some(id) = schema.get_schema_id() { + ref_lookup.insert(id.to_owned(), key.clone()); + } + + if let Some(root_id) = root.schema.get_schema_id() { + ref_lookup.insert(format!("{root_id}#/definitions/{key}"), key.clone()); + ref_lookup.insert(format!("{root_id}#/$defs/{key}"), key.clone()); + } + + ref_lookup.insert(format!("#/definitions/{key}"), key.clone()); + ref_lookup.insert(format!("#/$defs/{key}"), key.clone()); + } + + Self { ref_lookup } + } + + /// Resolves a reference. + /// + /// `root` must be the same schema that was used to construct the resolver. + /// This is not checked. + pub fn resolve<'a>(&self, root: &'a RootSchema, reference: &str) -> Option<&'a Schema> { + let key = self.ref_lookup.get(reference)?; + root.definitions.get(key) + } +} + +trait MayHaveSchemaId { + fn get_schema_id(&self) -> Option<&str>; +} + +impl MayHaveSchemaId for SchemaObject { + fn get_schema_id(&self) -> Option<&str> { + self.metadata + .as_ref() + .and_then(|m| m.id.as_ref()) + .map(|id| id.as_str()) + } +} + +impl MayHaveSchemaId for Schema { + fn get_schema_id(&self) -> Option<&str> { + match self { + Schema::Object(schema_obj) => schema_obj.get_schema_id(), + Schema::Bool(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn draft7_definitions() { + let root: RootSchema = serde_json::from_str( + r#"{ + "definitions": { + "A": {} + } + }"#, + ) + .unwrap(); + let resolver = Resolver::for_schema(&root); + + let resolved = resolver.resolve(&root, "#/definitions/A"); + assert!(resolved.is_some()); + + let resolved = resolver.resolve(&root, "#/definitions/not-there"); + assert!(resolved.is_none()); + } + + #[test] + fn draft7_root_has_id() { + let root: RootSchema = serde_json::from_str( + r#"{ + "$id": "urn:uuid:e773a2e8-d746-4dc6-9480-0bba5ff33504", + "definitions": { + "A": {} + } + }"#, + ) + .unwrap(); + let resolver = Resolver::for_schema(&root); + + let resolved = resolver.resolve(&root, "#/definitions/A"); + assert!(resolved.is_some()); + let resolved = resolver.resolve( + &root, + "urn:uuid:e773a2e8-d746-4dc6-9480-0bba5ff33504#/definitions/A", + ); + assert!(resolved.is_some()); + } + + #[test] + fn draft7_definition_has_id() { + let root: RootSchema = serde_json::from_str( + r#"{ + "definitions": { + "A": { + "$id": "some-id" + } + } + }"#, + ) + .unwrap(); + let resolver = Resolver::for_schema(&root); + + let resolved = resolver.resolve(&root, "some-id"); + assert!(resolved.is_some()); + assert_eq!(resolved, resolver.resolve(&root, "#/definitions/A")) + } + + #[test] + fn draft2020_12_defs() { + let root: RootSchema = serde_json::from_str( + r#"{ + "$defs": { + "A": { + "$id": "some-id" + } + } + }"#, + ) + .unwrap(); + let resolver = Resolver::for_schema(&root); + + let resolved = resolver.resolve(&root, "#/$defs/A"); + assert!(resolved.is_some()); + assert_eq!(resolved, resolver.resolve(&root, "some-id")); + + let resolved = resolver.resolve(&root, "#/$defs/not-there"); + assert!(resolved.is_none()); + } +} From 901afeb24024570bd4c05b09c1a438c1a7cb0c72 Mon Sep 17 00:00:00 2001 From: cakemanny Date: Sun, 2 Mar 2025 17:24:47 +0100 Subject: [PATCH 2/3] chore: fix clippy lint: unnecessary_map_or https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_map_or --- src/diff_walker.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/diff_walker.rs b/src/diff_walker.rs index c887d8e..d2cb107 100644 --- a/src/diff_walker.rs +++ b/src/diff_walker.rs @@ -149,8 +149,7 @@ impl DiffWalker { let lhs_additional_properties = lhs .object() .additional_properties - .as_ref() - .map_or(true, |x| x.clone().into_object().is_true()); + .as_ref().is_none_or(|x| x.clone().into_object().is_true()); for removed in lhs_props.difference(&rhs_props) { (self.cb)(Change { @@ -537,8 +536,7 @@ impl JsonSchemaExt for SchemaObject { } else if self .subschemas() .not - .as_ref() - .map_or(false, |x| x.clone().into_object().is_true()) + .as_ref().is_some_and(|x| x.clone().into_object().is_true()) { InternalJsonSchemaType::Never } else { From d49699feec35658203c3c4e4f0e06fca46ddf258 Mon Sep 17 00:00:00 2001 From: cakemanny Date: Mon, 3 Mar 2025 14:03:38 +0100 Subject: [PATCH 3/3] chore: fix formatting after clippy lint fix --- src/diff_walker.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/diff_walker.rs b/src/diff_walker.rs index d2cb107..37d755f 100644 --- a/src/diff_walker.rs +++ b/src/diff_walker.rs @@ -149,7 +149,8 @@ impl DiffWalker { let lhs_additional_properties = lhs .object() .additional_properties - .as_ref().is_none_or(|x| x.clone().into_object().is_true()); + .as_ref() + .is_none_or(|x| x.clone().into_object().is_true()); for removed in lhs_props.difference(&rhs_props) { (self.cb)(Change { @@ -536,7 +537,8 @@ impl JsonSchemaExt for SchemaObject { } else if self .subschemas() .not - .as_ref().is_some_and(|x| x.clone().into_object().is_true()) + .as_ref() + .is_some_and(|x| x.clone().into_object().is_true()) { InternalJsonSchemaType::Never } else {