use std::fmt;
use serde_json::Value;
use thiserror::Error;
use crate::payload::Payload;
pub trait ValidatedPayload: Payload {
const SCHEMA_JSON: &'static str;
fn validate_value(value: &Value) -> Result<(), ValidationError> {
against_schema(Self::SCHEMA_JSON, value)
}
}
pub fn against_schema(schema_json: &str, value: &Value) -> Result<(), ValidationError> {
let schema_value: Value =
serde_json::from_str(schema_json).map_err(ValidationError::schema_parse)?;
let compiled = jsonschema::options()
.with_draft(jsonschema::Draft::Draft202012)
.build(&schema_value)
.map_err(|e| ValidationError::schema_compile(e.to_string()))?;
let messages: Vec<String> = compiled.iter_errors(value).map(|e| e.to_string()).collect();
if !messages.is_empty() {
return Err(ValidationError::instance(messages));
}
Ok(())
}
#[derive(Debug, Error)]
pub struct ValidationError {
kind: ErrorKind,
messages: Vec<String>,
}
#[derive(Debug)]
enum ErrorKind {
SchemaParse,
SchemaCompile,
Instance,
}
impl ValidationError {
fn schema_parse(e: serde_json::Error) -> Self {
Self {
kind: ErrorKind::SchemaParse,
messages: vec![e.to_string()],
}
}
fn schema_compile(message: String) -> Self {
Self {
kind: ErrorKind::SchemaCompile,
messages: vec![message],
}
}
fn instance(messages: Vec<String>) -> Self {
Self {
kind: ErrorKind::Instance,
messages,
}
}
pub fn messages(&self) -> &[String] {
&self.messages
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self.kind {
ErrorKind::SchemaParse => "schema JSON did not parse",
ErrorKind::SchemaCompile => "schema did not compile",
ErrorKind::Instance => "payload failed schema validation",
};
write!(f, "{label}: {}", self.messages.join("; "))
}
}