#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Valid,
Invalid(Vec<String>),
NotValidated(NotValidated),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotValidated {
NoSchema,
NoRegistry,
FeatureOff,
KindUnsupported,
Undecodable,
BadSchema,
}
impl std::fmt::Display for NotValidated {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
NotValidated::NoSchema => "no schema served for this type",
NotValidated::NoRegistry => "no registry loaded, so no type was looked up",
NotValidated::FeatureOff => "validation compiled out (validate-json)",
NotValidated::KindUnsupported => "schema kind has no validator beyond decode",
NotValidated::Undecodable => "bytes did not decode",
NotValidated::BadSchema => "served schema does not compile",
})
}
}
impl std::fmt::Display for Verdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Verdict::Valid => f.write_str("valid"),
Verdict::Invalid(errors) => write!(f, "invalid ({} violation(s))", errors.len()),
Verdict::NotValidated(reason) => write!(f, "not validated — {reason}"),
}
}
}
#[cfg(feature = "validate-json")]
pub fn validate_json(validator: &jsonschema::Validator, value: &serde_json::Value) -> Verdict {
let errors: Vec<String> = validator
.iter_errors(value)
.map(|e| {
let path = e.instance_path().to_string();
if path.is_empty() {
e.to_string()
} else {
format!("{path}: {e}")
}
})
.collect();
if errors.is_empty() {
Verdict::Valid
} else {
Verdict::Invalid(errors)
}
}
#[cfg(test)]
mod vocabulary_tests {
use super::*;
#[test]
fn the_two_silences_have_distinct_wire_spellings() {
assert_eq!(
NotValidated::NoSchema.to_string(),
"no schema served for this type"
);
assert_eq!(
NotValidated::NoRegistry.to_string(),
"no registry loaded, so no type was looked up"
);
}
}
#[cfg(all(test, feature = "validate-json"))]
mod tests {
use super::*;
use serde_json::json;
fn validator() -> jsonschema::Validator {
jsonschema::validator_for(&json!({
"type": "object",
"required": ["x"],
"properties": {
"x": { "type": "integer", "minimum": 0 },
"name": { "type": "string" },
},
}))
.expect("fixture schema compiles")
}
#[test]
fn conformant_and_nonconformant_values_get_opposite_verdicts() {
let v = validator();
assert_eq!(validate_json(&v, &json!({"x": 3})), Verdict::Valid);
match validate_json(&v, &json!({"x": -2, "name": 7})) {
Verdict::Invalid(errors) => {
assert_eq!(errors.len(), 2, "{errors:?}");
assert!(errors.iter().any(|e| e.contains("/x")), "{errors:?}");
assert!(errors.iter().any(|e| e.contains("/name")), "{errors:?}");
}
other => panic!("expected Invalid, got {other:?}"),
}
assert!(matches!(validate_json(&v, &json!({})), Verdict::Invalid(_)));
}
}