use crate::{paths::Location, ValidationError};
use ahash::AHashMap;
use referencing::Uri;
use serde::{
ser::{SerializeMap, SerializeSeq, SerializeStruct},
Serialize,
};
use std::{fmt, sync::Arc};
#[derive(Debug, Clone, PartialEq)]
pub struct Annotations(Arc<serde_json::Value>);
impl Annotations {
#[must_use]
pub(crate) fn new(v: serde_json::Value) -> Self {
Annotations(Arc::new(v))
}
#[must_use]
pub(crate) fn from_arc(v: Arc<serde_json::Value>) -> Self {
Annotations(v)
}
#[inline]
#[must_use]
pub fn into_inner(self) -> serde_json::Value {
Arc::try_unwrap(self.0).unwrap_or_else(|arc| (*arc).clone())
}
#[must_use]
pub fn value(&self) -> &serde_json::Value {
&self.0
}
}
impl serde::Serialize for Annotations {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorDescription {
keyword: String,
message: String,
}
impl ErrorDescription {
#[inline]
#[must_use]
pub(crate) fn new(keyword: impl Into<String>, message: String) -> Self {
Self {
keyword: keyword.into(),
message,
}
}
#[inline]
#[must_use]
pub(crate) fn from_validation_error(e: &ValidationError<'_>) -> Self {
ErrorDescription {
keyword: e.kind().keyword().to_owned(),
message: e.to_string(),
}
}
#[inline]
#[must_use]
pub fn keyword(&self) -> &str {
&self.keyword
}
#[inline]
#[must_use]
pub fn into_inner(self) -> String {
self.message
}
#[inline]
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for ErrorDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct EvaluationNode {
pub(crate) keyword_location: Location,
pub(crate) absolute_keyword_location: Option<Arc<Uri<String>>>,
pub(crate) schema_location: Arc<str>,
pub(crate) instance_location: Location,
pub(crate) valid: bool,
pub(crate) annotations: Option<Annotations>,
pub(crate) dropped_annotations: Option<Annotations>,
pub(crate) errors: Vec<ErrorDescription>,
pub(crate) children: Vec<EvaluationNode>,
}
impl EvaluationNode {
pub(crate) fn valid(
keyword_location: Location,
absolute_keyword_location: Option<Arc<Uri<String>>>,
schema_location: impl Into<Arc<str>>,
instance_location: Location,
annotations: Option<Annotations>,
children: Vec<EvaluationNode>,
) -> Self {
let schema_location = schema_location.into();
EvaluationNode {
keyword_location,
absolute_keyword_location,
schema_location,
instance_location,
valid: true,
annotations,
dropped_annotations: None,
errors: Vec::new(),
children,
}
}
pub(crate) fn invalid(
keyword_location: Location,
absolute_keyword_location: Option<Arc<Uri<String>>>,
schema_location: impl Into<Arc<str>>,
instance_location: Location,
annotations: Option<Annotations>,
errors: Vec<ErrorDescription>,
children: Vec<EvaluationNode>,
) -> Self {
let schema_location = schema_location.into();
EvaluationNode {
keyword_location,
absolute_keyword_location,
schema_location,
instance_location,
valid: false,
annotations: None,
dropped_annotations: annotations,
errors,
children,
}
}
}
#[derive(Debug)]
pub struct Evaluation {
root: EvaluationNode,
}
impl Evaluation {
pub(crate) fn new(root: EvaluationNode) -> Self {
Evaluation { root }
}
#[must_use]
pub fn flag(&self) -> FlagOutput {
FlagOutput {
valid: self.root.valid,
}
}
#[must_use]
pub fn list(&self) -> ListOutput<'_> {
ListOutput { root: &self.root }
}
#[must_use]
pub fn hierarchical(&self) -> HierarchicalOutput<'_> {
HierarchicalOutput { root: &self.root }
}
#[must_use]
pub fn iter_annotations(&self) -> AnnotationIter<'_> {
AnnotationIter::new(&self.root)
}
#[must_use]
pub fn iter_errors(&self) -> ErrorIter<'_> {
ErrorIter::new(&self.root)
}
}
#[derive(Clone, Copy, Debug, Serialize)]
pub struct FlagOutput {
pub valid: bool,
}
#[derive(Debug)]
pub struct ListOutput<'a> {
root: &'a EvaluationNode,
}
#[derive(Debug)]
pub struct HierarchicalOutput<'a> {
root: &'a EvaluationNode,
}
impl Serialize for ListOutput<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_list(self.root, serializer)
}
}
impl Serialize for HierarchicalOutput<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_hierarchical(self.root, serializer)
}
}
fn serialize_list<S>(root: &EvaluationNode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("ListOutput", 2)?;
state.serialize_field("valid", &root.valid)?;
let mut entries = Vec::new();
collect_list_entries(root, &mut entries);
state.serialize_field("details", &entries)?;
state.end()
}
fn serialize_hierarchical<S>(root: &EvaluationNode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_unit(root, serializer, true)
}
fn collect_list_entries<'a>(node: &'a EvaluationNode, out: &mut Vec<ListEntry<'a>>) {
out.push(ListEntry::new(node));
for child in &node.children {
collect_list_entries(child, out);
}
}
fn serialize_unit<S>(
node: &EvaluationNode,
serializer: S,
include_children: bool,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("OutputUnit", 7)?;
state.serialize_field("valid", &node.valid)?;
state.serialize_field("evaluationPath", node.keyword_location.as_str())?;
state.serialize_field("schemaLocation", node.schema_location.as_ref())?;
state.serialize_field("instanceLocation", node.instance_location.as_str())?;
if let Some(annotations) = &node.annotations {
state.serialize_field("annotations", annotations)?;
}
if let Some(annotations) = &node.dropped_annotations {
state.serialize_field("droppedAnnotations", annotations)?;
}
if !node.errors.is_empty() {
state.serialize_field("errors", &ErrorEntriesSerializer(&node.errors))?;
}
if include_children && !node.children.is_empty() {
state.serialize_field(
"details",
&DetailsSerializer {
children: &node.children,
},
)?;
}
state.end()
}
pub(crate) fn format_schema_location(
location: &Location,
absolute: Option<&Arc<Uri<String>>>,
) -> Arc<str> {
if let Some(uri) = absolute {
let base = uri.strip_fragment();
if location.as_str().is_empty() {
Arc::from(format!("{base}#"))
} else {
Arc::from(format!("{base}#{}", location.as_str()))
}
} else {
location.as_arc()
}
}
struct ListEntry<'a> {
node: &'a EvaluationNode,
}
impl<'a> ListEntry<'a> {
fn new(node: &'a EvaluationNode) -> Self {
ListEntry { node }
}
}
impl Serialize for ListEntry<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_unit(self.node, serializer, false)
}
}
struct DetailsSerializer<'a> {
children: &'a [EvaluationNode],
}
impl Serialize for DetailsSerializer<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.children.len()))?;
for child in self.children {
seq.serialize_element(&SeqEntry { node: child })?;
}
seq.end()
}
}
struct SeqEntry<'a> {
node: &'a EvaluationNode,
}
impl Serialize for SeqEntry<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_unit(self.node, serializer, true)
}
}
#[derive(Clone, Copy, Debug)]
pub struct AnnotationEntry<'a> {
pub schema_location: &'a str,
pub absolute_keyword_location: Option<&'a Uri<String>>,
pub instance_location: &'a Location,
pub annotations: &'a Annotations,
}
#[derive(Clone, Copy, Debug)]
pub struct ErrorEntry<'a> {
pub schema_location: &'a str,
pub absolute_keyword_location: Option<&'a Uri<String>>,
pub instance_location: &'a Location,
pub error: &'a ErrorDescription,
}
struct NodeIter<'a> {
stack: Vec<&'a EvaluationNode>,
}
impl<'a> NodeIter<'a> {
fn new(root: &'a EvaluationNode) -> Self {
NodeIter { stack: vec![root] }
}
}
impl<'a> Iterator for NodeIter<'a> {
type Item = &'a EvaluationNode;
fn next(&mut self) -> Option<Self::Item> {
let node = self.stack.pop()?;
for child in node.children.iter().rev() {
self.stack.push(child);
}
Some(node)
}
}
pub struct AnnotationIter<'a> {
nodes: NodeIter<'a>,
}
impl<'a> AnnotationIter<'a> {
fn new(root: &'a EvaluationNode) -> Self {
AnnotationIter {
nodes: NodeIter::new(root),
}
}
}
impl<'a> Iterator for AnnotationIter<'a> {
type Item = AnnotationEntry<'a>;
fn next(&mut self) -> Option<Self::Item> {
for node in self.nodes.by_ref() {
if let Some(annotations) = node.annotations.as_ref() {
return Some(AnnotationEntry {
schema_location: &node.schema_location,
absolute_keyword_location: node.absolute_keyword_location.as_deref(),
instance_location: &node.instance_location,
annotations,
});
}
}
None
}
}
pub struct ErrorIter<'a> {
nodes: NodeIter<'a>,
current: Option<(&'a EvaluationNode, usize)>,
}
impl<'a> ErrorIter<'a> {
fn new(root: &'a EvaluationNode) -> Self {
ErrorIter {
nodes: NodeIter::new(root),
current: None,
}
}
}
impl<'a> Iterator for ErrorIter<'a> {
type Item = ErrorEntry<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some((node, idx)) = self.current {
if idx < node.errors.len() {
let entry = ErrorEntry {
schema_location: &node.schema_location,
absolute_keyword_location: node.absolute_keyword_location.as_deref(),
instance_location: &node.instance_location,
error: &node.errors[idx],
};
self.current = Some((node, idx + 1));
return Some(entry);
}
self.current = None;
}
match self.nodes.next() {
Some(node) => {
if node.errors.is_empty() {
continue;
}
self.current = Some((node, 0));
}
None => return None,
}
}
}
}
struct ErrorEntriesSerializer<'a>(&'a [ErrorDescription]);
impl<'a> Serialize for ErrorEntriesSerializer<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut grouped: Vec<(&'a str, Vec<&'a str>)> = Vec::new();
let mut indexes: AHashMap<&'a str, usize> = AHashMap::new();
for error in self.0 {
let keyword = error.keyword();
let msg = error.message();
if let Some(&idx) = indexes.get(keyword) {
grouped[idx].1.push(msg);
} else {
indexes.insert(keyword, grouped.len());
grouped.push((keyword, vec![msg]));
}
}
let mut map = serializer.serialize_map(Some(grouped.len()))?;
for (keyword, messages) in grouped {
if messages.len() == 1 {
map.serialize_entry(keyword, messages[0])?;
} else {
map.serialize_entry(keyword, &messages)?;
}
}
map.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::sync::Arc;
fn loc() -> Location {
Location::new()
}
fn annotation(value: serde_json::Value) -> Annotations {
Annotations::new(value)
}
impl ErrorDescription {
fn from_string(s: &str) -> Self {
ErrorDescription {
keyword: "error".into(),
message: s.to_string(),
}
}
}
fn leaf_with_annotation(schema: &str, ann: serde_json::Value) -> EvaluationNode {
EvaluationNode::valid(
loc(),
None,
schema.to_string(),
loc(),
Some(annotation(ann)),
Vec::new(),
)
}
fn leaf_with_error(schema: &str, msg: &str) -> EvaluationNode {
EvaluationNode::invalid(
loc(),
None,
schema.to_string(),
loc(),
None,
vec![ErrorDescription::from_string(msg)],
Vec::new(),
)
}
#[test]
fn iter_annotations_visits_all_nodes() {
let child = leaf_with_annotation("/child", json!({"k": "v"}));
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"root": true}))),
vec![child],
);
let evaluation = Evaluation::new(root);
let entries: Vec<_> = evaluation.iter_annotations().collect();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].schema_location, "/root");
assert_eq!(entries[1].schema_location, "/child");
}
#[test]
fn iter_errors_visits_all_nodes() {
let child = leaf_with_error("/child", "boom");
let root = EvaluationNode::invalid(
loc(),
None,
"/root".to_string(),
loc(),
None,
vec![ErrorDescription::from_string("root error")],
vec![child],
);
let evaluation = Evaluation::new(root);
let entries: Vec<_> = evaluation.iter_errors().collect();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].error.to_string(), "root error");
assert_eq!(entries[1].error.to_string(), "boom");
}
#[test]
fn flag_output_valid() {
let root = EvaluationNode::valid(loc(), None, "/root".to_string(), loc(), None, Vec::new());
let evaluation = Evaluation::new(root);
let flag = evaluation.flag();
assert!(flag.valid);
}
#[test]
fn flag_output_invalid() {
let root = EvaluationNode::invalid(
loc(),
None,
"/root".to_string(),
loc(),
None,
vec![ErrorDescription::from_string("error")],
Vec::new(),
);
let evaluation = Evaluation::new(root);
let flag = evaluation.flag();
assert!(!flag.valid);
}
#[test]
fn flag_output_serialization() {
let root = EvaluationNode::valid(loc(), None, "/root".to_string(), loc(), None, Vec::new());
let evaluation = Evaluation::new(root);
let flag = evaluation.flag();
let serialized = serde_json::to_value(flag).expect("serialization succeeds");
assert_eq!(serialized, json!({"valid": true}));
}
#[test]
fn list_output_serialization_valid() {
let root = EvaluationNode::valid(loc(), None, "#".to_string(), loc(), None, Vec::new());
let evaluation = Evaluation::new(root);
let list = evaluation.list();
let serialized = serde_json::to_value(list).expect("serialization succeeds");
assert_eq!(
serialized,
json!({
"valid": true,
"details": [
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "#",
"instanceLocation": ""
}
]
})
);
}
#[test]
fn list_output_serialization_with_children() {
let child1 = leaf_with_annotation("/child1", json!({"key": "value"}));
let child2 = leaf_with_error("/child2", "child error");
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"root": true}))),
vec![child1, child2],
);
let evaluation = Evaluation::new(root);
let list = evaluation.list();
let serialized = serde_json::to_value(list).expect("serialization succeeds");
assert_eq!(
serialized,
json!({
"valid": true,
"details": [
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "/root",
"instanceLocation": "",
"annotations": {"root": true}
},
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "/child1",
"instanceLocation": "",
"annotations": {"key": "value"}
},
{
"valid": false,
"evaluationPath": "",
"schemaLocation": "/child2",
"instanceLocation": "",
"errors": {"error": "child error"}
}
]
})
);
}
#[test]
fn hierarchical_output_serialization() {
let child = leaf_with_annotation("/child", json!({"nested": "data"}));
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"root": "annotation"}))),
vec![child],
);
let evaluation = Evaluation::new(root);
let hierarchical = evaluation.hierarchical();
let serialized = serde_json::to_value(hierarchical).expect("serialization succeeds");
assert_eq!(
serialized,
json!({
"valid": true,
"evaluationPath": "",
"schemaLocation": "/root",
"instanceLocation": "",
"annotations": {"root": "annotation"},
"details": [
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "/child",
"instanceLocation": "",
"annotations": {"nested": "data"}
}
]
})
);
}
#[test]
fn outputs_include_errors_and_dropped_annotations() {
let invalid_child = EvaluationNode::invalid(
loc(),
None,
"/items/type".to_string(),
Location::new().join(1usize),
None,
vec![ErrorDescription::from_string("child error")],
Vec::new(),
);
let prefix_child = leaf_with_annotation("/prefix", json!(0));
let root = EvaluationNode::invalid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"dropped": true}))),
vec![ErrorDescription::from_string("root failure")],
vec![invalid_child, prefix_child],
);
let evaluation = Evaluation::new(root);
let list = serde_json::to_value(evaluation.list()).expect("serialization succeeds");
assert_eq!(
list,
json!({
"valid": false,
"details": [
{
"valid": false,
"evaluationPath": "",
"schemaLocation": "/root",
"instanceLocation": "",
"droppedAnnotations": {"dropped": true},
"errors": {"error": "root failure"}
},
{
"valid": false,
"evaluationPath": "",
"schemaLocation": "/items/type",
"instanceLocation": "/1",
"errors": {"error": "child error"}
},
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "/prefix",
"instanceLocation": "",
"annotations": 0
}
]
})
);
let hierarchical =
serde_json::to_value(evaluation.hierarchical()).expect("serialization succeeds");
assert_eq!(
hierarchical,
json!({
"valid": false,
"evaluationPath": "",
"schemaLocation": "/root",
"instanceLocation": "",
"droppedAnnotations": {"dropped": true},
"errors": {"error": "root failure"},
"details": [
{
"valid": false,
"evaluationPath": "",
"schemaLocation": "/items/type",
"instanceLocation": "/1",
"errors": {"error": "child error"}
},
{
"valid": true,
"evaluationPath": "",
"schemaLocation": "/prefix",
"instanceLocation": "",
"annotations": 0
}
]
})
);
}
#[test]
fn empty_evaluation_tree() {
let root = EvaluationNode::valid(loc(), None, "/root".to_string(), loc(), None, Vec::new());
let evaluation = Evaluation::new(root);
assert_eq!(evaluation.iter_annotations().count(), 0);
assert_eq!(evaluation.iter_errors().count(), 0);
let flag = evaluation.flag();
assert!(flag.valid);
}
#[test]
fn deep_nesting() {
let level3 = leaf_with_annotation("/level3", json!({"level": 3}));
let level2 = EvaluationNode::valid(
loc(),
None,
"/level2".to_string(),
loc(),
Some(annotation(json!({"level": 2}))),
vec![level3],
);
let level1 = EvaluationNode::valid(
loc(),
None,
"/level1".to_string(),
loc(),
Some(annotation(json!({"level": 1}))),
vec![level2],
);
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"level": 0}))),
vec![level1],
);
let evaluation = Evaluation::new(root);
let annotations: Vec<_> = evaluation.iter_annotations().collect();
assert_eq!(annotations.len(), 4);
assert_eq!(annotations[0].schema_location, "/root");
assert_eq!(annotations[1].schema_location, "/level1");
assert_eq!(annotations[2].schema_location, "/level2");
assert_eq!(annotations[3].schema_location, "/level3");
}
#[test]
fn wide_tree() {
let children: Vec<_> = (0..10)
.map(|i| leaf_with_annotation(&format!("/child{i}"), json!({"index": i})))
.collect();
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"root": true}))),
children,
);
let evaluation = Evaluation::new(root);
let annotations: Vec<_> = evaluation.iter_annotations().collect();
assert_eq!(annotations.len(), 11); }
#[test]
fn multiple_errors_per_node() {
let errors = vec![
ErrorDescription::from_string("error 1"),
ErrorDescription::from_string("error 2"),
ErrorDescription::from_string("error 3"),
];
let root = EvaluationNode::invalid(
loc(),
None,
"/root".to_string(),
loc(),
None,
errors,
Vec::new(),
);
let evaluation = Evaluation::new(root);
let error_entries: Vec<_> = evaluation.iter_errors().collect();
assert_eq!(error_entries.len(), 3);
assert_eq!(error_entries[0].error.to_string(), "error 1");
assert_eq!(error_entries[1].error.to_string(), "error 2");
assert_eq!(error_entries[2].error.to_string(), "error 3");
}
#[test]
fn mixed_valid_and_invalid_nodes() {
let valid_child = leaf_with_annotation("/valid", json!({"ok": true}));
let invalid_child = leaf_with_error("/invalid", "failed");
let root = EvaluationNode::invalid(
loc(),
None,
"/root".to_string(),
loc(),
Some(annotation(json!({"attempted": true}))),
vec![ErrorDescription::from_string("root failed")],
vec![valid_child, invalid_child],
);
let evaluation = Evaluation::new(root);
let annotations: Vec<_> = evaluation.iter_annotations().collect();
assert_eq!(annotations.len(), 1);
assert_eq!(annotations[0].schema_location, "/valid");
let errors: Vec<_> = evaluation.iter_errors().collect();
assert_eq!(errors.len(), 2);
}
#[test]
fn annotations_iterator_skips_nodes_without_annotations() {
let no_annotation =
EvaluationNode::valid(loc(), None, "/no_ann".to_string(), loc(), None, Vec::new());
let with_annotation = leaf_with_annotation("/with_ann", json!({"present": true}));
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
None,
vec![no_annotation, with_annotation],
);
let evaluation = Evaluation::new(root);
let annotations: Vec<_> = evaluation.iter_annotations().collect();
assert_eq!(annotations.len(), 1);
assert_eq!(annotations[0].schema_location, "/with_ann");
}
#[test]
fn errors_iterator_skips_nodes_without_errors() {
let no_error = EvaluationNode::valid(
loc(),
None,
"/no_error".to_string(),
loc(),
Some(annotation(json!({"ok": true}))),
Vec::new(),
);
let with_error = leaf_with_error("/with_error", "failed");
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
None,
vec![no_error, with_error],
);
let evaluation = Evaluation::new(root);
let errors: Vec<_> = evaluation.iter_errors().collect();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].schema_location, "/with_error");
}
#[test]
fn error_entries_serialization_empty() {
let entries = ErrorEntriesSerializer(&[]);
let serialized = serde_json::to_value(&entries).expect("serialization succeeds");
assert!(serialized.is_object());
assert_eq!(serialized.as_object().unwrap().len(), 0);
}
#[test]
fn error_entries_serialization_single() {
let errors = vec![ErrorDescription::from_string("test error")];
let entries = ErrorEntriesSerializer(&errors);
let serialized = serde_json::to_value(&entries).expect("serialization succeeds");
assert!(serialized.is_object());
assert_eq!(serialized.as_object().unwrap().len(), 1);
assert!(serialized.get("error").is_some());
}
#[test]
fn error_entries_serialization_multiple() {
let errors = vec![
ErrorDescription::new("alpha", "error 1".to_string()),
ErrorDescription::new("beta", "error 2".to_string()),
ErrorDescription::new("gamma", "error 3".to_string()),
];
let entries = ErrorEntriesSerializer(&errors);
let serialized = serde_json::to_value(&entries).expect("serialization succeeds");
assert_eq!(serialized.as_object().unwrap().len(), 3);
assert!(serialized.get("alpha").is_some());
assert!(serialized.get("beta").is_some());
assert!(serialized.get("gamma").is_some());
}
#[test]
fn error_entries_serialization_preserves_duplicates() {
let errors = vec![
ErrorDescription::new("required", "\"foo\" is required".to_string()),
ErrorDescription::new("required", "\"bar\" is required".to_string()),
];
let entries = ErrorEntriesSerializer(&errors);
let serialized = serde_json::to_value(&entries).expect("serialization succeeds");
let value = serialized
.get("required")
.expect("required keyword present")
.as_array()
.expect("multiple errors serialized as array");
assert_eq!(value.len(), 2);
assert_eq!(value[0], "\"foo\" is required");
assert_eq!(value[1], "\"bar\" is required");
}
#[test]
fn list_output_preserves_multiple_errors_per_keyword() {
let errors = vec![
ErrorDescription::new("required", "\"foo\" is required".to_string()),
ErrorDescription::new("required", "\"bar\" is required".to_string()),
];
let root = EvaluationNode::invalid(
loc(),
None,
"/required".to_string(),
loc(),
None,
errors,
Vec::new(),
);
let evaluation = Evaluation::new(root);
let list = serde_json::to_value(evaluation.list()).expect("serialization succeeds");
let root_unit = list
.get("details")
.and_then(|value| value.as_array())
.and_then(|details| details.first())
.expect("list output contains root unit");
let errors = root_unit
.get("errors")
.and_then(|errors| errors.get("required"))
.and_then(|value| value.as_array())
.expect("errors serialized as array");
assert_eq!(errors.len(), 2);
assert_eq!(errors[0], "\"foo\" is required");
assert_eq!(errors[1], "\"bar\" is required");
}
#[test]
fn format_schema_location_without_absolute() {
let location = Location::new().join("properties").join("name");
let formatted = format_schema_location(&location, None);
assert_eq!(formatted.as_ref(), "/properties/name");
}
#[test]
fn format_schema_location_with_absolute_no_fragment() {
let location = Location::new().join("properties");
let uri = Arc::new(
Uri::parse("http://example.com/schema.json")
.unwrap()
.to_owned(),
);
let formatted = format_schema_location(&location, Some(&uri));
assert_eq!(
formatted.as_ref(),
"http://example.com/schema.json#/properties"
);
}
#[test]
fn format_schema_location_with_absolute_empty_location() {
let location = Location::new();
let uri = Arc::new(
Uri::parse("http://example.com/schema.json")
.unwrap()
.to_owned(),
);
let formatted = format_schema_location(&location, Some(&uri));
assert_eq!(formatted.as_ref(), "http://example.com/schema.json#");
}
#[test]
fn format_schema_location_with_absolute_existing_fragment() {
let location = Location::new().join("properties");
let uri = Arc::new(
Uri::parse("http://example.com/schema.json#/defs/myDef")
.unwrap()
.to_owned(),
);
let formatted = format_schema_location(&location, Some(&uri));
assert_eq!(
formatted.as_ref(),
"http://example.com/schema.json#/properties"
);
}
#[test]
fn dropped_annotations_on_invalid_node() {
let annotations = Some(annotation(json!({"dropped": true})));
let root = EvaluationNode::invalid(
loc(),
None,
"/root".to_string(),
loc(),
annotations.clone(),
vec![ErrorDescription::from_string("failed")],
Vec::new(),
);
assert!(!root.valid);
assert!(root.annotations.is_none());
assert!(root.dropped_annotations.is_some());
assert_eq!(
root.dropped_annotations.as_ref().unwrap(),
annotations.as_ref().unwrap()
);
}
#[test]
fn valid_node_has_no_dropped_annotations() {
let annotations = Some(annotation(json!({"kept": true})));
let root = EvaluationNode::valid(
loc(),
None,
"/root".to_string(),
loc(),
annotations.clone(),
Vec::new(),
);
assert!(root.valid);
assert!(root.annotations.is_some());
assert!(root.dropped_annotations.is_none());
assert_eq!(
root.annotations.as_ref().unwrap(),
annotations.as_ref().unwrap()
);
}
#[test]
fn absolute_keyword_location_populated_with_id() {
use serde_json::json;
let schema = json!({
"$id": "https://example.com/schema",
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let validator = crate::validator_for(&schema).expect("schema compiles");
let evaluation = validator.evaluate(&json!({"name": "test"}));
let annotations: Vec<_> = evaluation.iter_annotations().collect();
assert!(!annotations.is_empty());
let with_absolute = annotations
.iter()
.filter(|a| a.absolute_keyword_location.is_some())
.count();
assert!(with_absolute > 0);
for annotation in annotations
.iter()
.filter(|a| a.absolute_keyword_location.is_some())
{
let uri_str = annotation.absolute_keyword_location.unwrap().as_str();
assert!(uri_str.starts_with("https://example.com/schema"));
}
}
#[test]
fn annotations_value_returns_reference() {
let expected = json!({"key": "value"});
let annotations = Annotations::new(expected.clone());
assert_eq!(annotations.value(), &expected);
}
#[test]
fn annotations_into_inner_consumes_and_returns_value() {
let expected = json!({"key": "value", "nested": {"array": [1, 2, 3]}});
let annotations = Annotations::new(expected.clone());
let inner = annotations.into_inner();
assert_eq!(inner, expected);
}
#[test]
fn error_description_into_inner_consumes_and_returns_message() {
let expected_message = "test error message";
let error = ErrorDescription::from_string(expected_message);
let message = error.into_inner();
assert_eq!(message, expected_message);
}
}