From 1a5e94dd86d0039c2bd78213326db6509e77131a Mon Sep 17 00:00:00 2001 From: Maaz Ahmed <maaz.a@subcom.tech> Date: Mon, 12 Feb 2024 15:18:56 +0530 Subject: [PATCH] chore(docs): update documentation --- Cargo.toml | 2 ++ README.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 README.md diff --git a/Cargo.toml b/Cargo.toml index dae8f76..a8db2bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,6 @@ edition = "2021" [dependencies] jsonschema = {version = "0.17.1", default-features = false, features = ["draft202012"]} serde_json = "1.0.113" + +[dev-dependencies] utoipa = "4.2.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..253ee60 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Schema Police +This library provides the [`SchemaInspector`] type which simplifies building a JSON validator from the schemas present in the OpenAPI spec (tested on OpenAPI spec 3.0.3 wich uses the Json Schema draft 202012). + +The primary goal of this libary is to provide the necessary glue to make it possible to use the OpenAPI spec generated by the [utoipa](https://crates.io/crates/utoipa) crate with the [jsonschema](https://crates.io/crates/jsonschema) schema validation library. However, this approach may become unnessary if local references start to get resolved correctly in the `jsonschema` crate (which currently results in a `invalid reference` error). + +## Why the name `Schema Police`? +It's a reference to the Radiohead song 'Karma Police' + +## Usage Example +```rust +use schema_police::SchemaInspector; +use utoipa::OpenApi; + +fn main() { + // Get the schemas from OpenAPI spec generated by utoipa + let schemas = serde_json::to_value(ApiDoc::openapi().components.unwrap().schemas).unwrap(); + // Initialize JSON inspector for the `ExampleSchema` type + let inspector = SchemaInspector::new_infer::<ExampleSchema>(&schemas, false).unwrap(); + // Example JSON request that contains errors + let example_request = serde_json::json!({ + "integer": 10, + "string": "hello", + "array": ["one"] + }); + + let Err(result) = inspector.inspect(&example_request) else { + panic!("This should be an error!"); + }; + + assert_eq!( + result.to_string(), + r#"JSON doesn't match expected schema: ["one"] has less than 2 items, 10 is less than the minimum of 15"# + ); +} + +#[derive(utoipa::OpenApi)] +#[openapi(components(schemas(ExampleSchema)))] +struct ApiDoc; + +#[allow(unused)] +#[derive(utoipa::ToSchema)] +struct ExampleSchema { + #[schema(minimum = 15, maximum = 90)] + integer: i32, + #[schema(pattern = "^hello.*")] + string: String, + #[schema(min_items = 2)] + array: Vec<String>, +} +``` diff --git a/src/lib.rs b/src/lib.rs index ad29848..cde9123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,48 @@ +#[doc = include_str!("../README.md")] use jsonschema::{Draft, JSONSchema}; use serde_json::{Map, Value}; use std::fmt::Display; -pub struct Inspector { - schema: JSONSchema, +/// The JSON schema inspector (validator) +/// +/// This contains the necessary glue to make all the schemas from the OpenAPI spec (tested on 3.0.3) work with the `jsonschema` crate. +/// This is achieved by simply resolving the local references present in the schemas with the actual schemas. +/// +/// It is recommended to initialize one `SchemaInspector` for one schema only once, and use the same instance to validate/inspect +/// multiple JSON objects to avoid performance overhead of compiling the schema multiple times. +/// +/// Note: Resolving references is optional and is not required for schemas that do not contain any references to nested schemas. +#[derive(Debug)] +pub struct SchemaInspector { + schema: JSONSchema, // the compiled schema used to validate JSON } -impl Inspector { +impl SchemaInspector { const PROP: &'static str = "properties"; const REF: &'static str = "$ref"; const AOF: &'static str = "allOf"; - /// Initialize an Inspector using the name of the type provided to the function using the turbofish syntax + /// Initialize the `SchemaInspector` using the name of the type provided to the function using the turbofish syntax instead of using a string + /// + /// Note: Resolving references is optional and is not required for schemas that do not contain any references to nested schemas. + /// It is recommended to set `resolve_refs` to false when there are no references present in the schema to avoid extra overhead. + /// + /// ## Example + /// ``` + /// use schema_police::{SchemaInspector, InitError}; + /// + /// fn construct_inspector(schema: &serde_json::Value) -> Result<SchemaInspector, InitError> { + /// SchemaInspector::new_infer::<SchemaType>(schema, true) + /// } + /// + /// #[derive(utoipa::ToSchema)] + /// struct SchemaType { + /// field: String, + /// } + /// ``` + /// + /// Note: the function internally uses [`std::any::type_name`] to get the name of the type. Accodring to the std lib docs, the function + /// is meant to be used for debugging and makes no guarantees. Therefore, it is not recommended to rely on this method in mission critical contexts. pub fn new_infer<T>(schemas: &Value, resolve_refs: bool) -> Result<Self, InitError> { // get target name from type inference let target = std::any::type_name::<T>() @@ -21,7 +52,10 @@ impl Inspector { Self::new(target, schemas, resolve_refs) } - /// Construct an instance of a schema `Inspector` using the provided OpenAPI spec's schemas (in the form of `serde_json::Value`) + /// Construct an instance of the `SchemaInspector` using the provided OpenAPI spec's schemas (in the form of `serde_json::Value`) + /// + /// Note: Resolving references is optional and is not required for schemas that do not contain any references to nested schemas. + /// It is recommended to set `resolve_refs` to false when there are no references present in the schema to avoid extra overhead. pub fn new(target: &str, schemas: &Value, resolve_refs: bool) -> Result<Self, InitError> { use InitError::*; // extract schemas object @@ -56,7 +90,9 @@ impl Inspector { }) } - // Replace internal '$ref' references with the correct schemas + /// 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>, @@ -87,7 +123,7 @@ impl Inspector { Ok(()) } - // Extract the fragment and pack it with the given field name + /// Extract the fragment and pack it with the given field name #[inline] fn extract_ref(val: &Value) -> Option<String> { let refr = val.get(Self::REF)?; @@ -115,6 +151,10 @@ impl Inspector { } } +/// The error that is returned by the [`SchemaInspector`] when the provided JSON to the `inspect` function +/// fails to conform to the schema the inspector was compiled with. +/// +/// The error type internally holds a string containing detailed information about what part of the JSON differs from the schema #[derive(Debug)] pub struct SchemaError { cause: String, @@ -128,6 +168,7 @@ impl Display for SchemaError { } } +/// All the possible errors that can occur while initializing [`SchemaInspector`] #[derive(Debug)] pub enum InitError { SchemaNotFound(String), -- GitLab