Skip to content
Snippets Groups Projects
Commit 02b075e5 authored by VoltaireNoir's avatar VoltaireNoir
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
/target
This diff is collapsed.
[package]
name = "schema-police"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
jsonschema = {version = "0.17.1", default-features = false, features = ["draft202012"]}
serde_json = "1.0.113"
utoipa = "4.2.0"
use std::{fmt::Display, marker::PhantomData};
use jsonschema::{Draft, JSONSchema};
use serde_json::{Map, Value};
pub struct Inspector<T> {
schema: JSONSchema,
marker: PhantomData<T>,
}
impl<T> Inspector<T> {
const PROP: &'static str = "properties";
const REF: &'static str = "$ref";
const AOF: &'static str = "allOf";
/// Construct an instance of a schema `Inspector` using the provided OpenAPI spec's schemas (in the form of `serde_json::Value`)
pub fn new(schemas: &Value, resolve_refs: bool) -> Result<Self, InitError> {
use InitError::*;
// extract schemas object
let Value::Object(schemas) = schemas else {
return Err(InitError::UnknownFailure);
};
// get target name from type inference
let target_name = std::any::type_name::<T>()
.split("::")
.last()
.ok_or(UnknownFailure)?;
// extract target from schema
let Value::Object(mut target) = schemas
.get(target_name)
.ok_or(SchemaNotFound(target_name))?
.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()?,
marker: PhantomData,
})
}
// 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(&'static str),
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)
}
}
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