feat: add support for unevaluatedProperties to draft 2019-09 and 2020-12

This commit is contained in:
Toby Lawrence 2023-03-01 16:10:10 -05:00 committed by Dmitry Dygalo
parent 848b7b1284
commit 63494600b8
13 changed files with 1466 additions and 205 deletions

View File

@ -9,6 +9,9 @@
- Bump `fraction` to `0.13`.
- Bump `iso8601` to `0.6`.
- Replace `lazy_static` with `once_cell`.
- Add support for `unevaluatedProperties`. (gated by the `draft201909`/`draft202012` feature flags)
- When using the draft 2019-09 or draft 2020-12 specification, `$ref` is now evaluated alongside
other keywords.
## [0.16.1] - 2022-10-20

View File

@ -154,7 +154,15 @@ pub(crate) fn compile_validators<'a>(
)),
},
Value::Object(object) => {
if let Some(reference) = object.get("$ref") {
// In Draft 2019-09 and later, `$ref` can be evaluated alongside other attribute aka
// adjacent validation. We check here to see if adjacent validation is supported, and if
// so, we use the normal keyword validator collection logic.
//
// Otherwise, we isolate `$ref` and generate a schema reference validator directly.
let maybe_reference = object
.get("$ref")
.filter(|_| !keywords::ref_::supports_adjacent_validation(context.config.draft()));
if let Some(reference) = maybe_reference {
let unmatched_keywords = object
.iter()
.filter_map(|(k, v)| {
@ -165,24 +173,16 @@ pub(crate) fn compile_validators<'a>(
}
})
.collect();
let mut validators = Vec::new();
if let Value::String(reference) = reference {
let validator = keywords::ref_::compile(schema, reference, &context)
.expect("Should always return Some")?;
validators.push(("$ref".to_string(), validator));
Ok(SchemaNode::new_from_keywords(
&context,
validators,
Some(unmatched_keywords),
))
} else {
Err(ValidationError::single_type_error(
JSONPointer::default(),
relative_path,
reference,
PrimitiveType::String,
))
}
let validator = keywords::ref_::compile(object, reference, &context)
.expect("should always return Some")?;
let validators = vec![("$ref".to_string(), validator)];
Ok(SchemaNode::new_from_keywords(
&context,
validators,
Some(unmatched_keywords),
))
} else {
let mut validators = Vec::with_capacity(object.len());
let mut unmatched_keywords = AHashMap::new();

View File

@ -138,6 +138,8 @@ pub enum ValidationErrorKind {
Schema,
/// When the input value doesn't match one or multiple required types.
Type { kind: TypeKind },
/// Unexpected properties.
UnevaluatedProperties { unexpected: Vec<String> },
/// When the input array has non-unique elements.
UniqueItems,
/// Reference contains unknown scheme.
@ -692,6 +694,19 @@ impl<'a> ValidationError<'a> {
schema_path,
}
}
pub(crate) const fn unevaluated_properties(
schema_path: JSONPointer,
instance_path: JSONPointer,
instance: &'a Value,
unexpected: Vec<String>,
) -> ValidationError<'a> {
ValidationError {
instance_path,
instance: Cow::Borrowed(instance),
kind: ValidationErrorKind::UnevaluatedProperties { unexpected },
schema_path,
}
}
pub(crate) const fn unique_items(
schema_path: JSONPointer,
instance_path: JSONPointer,
@ -937,6 +952,25 @@ impl fmt::Display for ValidationError<'_> {
ValidationErrorKind::MultipleOf { multiple_of } => {
write!(f, "{} is not a multiple of {}", self.instance, multiple_of)
}
ValidationErrorKind::UnevaluatedProperties { unexpected } => {
let verb = {
if unexpected.len() == 1 {
"was"
} else {
"were"
}
};
write!(
f,
"Unevaluated properties are not allowed ({} {} unexpected)",
unexpected
.iter()
.map(|x| format!("'{}'", x))
.collect::<Vec<String>>()
.join(", "),
verb
)
}
ValidationErrorKind::UniqueItems => {
write!(f, "{} has non-unique elements", self.instance)
}

View File

@ -12,76 +12,12 @@ use crate::{
keywords::CompilationResult,
output::{Annotations, BasicOutput, OutputUnit},
paths::{AbsolutePath, InstancePath, JSONPointer},
properties::*,
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
use ahash::AHashMap;
use fancy_regex::Regex;
use serde_json::{Map, Value};
pub(crate) type PatternedValidators = Vec<(Regex, SchemaNode)>;
/// Provide mapping API to get validators associated with a property from the underlying storage.
pub(crate) trait PropertiesValidatorsMap: Send + Sync {
fn get_validator(&self, property: &str) -> Option<&SchemaNode>;
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)>;
}
// Iterating over a small vector and comparing strings is faster than a map lookup
const MAP_SIZE_THRESHOLD: usize = 40;
pub(crate) type SmallValidatorsMap = Vec<(String, SchemaNode)>;
pub(crate) type BigValidatorsMap = AHashMap<String, SchemaNode>;
impl PropertiesValidatorsMap for SmallValidatorsMap {
#[inline]
fn get_validator(&self, property: &str) -> Option<&SchemaNode> {
for (prop, node) in self {
if prop == property {
return Some(node);
}
}
None
}
#[inline]
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> {
for (prop, node) in self {
if prop == property {
return Some((prop, node));
}
}
None
}
}
impl PropertiesValidatorsMap for BigValidatorsMap {
#[inline]
fn get_validator(&self, property: &str) -> Option<&SchemaNode> {
self.get(property)
}
#[inline]
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> {
self.get_key_value(property)
}
}
macro_rules! dynamic_map {
($validator:tt, $properties:ident, $( $arg:expr ),* $(,)*) => {{
if let Value::Object(map) = $properties {
if map.len() < MAP_SIZE_THRESHOLD {
Some($validator::<SmallValidatorsMap>::compile(
map, $($arg, )*
))
} else {
Some($validator::<BigValidatorsMap>::compile(
map, $($arg, )*
))
}
} else {
Some(Err(ValidationError::null_schema()))
}
}};
}
macro_rules! is_valid {
($node:expr, $value:ident) => {{
$node.is_valid($value)
@ -124,37 +60,6 @@ macro_rules! validate {
}};
}
fn compile_small_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<SmallValidatorsMap, ValidationError<'a>> {
let mut properties = Vec::with_capacity(map.len());
let keyword_context = context.with_path("properties");
for (key, subschema) in map {
let property_context = keyword_context.with_path(key.clone());
properties.push((
key.clone(),
compile_validators(subschema, &property_context)?,
));
}
Ok(properties)
}
fn compile_big_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<BigValidatorsMap, ValidationError<'a>> {
let mut properties = AHashMap::with_capacity(map.len());
let keyword_context = context.with_path("properties");
for (key, subschema) in map {
let property_context = keyword_context.with_path(key.clone());
properties.insert(
key.clone(),
compile_validators(subschema, &property_context)?,
);
}
Ok(properties)
}
/// # Schema example
///
/// ```json
@ -345,16 +250,11 @@ impl AdditionalPropertiesNotEmptyFalseValidator<BigValidatorsMap> {
}
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseValidator<M> {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for (property, value) in item {
if let Some(node) = self.properties.get_validator(property) {
is_valid_pattern_schema!(node, value)
}
// No extra properties are allowed
return false;
}
if let Value::Object(props) = instance {
are_properties_valid(&self.properties, props, |_| false)
} else {
true
}
true
}
fn validate<'instance>(
@ -484,17 +384,13 @@ impl AdditionalPropertiesNotEmptyValidator<BigValidatorsMap> {
}
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValidator<M> {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(map) = instance {
for (property, value) in map {
if let Some(property_validators) = self.properties.get_validator(property) {
is_valid_pattern_schema!(property_validators, value)
}
if !self.node.is_valid(value) {
return false;
}
}
if let Value::Object(props) = instance {
are_properties_valid(&self.properties, props, |instance| {
self.node.is_valid(instance)
})
} else {
true
}
true
}
fn validate<'instance>(
@ -1263,7 +1159,7 @@ pub(crate) fn compile<'a>(
Value::Bool(true) => None, // "additionalProperties" are "true" by default
Value::Bool(false) => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesWithPatternsNotEmptyFalseValidator,
properties,
compiled_patterns,
@ -1278,7 +1174,7 @@ pub(crate) fn compile<'a>(
}
_ => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesWithPatternsNotEmptyValidator,
properties,
schema,
@ -1305,7 +1201,7 @@ pub(crate) fn compile<'a>(
Value::Bool(true) => None, // "additionalProperties" are "true" by default
Value::Bool(false) => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesNotEmptyFalseValidator,
properties,
context
@ -1317,7 +1213,7 @@ pub(crate) fn compile<'a>(
}
_ => {
if let Some(properties) = properties {
dynamic_map!(
compile_dynamic_prop_map_validator!(
AdditionalPropertiesNotEmptyValidator,
properties,
schema,
@ -1331,31 +1227,6 @@ pub(crate) fn compile<'a>(
}
}
/// Create a vector of pattern-validators pairs.
#[inline]
fn compile_patterns<'a>(
obj: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<PatternedValidators, ValidationError<'a>> {
let keyword_context = context.with_path("patternProperties");
let mut compiled_patterns = Vec::with_capacity(obj.len());
for (pattern, subschema) in obj {
let pattern_context = keyword_context.with_path(pattern.to_string());
if let Ok(compiled_pattern) = Regex::new(pattern) {
let node = compile_validators(subschema, &pattern_context)?;
compiled_patterns.push((compiled_pattern, node));
} else {
return Err(ValidationError::format(
JSONPointer::default(),
keyword_context.clone().into_pointer(),
subschema,
"regex",
));
}
}
Ok(compiled_patterns)
}
#[cfg(test)]
mod tests {
use crate::tests_util;

View File

@ -34,6 +34,8 @@ pub(crate) mod property_names;
pub(crate) mod ref_;
pub(crate) mod required;
pub(crate) mod type_;
#[cfg(any(feature = "draft201909", feature = "draft202012"))]
pub(crate) mod unevaluated_properties;
pub(crate) mod unique_items;
use crate::{error, validator::Validate};

View File

@ -3,13 +3,14 @@ use crate::{
error::{error, ErrorIterator},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
primitive_type::PrimitiveType,
resolver::Resolver,
schema_node::SchemaNode,
validator::Validate,
CompilationOptions,
CompilationOptions, Draft, ValidationError,
};
use parking_lot::RwLock;
use serde_json::Value;
use serde_json::{Map, Value};
use std::sync::Arc;
use url::Url;
@ -123,11 +124,33 @@ impl core::fmt::Display for RefValidator {
#[inline]
pub(crate) fn compile<'a>(
_: &'a Value,
reference: &'a str,
_: &'a Map<String, Value>,
schema: &'a Value,
context: &CompilationContext,
) -> Option<CompilationResult<'a>> {
Some(RefValidator::compile(reference, context))
Some(
schema
.as_str()
.ok_or_else(|| {
ValidationError::single_type_error(
JSONPointer::default(),
context.clone().into_pointer(),
schema,
PrimitiveType::String,
)
})
.and_then(|reference| RefValidator::compile(reference, context)),
)
}
pub(crate) const fn supports_adjacent_validation(draft: Draft) -> bool {
match draft {
#[cfg(feature = "draft201909")]
Draft::Draft201909 => true,
#[cfg(feature = "draft202012")]
Draft::Draft202012 => true,
_ => false,
}
}
#[cfg(test)]

File diff suppressed because it is too large Load Diff

View File

@ -91,6 +91,7 @@ mod keywords;
pub mod output;
pub mod paths;
pub mod primitive_type;
pub(crate) mod properties;
mod resolver;
mod schema_node;
mod schemas;

View File

@ -213,6 +213,12 @@ impl From<&[PathChunk]> for JSONPointer {
}
}
impl From<&str> for JSONPointer {
fn from(value: &str) -> Self {
JSONPointer(vec![value.to_string().into()])
}
}
/// An absolute reference
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbsolutePath(url::Url);

