From e4091bcfd8c5d3637e5d5bd74320c78181069fe0 Mon Sep 17 00:00:00 2001 From: Maaz Ahmed <maaz.a@subcom.tech> Date: Tue, 19 Mar 2024 08:55:09 +0000 Subject: [PATCH] fix: resolve all references correctly --- Cargo.lock | 93 +++++++++++------------ Cargo.toml | 3 +- src/lib.rs | 84 ++++++--------------- tests/basic.rs | 196 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 269 insertions(+), 107 deletions(-) create mode 100644 tests/basic.rs diff --git a/Cargo.lock b/Cargo.lock index 79e74ad..63963cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 36b35c2..a3668b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [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"]} diff --git a/src/lib.rs b/src/lib.rs index 5d777b8..30f8c28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)?; diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..3d74ba3 --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,196 @@ +#![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()); +} -- GitLab