diff --git a/Cargo.toml b/Cargo.toml index 763cfef4844189bac64b8d9d32cbb57879f6d9f1..00d916515e7b73dc731aaee5c6e3b29d49e53657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +serde = ["dep:serde"] + [dependencies] jsonschema = {version = "0.17.1", default-features = false, features = ["draft202012"]} serde_json = "1.0.113" +serde = { version = "*", features = ["derive"], optional = true} + [dev-dependencies] utoipa = "4.2.0" diff --git a/README.md b/README.md index 253ee60cb7d3cb72fdca2e105abc3ab6cde49dad..691f743db69e1f52b7676faf0ec04cf1e5207d60 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,12 @@ fn main() { panic!("This should be an error!"); }; + let context = result.context(); + assert_eq!(context[0].description, r#"["one"] has less than 2 items"#); + assert_eq!(context[1].description, "10 is less than the minimum of 15"); 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"# + r#"JSON doesn't match expected schema: found 2 errors"# ); } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..65859b0d223c4ede4d9df4402b21811899a756b6 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,74 @@ +use std::fmt::Display; + +/// 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 context containing detailed information about what part of the JSON differs from the schema +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SchemaError { + pub(crate) context: Vec<ErrContext>, +} + +/// The interal type containing metadata where the error has occurred along with why +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct ErrContext { + /// the field where the validation failed + pub field: String, + /// detailed description of why the validation failed + pub description: String, +} + +impl SchemaError { + /// Get the context of the error + pub fn context(&self) -> &[ErrContext] { + &self.context + } +} + +impl std::error::Error for SchemaError {} + +impl Display for SchemaError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let num_errors = self.context.len(); + let noun = if num_errors < 2 { "error" } else { "errors" }; + write!( + f, + "JSON doesn't match expected schema: found {num_errors} {noun}", + ) + } +} + +/// All the possible errors that can occur while initializing [`SchemaInspector`] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InitError { + SchemaNotFound(String), + ResolutionFailure(String), // contains the reference that was not resolved + InvalidSchema(Option<String>), // contains the missing field in the schema + CompileFailure, + InferenceFailure, +} + +impl std::error::Error for InitError {} + +impl Display for InitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let base = "failed to initialize Inspector:"; + match self { + Self::SchemaNotFound(schema) => { + write!(f, "{base} {schema} was not found in the provided schemas") + } + Self::ResolutionFailure(schema) => write!( + f, + "{base} failed to resolve references: referenced schema {schema} was not found" + ), + Self::InferenceFailure => write!(f, "{base} name of the type couldn't be inferred"), + Self::CompileFailure => write!(f, "{base} failed to compile schema"), + Self::InvalidSchema(schema) => write!( + f, + "{base} invalid schema provided: {} is missing or is not an object", + schema.as_deref().unwrap_or("field") + ), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index ed249908bfba1de226ffd6a3519e3ae27155bbc7..dddb9eb545a4573cc40a8ed0cfc9cd028a779ae7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,10 @@ #[doc = include_str!("../README.md")] use jsonschema::{Draft, JSONSchema}; use serde_json::{Map, Value}; -use std::fmt::Display; + +pub mod error; + +pub use error::*; /// The JSON schema inspector (validator) /// @@ -123,65 +126,14 @@ impl SchemaInspector { /// that is inconsistent with the compiled schema. pub fn inspect(&self, json: &Value) -> Result<(), SchemaError> { self.schema.validate(json).map_err(|err| { - let mut cause = err.fold(String::new(), |mut acc, err| { - acc.push_str(&err.to_string()); - acc.push_str(", "); - acc - }); - cause.pop(); - cause.pop(); - SchemaError { cause } - }) - } -} - -/// 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, Clone, PartialEq, Eq)] -pub struct SchemaError { - cause: String, -} - -impl std::error::Error for SchemaError {} + let context = err + .map(|err| ErrContext { + field: err.instance_path.to_string(), + description: err.to_string(), + }) + .collect(); -impl Display for SchemaError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "JSON doesn't match expected schema: {}", self.cause) - } -} - -/// All the possible errors that can occur while initializing [`SchemaInspector`] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum InitError { - SchemaNotFound(String), - ResolutionFailure(String), // contains the reference that was not resolved - InvalidSchema(Option<String>), // contains the missing field in the schema - CompileFailure, - InferenceFailure, -} - -impl std::error::Error for InitError {} - -impl Display for InitError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let base = "failed to initialize Inspector:"; - match self { - Self::SchemaNotFound(schema) => { - write!(f, "{base} {schema} was not found in the provided schemas") - } - Self::ResolutionFailure(schema) => write!( - f, - "{base} failed to resolve references: referenced schema {schema} was not found" - ), - Self::InferenceFailure => write!(f, "{base} name of the type couldn't be inferred"), - Self::CompileFailure => write!(f, "{base} failed to compile schema"), - Self::InvalidSchema(schema) => write!( - f, - "{base} invalid schema provided: {} is missing or is not an object", - schema.as_deref().unwrap_or("field") - ), - } + SchemaError { context } + }) } } diff --git a/tests/basic.rs b/tests/basic.rs index 81789f12bf512651f35aa466fb6c35433bb25aed..9dd1a8200e350c8492fd193ca6eef4edbd11ca5b 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -138,7 +138,18 @@ fn validate_regular_schema_err() { "field3": true, "field4": [""] })); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!( + err.to_string(), + "JSON doesn't match expected schema: found 1 error" + ); + assert_eq!(err.context()[0].field, "/field2"); + assert_eq!( + err.context()[0].description, + "0 is less than the minimum of 5" + ); } #[test] @@ -176,7 +187,11 @@ fn validate_nested_schema_err() { } } })); - assert!(result.is_err()) + let err = result.err().unwrap(); + assert_eq!( + err.to_string(), + "JSON doesn't match expected schema: found 2 errors" + ); } #[test]