View File

@ -0,0 +1,180 @@
use ahash::AHashMap;
use fancy_regex::Regex;
use serde_json::{Map, Value};
use crate::{
compilation::{compile_validators, context::CompilationContext},
paths::JSONPointer,
schema_node::SchemaNode,
validator::Validate,
ValidationError,
};
pub(crate) type PatternedValidators = Vec<(Regex, SchemaNode)>;
/// A value that can look up property validators by name.
pub(crate) trait PropertiesValidatorsMap: Send + Sync {
fn from_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<Self, ValidationError<'a>>
where
Self: Sized;
fn get_validator(&self, property: &str) -> Option<&SchemaNode>;
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)>;
}
// We're defining two different property validator map implementations, one for small map sizes and
// one for large map sizes, to optimize the performance depending on the number of properties
// present.
//
// Implementors should use `compile_dynamic_prop_map_validator!` for building their validator maps
// at runtime, as it wraps up all of the logic to choose the right map size and then build and
// compile the validator.
pub(crate) type SmallValidatorsMap = Vec<(String, SchemaNode)>;
pub(crate) type BigValidatorsMap = AHashMap<String, SchemaNode>;
impl PropertiesValidatorsMap for SmallValidatorsMap {
fn from_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<Self, ValidationError<'a>>
where
Self: Sized,
{
compile_small_map(map, context)
}
#[inline]
fn get_validator(&self, property: &str) -> Option<&SchemaNode> {
for (prop, node) in self {
if prop == property {
return Some(node);
}
}
None
}
#[inline]
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> {
for (prop, node) in self {
if prop == property {
return Some((prop, node));
}
}
None
}
}
impl PropertiesValidatorsMap for BigValidatorsMap {
fn from_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<Self, ValidationError<'a>>
where
Self: Sized,
{
compile_big_map(map, context)
}
#[inline]
fn get_validator(&self, property: &str) -> Option<&SchemaNode> {
self.get(property)
}
#[inline]
fn get_key_validator(&self, property: &str) -> Option<(&String, &SchemaNode)> {
self.get_key_value(property)
}
}
pub(crate) fn compile_small_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<SmallValidatorsMap, ValidationError<'a>> {
let mut properties = Vec::with_capacity(map.len());
let keyword_context = context.with_path("properties");
for (key, subschema) in map {
let property_context = keyword_context.with_path(key.clone());
properties.push((
key.clone(),
compile_validators(subschema, &property_context)?,
));
}
Ok(properties)
}
pub(crate) fn compile_big_map<'a>(
map: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<BigValidatorsMap, ValidationError<'a>> {
let mut properties = AHashMap::with_capacity(map.len());
let keyword_context = context.with_path("properties");
for (key, subschema) in map {
let property_context = keyword_context.with_path(key.clone());
properties.insert(
key.clone(),
compile_validators(subschema, &property_context)?,
);
}
Ok(properties)
}
pub(crate) fn are_properties_valid<M, F>(prop_map: &M, props: &Map<String, Value>, check: F) -> bool
where
M: PropertiesValidatorsMap,
F: Fn(&Value) -> bool,
{
props.iter().all(|(property, instance)| {
if let Some(validator) = prop_map.get_validator(property) {
validator.is_valid(instance)
} else {
check(instance)
}
})
}
/// Create a vector of pattern-validators pairs.
#[inline]
pub(crate) fn compile_patterns<'a>(
obj: &'a Map<String, Value>,
context: &CompilationContext,
) -> Result<PatternedValidators, ValidationError<'a>> {
let keyword_context = context.with_path("patternProperties");
let mut compiled_patterns = Vec::with_capacity(obj.len());
for (pattern, subschema) in obj {
let pattern_context = keyword_context.with_path(pattern.to_string());
if let Ok(compiled_pattern) = Regex::new(pattern) {
let node = compile_validators(subschema, &pattern_context)?;
compiled_patterns.push((compiled_pattern, node));
} else {
return Err(ValidationError::format(
JSONPointer::default(),
keyword_context.clone().into_pointer(),
subschema,
"regex",
));
}
}
Ok(compiled_patterns)
}
macro_rules! compile_dynamic_prop_map_validator {
($validator:tt, $properties:ident, $( $arg:expr ),* $(,)*) => {{
if let Value::Object(map) = $properties {
if map.len() < 40 {
Some($validator::<SmallValidatorsMap>::compile(
map, $($arg, )*
))
} else {
Some($validator::<BigValidatorsMap>::compile(
map, $($arg, )*
))
}
} else {
Some(Err(ValidationError::null_schema()))
}
}};
}
pub(crate) use compile_dynamic_prop_map_validator;

