use std::fmt::Display; use jsonschema::{Draft, JSONSchema}; use serde_json::{Map, Value}; pub struct Inspector { schema: JSONSchema, } impl Inspector { 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 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>() .split("::") .last() .ok_or(InitError::UnknownFailure)?; 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`) pub fn new(target: &str, schemas: &Value, resolve_refs: bool) -> Result<Self, InitError> { use InitError::*; // extract schemas object let Value::Object(schemas) = schemas else { return Err(InitError::UnknownFailure); }; // extract target from schema let Value::Object(mut target) = schemas .get(target) .ok_or_else(|| SchemaNotFound(target.to_owned()))? .to_owned() else { return Err(InitError::UnknownFailure); }; if resolve_refs { // extract properties let Value::Object(mut props) = target.remove(Self::PROP).ok_err()? else { return Err(InitError::UnknownFailure); }; Self::resolve_references(schemas, &mut props)?; // reinsert resolved properties target.insert(Self::PROP.into(), Value::Object(props)); } Ok(Self { schema: JSONSchema::options() .with_draft(Draft::Draft202012) .compile(&Value::Object(target)) .ok() .ok_err()?, }) } // Replace internal '$ref' references with the correct schemas 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(); if let Some(Value::Object(inner_props)) = inner.get_mut(Self::PROP) { Self::resolve_references(schemas, inner_props)?; } } Ok(()) } // 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)?; let Value::String(final_refr) = refr else { return None; }; final_refr.split('/').last().map(ToOwned::to_owned) } /// Inspect (validate) the given JSON value to check if it conforms to the compiled schema /// /// [`SchemaError`] is returned upon failure which contains a detailed explanation of everything /// 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 } }) } } #[derive(Debug)] pub struct SchemaError { cause: String, } impl std::error::Error for SchemaError {} 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) } } #[derive(Debug)] pub enum InitError { SchemaNotFound(String), ResolutionFailure(String), CompileFailure, UnknownFailure, } 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::UnknownFailure => write!(f, "{base} unknown failure occured"), Self::CompileFailure => write!(f, "{base} failed to compile schema"), } } } trait TempMap<T> where Self: Sized, { fn ok_err(self) -> Result<T, InitError>; } impl<T> TempMap<T> for Option<T> { fn ok_err(self) -> Result<T, InitError> { self.ok_or(InitError::UnknownFailure) } }