use json_structure::{InstanceValidator, SchemaValidator};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
fn find_test_assets_dir() -> Option<PathBuf> {
let possible_paths = [
PathBuf::from("../test-assets"), PathBuf::from("../../test-assets"), PathBuf::from("test-assets"), ];
for path in &possible_paths {
if path.exists() && path.is_dir() {
return Some(path.clone());
}
}
None
}
fn find_samples_dir() -> Option<PathBuf> {
let possible_paths = [
PathBuf::from("../primer-and-samples/samples/core"),
PathBuf::from("../../primer-and-samples/samples/core"),
PathBuf::from("primer-and-samples/samples/core"),
];
for path in &possible_paths {
if path.exists() && path.is_dir() {
return Some(path.clone());
}
}
None
}
fn load_json(path: &Path) -> Option<Value> {
let content = fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
fn get_schema_files(dir: &Path) -> Vec<PathBuf> {
if !dir.exists() {
return vec![];
}
fs::read_dir(dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map_or(false, |ext| ext == "json"))
.filter(|p| p.to_string_lossy().ends_with(".struct.json"))
.collect()
})
.unwrap_or_default()
}
fn get_json_files(dir: &Path) -> Vec<PathBuf> {
if !dir.exists() {
return vec![];
}
fs::read_dir(dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map_or(false, |ext| ext == "json"))
.collect()
})
.unwrap_or_default()
}
fn get_subdirs(dir: &Path) -> Vec<PathBuf> {
if !dir.exists() {
return vec![];
}
fs::read_dir(dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect()
})
.unwrap_or_default()
}
fn get_test_name(path: &Path) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.replace(".struct", ""))
.unwrap_or_else(|| "unknown".to_string())
}
#[test]
fn test_invalid_schemas() {
let test_assets = match find_test_assets_dir() {
Some(dir) => dir,
None => {
eprintln!("test-assets directory not found, skipping test");
return;
}
};
let invalid_schemas_dir = test_assets.join("schemas").join("invalid");
let schema_files = get_schema_files(&invalid_schemas_dir);
if schema_files.is_empty() {
eprintln!("No invalid schema files found");
return;
}
let validator = SchemaValidator::new();
let mut passed = 0;
let mut failed = 0;
for schema_file in &schema_files {
let test_name = get_test_name(schema_file);
let schema_json = match fs::read_to_string(schema_file) {
Ok(s) => s,
Err(e) => {
eprintln!(" ✗ {} - failed to read: {}", test_name, e);
failed += 1;
continue;
}
};
let result = validator.validate(&schema_json);
if !result.is_valid() {
passed += 1;
} else {
eprintln!(" ✗ {} - should be invalid but passed validation", test_name);
failed += 1;
}
}
println!("Invalid Schema Tests: {} passed, {} failed", passed, failed);
}
#[test]
fn test_warning_schemas() {
let test_assets = match find_test_assets_dir() {
Some(dir) => dir,
None => {
eprintln!("test-assets directory not found, skipping test");
return;
}
};
let warning_schemas_dir = test_assets.join("schemas").join("warnings");
let schema_files = get_schema_files(&warning_schemas_dir);
if schema_files.is_empty() {
eprintln!("No warning schema files found");
return;
}
let mut validator = SchemaValidator::new();
validator.set_extended(true);
validator.set_warn_on_extension_keywords(true);
let mut passed = 0;
let mut failed = 0;
for schema_file in &schema_files {
let test_name = get_test_name(schema_file);
let has_uses = test_name.contains("with-uses");
let schema_json = match fs::read_to_string(schema_file) {
Ok(s) => s,
Err(e) => {
eprintln!(" ✗ {} - failed to read: {}", test_name, e);
failed += 1;
continue;
}
};
let result = validator.validate(&schema_json);
if !result.is_valid() {
eprintln!(" ✗ {} - should be valid but failed: {:?}", test_name, result.errors().next());
failed += 1;
continue;
}
let has_warnings = result.warning_count() > 0;
if has_uses {
if has_warnings {
eprintln!(" ✗ {} - should not have warnings (has $uses)", test_name);
failed += 1;
} else {
passed += 1;
}
} else {
if has_warnings {
passed += 1;
} else {
eprintln!(" ✗ {} - should have warnings (no $uses)", test_name);
failed += 1;
}
}
}
println!("Warning Schema Tests: {} passed, {} failed", passed, failed);
}
#[test]
fn test_validation_instances() {
let test_assets = match find_test_assets_dir() {
Some(dir) => dir,
None => {
eprintln!("test-assets directory not found, skipping test");
return;
}
};
let validation_instances_dir = test_assets.join("instances").join("validation");
let validation_schemas_dir = test_assets.join("schemas").join("validation");
let instance_dirs = get_subdirs(&validation_instances_dir);
if instance_dirs.is_empty() {
eprintln!("No validation instance directories found");
return;
}
let mut passed = 0;
let mut failed = 0;
for instance_dir in &instance_dirs {
let category_name = get_test_name(instance_dir);
let schema_file = validation_schemas_dir.join(format!("{}.struct.json", category_name));
if !schema_file.exists() {
eprintln!(" - Skipping {} - no schema found", category_name);
continue;
}
let schema = match load_json(&schema_file) {
Some(s) => s,
None => {
eprintln!(" ✗ {} - failed to load schema", category_name);
failed += 1;
continue;
}
};
let mut validator = InstanceValidator::new();
validator.set_extended(true);
let instance_files = get_json_files(instance_dir);
for instance_file in &instance_files {
let test_name = get_test_name(instance_file);
let instance_data = match load_json(instance_file) {
Some(d) => d,
None => {
eprintln!(" ✗ {}/{} - failed to load instance", category_name, test_name);
failed += 1;
continue;
}
};
let expected_valid = instance_data.get("_expectedValid")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let _expected_error = instance_data.get("_expectedError")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let instance = prepare_instance_for_validation(&instance_data, &schema);
let instance_json = serde_json::to_string(&instance).unwrap();
let result = validator.validate(&instance_json, &schema);
if expected_valid {
if result.is_valid() {
passed += 1;
} else {
eprintln!(" ✗ {}/{} - expected valid but got errors: {:?}",
category_name, test_name, result.errors().next());
failed += 1;
}
} else {
if !result.is_valid() {
passed += 1;
} else {
eprintln!(" ✗ {}/{} - expected invalid but passed", category_name, test_name);
failed += 1;
}
}
}
}
println!("Validation Instance Tests: {} passed, {} failed", passed, failed);
}
fn prepare_instance_for_validation(instance: &Value, schema: &Value) -> Value {
let mut cleaned = instance.clone();
if let Some(obj) = cleaned.as_object_mut() {
obj.remove("_description");
obj.remove("_expectedError");
obj.remove("_expectedValid");
obj.remove("$schema");
}
let schema_type = schema.get("type").and_then(|t| t.as_str()).unwrap_or("");
let value_wrapper_types = [
"string", "number", "integer", "boolean", "int8", "uint8", "int16", "uint16",
"int32", "uint32", "float", "double", "decimal", "float8", "array", "set"
];
if value_wrapper_types.contains(&schema_type) {
if let Some(obj) = cleaned.as_object() {
if obj.len() == 1 && obj.contains_key("value") {
return obj.get("value").cloned().unwrap_or(cleaned);
}
}
}
cleaned
}
#[test]
fn test_invalid_instances() {
let test_assets = match find_test_assets_dir() {
Some(dir) => dir,
None => {
eprintln!("test-assets directory not found, skipping test");
return;
}
};
let samples_dir = match find_samples_dir() {
Some(dir) => dir,
None => {
eprintln!("samples directory not found, skipping test");
return;
}
};
let invalid_instances_dir = test_assets.join("instances").join("invalid");
let instance_dirs = get_subdirs(&invalid_instances_dir);
if instance_dirs.is_empty() {
eprintln!("No invalid instance directories found");
return;
}
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
for instance_dir in &instance_dirs {
let category_name = get_test_name(instance_dir);
let schema_file = samples_dir.join(&category_name).join("schema.struct.json");
if !schema_file.exists() {
eprintln!(" - Skipping {} - no schema found at {:?}", category_name, schema_file);
skipped += 1;
continue;
}
let schema = match load_json(&schema_file) {
Some(s) => s,
None => {
eprintln!(" ✗ {} - failed to load schema", category_name);
failed += 1;
continue;
}
};
let needs_extended = has_extended_keywords(&schema);
let mut validator = InstanceValidator::new();
validator.set_extended(needs_extended);
let instance_files = get_json_files(instance_dir);
for instance_file in &instance_files {
let test_name = get_test_name(instance_file);
let instance = match load_json(instance_file) {
Some(d) => d,
None => {
eprintln!(" ✗ {}/{} - failed to load instance", category_name, test_name);
failed += 1;
continue;
}
};
let instance_json = serde_json::to_string(&instance).unwrap();
let result = validator.validate(&instance_json, &schema);
if !result.is_valid() {
passed += 1;
} else {
eprintln!(" ✗ {}/{} - should be invalid but passed", category_name, test_name);
failed += 1;
}
}
}
println!("Invalid Instance Tests: {} passed, {} failed, {} skipped", passed, failed, skipped);
}
fn has_extended_keywords(value: &Value) -> bool {
let extended_keywords = [
"minLength",
"maxLength",
"pattern",
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
"minItems",
"maxItems",
"uniqueItems",
"minProperties",
"maxProperties",
"allOf",
"anyOf",
"oneOf",
"not",
"if",
"then",
"else",
"$extends",
];
match value {
Value::Object(obj) => {
for key in obj.keys() {
if extended_keywords.contains(&key.as_str()) {
return true;
}
}
for v in obj.values() {
if has_extended_keywords(v) {
return true;
}
}
false
}
Value::Array(arr) => arr.iter().any(has_extended_keywords),
_ => false,
}
}
#[test]
fn test_sample_schemas_are_valid() {
let samples_dir = match find_samples_dir() {
Some(dir) => dir,
None => {
eprintln!("samples directory not found, skipping test");
return;
}
};
let validator = SchemaValidator::new();
let mut passed = 0;
let mut failed = 0;
let sample_dirs = get_subdirs(&samples_dir);
for sample_dir in &sample_dirs {
let schema_file = sample_dir.join("schema.struct.json");
if !schema_file.exists() {
continue;
}
let test_name = get_test_name(sample_dir);
let schema_json = match fs::read_to_string(&schema_file) {
Ok(s) => s,
Err(e) => {
eprintln!(" ✗ {} - failed to read: {}", test_name, e);
failed += 1;
continue;
}
};
let result = validator.validate(&schema_json);
if result.is_valid() {
passed += 1;
} else {
eprintln!(
" ✗ {} - should be valid: {:?}",
test_name,
result.errors().next()
);
failed += 1;
}
}
println!("Sample Schema Tests: {} passed, {} failed", passed, failed);
}
#[test]
fn test_sample_instances_are_valid() {
let samples_dir = match find_samples_dir() {
Some(dir) => dir,
None => {
eprintln!("samples directory not found, skipping test");
return;
}
};
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
let sample_dirs = get_subdirs(&samples_dir);
for sample_dir in &sample_dirs {
let schema_file = sample_dir.join("schema.struct.json");
if !schema_file.exists() {
continue;
}
let category_name = get_test_name(sample_dir);
let schema = match load_json(&schema_file) {
Some(s) => s,
None => {
eprintln!(" ✗ {} - failed to load schema", category_name);
failed += 1;
continue;
}
};
let instance_files: Vec<PathBuf> = fs::read_dir(sample_dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
let name = p.file_name().unwrap_or_default().to_string_lossy();
name.starts_with("example") && name.ends_with(".json")
})
.collect()
})
.unwrap_or_default();
if instance_files.is_empty() {
skipped += 1;
continue;
}
let mut validator = InstanceValidator::new();
validator.set_extended(true);
for instance_file in &instance_files {
let instance_name = instance_file.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let instance_data = match load_json(instance_file) {
Some(d) => d,
None => {
eprintln!(" ✗ {}/{} - failed to load instance", category_name, instance_name);
failed += 1;
continue;
}
};
let instance = prepare_instance_for_validation(&instance_data, &schema);
let instance_json = serde_json::to_string(&instance).unwrap();
let result = validator.validate(&instance_json, &schema);
if result.is_valid() {
passed += 1;
} else {
eprintln!(
" ✗ {}/{} - should be valid: {:?}",
category_name,
instance_name,
result.errors().next()
);
failed += 1;
}
}
}
println!("Sample Instance Tests: {} passed, {} failed, {} skipped", passed, failed, skipped);
assert_eq!(failed, 0, "Some sample instances failed validation");
}
#[test]
fn test_invalid_schema_error_accuracy() {
let test_assets = match find_test_assets_dir() {
Some(dir) => dir,
None => {
eprintln!("test-assets directory not found, skipping test");
return;
}
};
let invalid_schemas_dir = test_assets.join("schemas").join("invalid");
let validator = SchemaValidator::new();
let expected_errors: std::collections::HashMap<&str, &str> = [
("unknown-type", "SCHEMA_TYPE_INVALID"),
("enum-empty", "SCHEMA_ENUM_EMPTY"),
("enum-duplicates", "SCHEMA_ENUM_DUPLICATE"),
("circular-ref-direct", "SCHEMA_REF_CIRCULAR"),
("ref-undefined", "SCHEMA_REF_NOT_FOUND"),
("array-missing-items", "SCHEMA_ARRAY_MISSING_ITEMS"),
("map-missing-values", "SCHEMA_MAP_MISSING_VALUES"),
("missing-type", "SCHEMA_ROOT_MISSING_TYPE"),
("required-missing-property", "SCHEMA_REQUIRED_PROPERTY_NOT_DEFINED"),
("required-not-array", "SCHEMA_REQUIRED_MUST_BE_ARRAY"),
("properties-not-object", "SCHEMA_PROPERTIES_MUST_BE_OBJECT"),
("defs-not-object", "SCHEMA_DEFINITIONS_MUST_BE_OBJECT"),
("allof-not-array", "SCHEMA_ALLOF_NOT_ARRAY"),
("tuple-missing-definition", "SCHEMA_TUPLE"),
("tuple-missing-prefixitems", "SCHEMA_TUPLE"),
].iter().cloned().collect();
let mut tested = 0;
let mut accurate = 0;
for (filename, expected_code) in &expected_errors {
let schema_file = invalid_schemas_dir.join(format!("{}.struct.json", filename));
if !schema_file.exists() {
continue;
}
tested += 1;
let schema_json = fs::read_to_string(&schema_file).unwrap();
let result = validator.validate(&schema_json);
if !result.is_valid() {
let has_expected = result.errors().any(|e| e.code().contains(expected_code));
if has_expected {
accurate += 1;
} else {
let codes: Vec<_> = result.errors().map(|e| e.code().to_string()).collect();
eprintln!(" ✗ {} - expected error containing '{}', got: {:?}",
filename, expected_code, codes);
}
} else {
eprintln!(" ✗ {} - should be invalid but passed", filename);
}
}
println!("Error Accuracy Tests: {} tested, {} accurate", tested, accurate);
assert_eq!(tested, accurate, "Some error codes were not as expected");
}
#[test]
fn test_warning_schema_message_accuracy() {
let test_assets = match find_test_assets_dir() {
Some(dir) => dir,
None => {
eprintln!("test-assets directory not found, skipping test");
return;
}
};
let warning_schemas_dir = test_assets.join("schemas").join("warnings");
let mut validator = SchemaValidator::new();
validator.set_extended(true);
validator.set_warn_on_extension_keywords(true);
let expected_warnings: std::collections::HashMap<&str, &str> = [
("string-pattern-without-uses", "pattern"),
("string-minlength-without-uses", "minLength"),
("numeric-minimum-without-uses", "minimum"),
("numeric-maximum-without-uses", "maximum"),
("numeric-exclusive-minimum-without-uses", "exclusiveMinimum"),
("numeric-exclusive-maximum-without-uses", "exclusiveMaximum"),
("numeric-multiple-of-without-uses", "multipleOf"),
("array-minitems-without-uses", "minItems"),
("array-maxitems-without-uses", "maxItems"),
("array-uniqueitems-without-uses", "uniqueItems"),
("array-contains-without-uses", "contains"),
("object-minproperties-without-uses", "minProperties"),
("object-maxproperties-without-uses", "maxProperties"),
("object-dependentrequired-without-uses", "dependentRequired"),
("object-patternproperties-without-uses", "patternProperties"),
("object-propertynames-without-uses", "propertyNames"),
].iter().cloned().collect();
let mut tested = 0;
let mut accurate = 0;
for (filename, expected_keyword) in &expected_warnings {
let schema_file = warning_schemas_dir.join(format!("{}.struct.json", filename));
if !schema_file.exists() {
continue;
}
tested += 1;
let schema_json = fs::read_to_string(&schema_file).unwrap();
let result = validator.validate(&schema_json);
if result.is_valid() && result.warning_count() > 0 {
let has_expected = result.warnings().any(|w| {
w.message().contains(expected_keyword) || w.path().contains(expected_keyword)
});
if has_expected {
accurate += 1;
} else {
let warnings: Vec<_> = result.warnings()
.map(|w| format!("{}: {}", w.path(), w.message()))
.collect();
eprintln!(" ✗ {} - expected warning about '{}', got: {:?}",
filename, expected_keyword, warnings);
}
} else if !result.is_valid() {
eprintln!(" ✗ {} - should be valid with warnings but got errors", filename);
} else {
eprintln!(" ✗ {} - expected warnings but got none", filename);
}
}
println!("Warning Accuracy Tests: {} tested, {} accurate", tested, accurate);
assert_eq!(tested, accurate, "Some warning messages were not as expected");
}