View File

@ -50,6 +50,7 @@ impl Draft {
#[allow(clippy::match_same_arms)]
pub(crate) fn get_validator(self, keyword: &str) -> Option<CompileFunc> {
match keyword {
"$ref" => Some(keywords::ref_::compile),
"additionalItems" => Some(keywords::additional_items::compile),
"additionalProperties" => Some(keywords::additional_properties::compile),
"allOf" => Some(keywords::all_of::compile),
@ -167,6 +168,13 @@ impl Draft {
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::type_::compile),
},
"unevaluatedProperties" => match self {
#[cfg(feature = "draft201909")]
Draft::Draft201909 => Some(keywords::unevaluated_properties::compile),
#[cfg(feature = "draft202012")]
Draft::Draft202012 => Some(keywords::unevaluated_properties::compile),
_ => None,
},
"uniqueItems" => Some(keywords::unique_items::compile),
_ => None,
}

View File

@ -129,6 +129,15 @@ impl<'a> PartialApplication<'a> {
}
}
/// A shortcut to check whether the partial represents passed validation.
#[must_use]
pub(crate) const fn is_valid(&self) -> bool {
match self {
Self::Valid { .. } => true,
Self::Invalid { .. } => false,
}
}
/// Set the annotation that will be returned for the current validator. If this
/// `PartialApplication` is invalid then this method does nothing
pub(crate) fn annotate(&mut self, new_annotations: Annotations<'a>) {

View File

@ -13,8 +13,6 @@ use std::fs;
// These depend on the new `$defs` keyword (which is renamed from `definitions`)
r"id_0_[0-6]",
// Various types of new behavior used in the `$ref` context
"ref_5_1",
"ref_13_0",
"refRemote_4_0",
"refRemote_4_1",
"recursiveRef_0_3",
@ -35,7 +33,6 @@ use std::fs;
r"optional_format_duration_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/265
r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266
r"unevaluatedItems_.+",
r"unevaluatedProperties_.+",
}))]
#[cfg_attr(feature = "draft202012", json_schema_test_suite("tests/suite", "draft2020-12", {
r"optional_format_idn_hostname_0_\d+", // https://github.com/Stranger6667/jsonschema-rs/issues/101
@ -43,8 +40,6 @@ use std::fs;
// These depend on the new `$defs` keyword (which is renamed from `definitions`)
r"id_0_[0-6]",
// Various types of new behavior used in the `$ref` context
"ref_5_1",
"ref_13_0",
"refRemote_4_0",
"refRemote_4_1",
"recursiveRef_0_3",
@ -72,7 +67,6 @@ use std::fs;
r"optional_format_uri_reference_.+",
r"optional_format_uri_template_.+",
r"unevaluatedItems_.+",
r"unevaluatedProperties_.+",
}))]
fn test_draft(_server_address: &str, test_case: TestCase) {
let draft_version = match test_case.draft_version.as_ref() {
@ -91,7 +85,7 @@ fn test_draft(_server_address: &str, test_case: TestCase) {
.with_meta_schemas()
.should_validate_formats(true)
.compile(&test_case.schema)
.unwrap();
.expect("should not fail to compile schema");
let result = compiled.validate(&test_case.instance);
@ -100,50 +94,72 @@ fn test_draft(_server_address: &str, test_case: TestCase) {
let first_error = errors_iterator.next();
assert!(
first_error.is_none(),
"Schema: {}\nInstance: {}\nError: {:?}",
"Test case should not have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
first_error,
);
}
if !compiled.is_valid(&test_case.instance) {
panic!(
"Schema: {}\nInstance: {}\nError: It is supposed to be VALID!",
test_case.schema, test_case.instance,
);
}
let output = compiled.apply(&test_case.instance).basic();
if !output.is_valid() {
panic!(
"Schema: {}\nInstance: {}\nError: {:?}",
test_case.schema, test_case.instance, output
);
}
} else {
assert!(
result.is_err(),
"Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!",
compiled.is_valid(&test_case.instance),
"Test case should be valid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
);
let errors: Vec<_> = result.expect_err("Errors").collect();
let output = compiled.apply(&test_case.instance).basic();
assert!(
output.is_valid(),
"Test case should be valid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
output
);
} else {
assert!(
result.is_err(),
"Test case should have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
);
let errors = result.unwrap_err();
for error in errors {
let pointer = error.instance_path.to_string();
assert_eq!(test_case.instance.pointer(&pointer), Some(&*error.instance))
}
if compiled.is_valid(&test_case.instance) {
panic!(
"Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!",
test_case.schema, test_case.instance,
assert_eq!(
test_case.instance.pointer(&pointer), Some(&*error.instance),
"Expected error instance did not match actual error instance:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
&*error.instance,
&pointer,
);
}
assert!(
!compiled.is_valid(&test_case.instance),
"Test case should be invalid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
);
let output = compiled.apply(&test_case.instance).basic();
if output.is_valid() {
panic!(
"Schema: {}\nInstance: {}\nError: It is supposed to be INVALID!",
test_case.schema, test_case.instance,
);
}
assert!(
!output.is_valid(),
"Test case should be invalid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}",
test_case.group_description,
test_case.test_case_description,
test_case.schema,
test_case.instance,
);
}
}