Skip to content
Snippets Groups Projects
Commit e4091bcf authored by Maaz Ahmed's avatar Maaz Ahmed
Browse files

fix: resolve all references correctly

parent 97dc5814
No related branches found
No related tags found
1 merge request!2Fix incomplete reference resolution
......@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "ahash"
version = "0.8.7"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom",
......@@ -27,9 +27,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "autocfg"
......@@ -66,9 +66,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.14.0"
version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]]
name = "bytecount"
......@@ -157,9 +157,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.2"
version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [
"equivalent",
"hashbrown",
......@@ -183,9 +183,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.68"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
......@@ -242,9 +242,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.20"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
......@@ -422,9 +422,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [
"unicode-ident",
]
......@@ -461,9 +461,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
......@@ -478,15 +478,16 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "ryu"
version = "1.0.16"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "schema-police"
version = "0.1.0"
version = "0.1.2"
dependencies = [
"jsonschema",
"serde",
"serde_json",
"utoipa",
]
......@@ -499,29 +500,29 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.53",
]
[[package]]
name = "serde_json"
version = "1.0.113"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
......@@ -546,9 +547,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.48"
version = "2.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
dependencies = [
"proc-macro2",
"quote",
......@@ -614,9 +615,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
......@@ -653,14 +654,14 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.53",
]
[[package]]
name = "uuid"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
[[package]]
name = "version_check"
......@@ -676,9 +677,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
......@@ -686,24 +687,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.53",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
......@@ -711,22 +712,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.53",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.91"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "windows-targets"
......@@ -802,5 +803,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.53",
]
[package]
name = "schema-police"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
......@@ -11,3 +11,4 @@ serde_json = "1.0.113"
[dev-dependencies]
utoipa = "4.2.0"
serde = { version = "*", features = ["derive"]}
......@@ -18,11 +18,7 @@ pub struct SchemaInspector {
}
impl SchemaInspector {
const PROP: &'static str = "properties";
const REF: &'static str = "$ref";
const AOF: &'static str = "allOf";
const OOF: &'static str = "oneOf";
const ITM: &'static str = "items";
/// Initialize the `SchemaInspector` using the name of the type provided to the function using the turbofish syntax instead of using a string
///
......@@ -65,29 +61,17 @@ impl SchemaInspector {
return Err(InitError::InvalidSchema(None));
};
// extract target from schema
let Value::Object(mut target) = schemas
let mut target = schemas
.get(target)
.ok_or_else(|| SchemaNotFound(target.to_owned()))?
.to_owned()
else {
return Err(InitError::InvalidSchema(None));
};
.to_owned();
if resolve_refs {
// extract properties
let Value::Object(mut props) = target
.remove(Self::PROP)
.ok_or_else(|| InvalidSchema(Some(Self::PROP.to_owned())))?
else {
return Err(InvalidSchema(Some(Self::PROP.to_owned())));
};
Self::resolve_references(schemas, &mut props)?;
// reinsert resolved properties
target.insert(Self::PROP.into(), Value::Object(props));
Self::resolve_references(schemas, &mut target)?;
}
Ok(Self {
schema: JSONSchema::options()
.with_draft(Draft::Draft202012)
.compile(&Value::Object(target))
.compile(&target)
.map_err(|_| CompileFailure)?,
})
}
......@@ -95,52 +79,32 @@ impl SchemaInspector {
/// Replace internal '$ref' references with the correct schemas
/// The current implementation disregards the paths in the references and only uses the schema names
/// to retrieve them from the root schemas object
fn resolve_references(
schemas: &Map<String, Value>,
props: &mut Map<String, Value>,
) -> Result<(), InitError> {
let to_resolve: Vec<(String, String)> = props
.iter()
.filter_map(|(key, val)| {
Self::extract_ref(val)
.or_else(|| {
// Get the first item of allOf array and get the "$ref" field from that object
val.get(Self::AOF)
.and_then(|aof| aof.get(0).and_then(Self::extract_ref))
})
.map(|schema_name| (key.to_owned(), schema_name.to_owned()))
})
.collect();
// replace the references in props fields with the correct schemas
for (field, schema) in &to_resolve {
let inner = props.get_mut(field).expect("known field cannot fail");
*inner = schemas
.get(schema)
.ok_or_else(|| InitError::ResolutionFailure(schema.to_owned()))?
.to_owned();
fn resolve_references(schemas: &Map<String, Value>, val: &mut Value) -> Result<(), InitError> {
if let Some(target) = Self::extract_ref(val) {
*val = schemas
.get(&target)
.ok_or(InitError::ResolutionFailure(format!(
"{target} not found in the given schemas"
)))?
.clone();
}
props
.values_mut()
.try_for_each(|obj| Self::resolve_nested(schemas, obj))
}
fn resolve_nested(schemas: &Map<String, Value>, val: &mut Value) -> Result<(), InitError> {
if let Some(Value::Object(inner_props)) = val.get_mut(Self::PROP) {
Self::resolve_references(schemas, inner_props)?;
} else if val.get(Self::ITM).is_some() {
// Rewrite - manually insert schema instead of calling resolve_references ?
if let Value::Object(false_props) = val {
Self::resolve_references(schemas, false_props)?;
match val {
Value::Object(ref mut map) => {
for v in map.values_mut() {
Self::resolve_references(schemas, v)?;
}
}
Value::Array(ref mut arr) => {
for v in arr.iter_mut() {
Self::resolve_references(schemas, v)?;
}
}
} else if let Some(Value::Array(enum_arr)) = val.get_mut(Self::OOF) {
return enum_arr
.iter_mut()
.try_for_each(|obj| Self::resolve_nested(schemas, obj));
_ => (),
}
Ok(())
}
/// Extract the fragment and pack it with the given field name
/// Extract the fragment and get the target field name
#[inline]
fn extract_ref(val: &Value) -> Option<String> {
let refr = val.get(Self::REF)?;
......
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use utoipa::{OpenApi, ToSchema};
#[derive(OpenApi)]
#[openapi(components(schemas(
RegularSchema,
NestedSchema,
InnerSchema,
InnerInnerSchema,
Options,
SerdeSchema,
EnumType,
)))]
struct ApiDocs;
#[derive(ToSchema)]
struct RegularSchema {
#[schema(min_length = 1, max_length = 5)]
field: String,
#[schema(minimum = 5, maximum = 10)]
field2: i64,
field3: bool,
field4: Vec<String>,
}
#[derive(ToSchema)]
struct NestedSchema {
#[schema(min_length = 1, max_length = 5)]
field: String,
nested: InnerSchema,
}
#[derive(ToSchema)]
struct InnerSchema {
field: bool,
#[schema(minimum = 5, maximum = 10)]
field2: i32,
nested: InnerInnerSchema,
}
#[derive(ToSchema)]
struct InnerInnerSchema {
#[schema(pattern = "^test$")]
field: String,
field2: Options,
}
#[derive(ToSchema)]
enum Options {
One,
Two,
Three,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct SerdeSchema {
#[serde(flatten)]
flat: EnumType,
#[schema(min_length = 1)]
regular: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[serde(tag = "tag")]
#[serde(rename_all = "lowercase")]
pub enum EnumType {
First { field: String },
Second { field: String },
None,
}
fn get_schemas() -> &'static serde_json::Value {
static SCHEMAS: OnceLock<serde_json::Value> = OnceLock::new();
SCHEMAS.get_or_init(|| {
serde_json::to_value(
ApiDocs::openapi()
.components
.expect("no components found")
.schemas,
)
.unwrap()
})
}
#[test]
fn init_validator_regular() {
let schemas = get_schemas();
schema_police::SchemaInspector::new_infer::<RegularSchema>(schemas, false).unwrap();
}
#[test]
fn init_validator_nested() {
let schemas = get_schemas();
schema_police::SchemaInspector::new_infer::<NestedSchema>(schemas, true).unwrap();
}
#[test]
fn init_validator_serde() {
let schemas = get_schemas();
schema_police::SchemaInspector::new_infer::<SerdeSchema>(schemas, true).unwrap();
}
#[test]
fn validate_regular_schema_ok() {
let schemas = get_schemas();
let validator =
schema_police::SchemaInspector::new_infer::<RegularSchema>(schemas, false).unwrap();
let result = validator.inspect(&serde_json::json!({
"field": "field",
"field2": 8,
"field3": true,
"field4": [""],
}));
assert_eq!(result, Ok(()));
}
#[test]
fn validate_regular_schema_err() {
let schemas = get_schemas();
let validator =
schema_police::SchemaInspector::new_infer::<RegularSchema>(schemas, false).unwrap();
let result = validator.inspect(&serde_json::json!({
"field": "eld",
"field2": 0,
"field3": true,
"field4": [""]
}));
assert!(result.is_err());
}
#[test]
fn validate_nested_schema_ok() {
let schemas = get_schemas();
let validator =
schema_police::SchemaInspector::new_infer::<NestedSchema>(schemas, true).unwrap();
let result = validator.inspect(&serde_json::json!({
"field": "field",
"nested": {
"field": true,
"field2": 6,
"nested": {
"field": "test",
"field2": "One"
}
}
}));
assert_eq!(result, Ok(()));
}
#[test]
fn validate_nested_schema_err() {
let schemas = get_schemas();
let validator =
schema_police::SchemaInspector::new_infer::<NestedSchema>(schemas, true).unwrap();
let result = validator.inspect(&serde_json::json!({
"field": "field",
"nested": {
"field": true,
"field2": 6,
"nested": {
"field": "",
"field2": "test"
}
}
}));
assert!(result.is_err())
}
#[test]
fn validate_serde_schema_ok() {
let schemas = get_schemas();
let validator =
schema_police::SchemaInspector::new_infer::<SerdeSchema>(schemas, true).unwrap();
let result = validator.inspect(&serde_json::json!({
"regular": "field",
"tag": "first",
"field": ""
}));
assert_eq!(result, Ok(()));
}
#[test]
fn validate_serde_schema_err() {
let schemas = get_schemas();
let validator =
schema_police::SchemaInspector::new_infer::<SerdeSchema>(schemas, true).unwrap();
let result = validator.inspect(&serde_json::json!({
"regular": "field",
"tag": "huh",
"field": ""
}));
assert!(result.is_err());
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment