branch error filtering

This commit is contained in:
2025-04-21 16:11:12 -04:00
parent b7f528d1f6
commit 3988308965
2 changed files with 60 additions and 17 deletions

View File

@ -91,13 +91,16 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
match cache.schemas.validate(&instance_value, *sch_index) {
Ok(_) => JsonB(json!({ "success": true })),
Err(validation_error) => {
// Directly use the result of format_validation_error
// which now includes the top-level success indicator and flat error list
let mut error_list = Vec::new();
collect_leaf_errors(&validation_error, &mut error_list);
// Collect all leaf errors first
let mut raw_error_list = Vec::new();
collect_leaf_errors(&validation_error, &mut raw_error_list);
// Filter the errors (e.g., deduplicate by instance_path)
let filtered_error_list = filter_boon_errors(raw_error_list);
JsonB(json!({
"success": false,
"error": error_list // Flat list of specific errors
"error": filtered_error_list // Return the filtered list
}))
}
}
@ -127,6 +130,31 @@ fn collect_leaf_errors(error: &ValidationError, errors_list: &mut Vec<Value>) {
}
}
// Filters collected errors, e.g., deduplicating by instance_path
fn filter_boon_errors(raw_errors: Vec<Value>) -> Vec<Value> {
use std::collections::HashMap;
use std::collections::hash_map::Entry;
// Use a HashMap to keep only the first error for each instance_path
let mut unique_errors: HashMap<String, Value> = HashMap::new();
for error_value in raw_errors {
if let Some(instance_path_value) = error_value.get("instance_path") {
if let Some(instance_path_str) = instance_path_value.as_str() {
// Use Entry API to insert only if the key is not present
if let Entry::Vacant(entry) = unique_errors.entry(instance_path_str.to_string()) {
entry.insert(error_value);
}
}
}
// If error doesn't have instance_path or it's not a string, we might ignore it or handle differently.
// For now, we implicitly ignore errors without a valid string instance_path for deduplication.
}
// Collect the unique errors from the map values
unique_errors.into_values().collect()
}
#[pg_extern(strict, parallel_safe)]
fn json_schema_cached(schema_id: &str) -> bool {
let cache = SCHEMA_CACHE.read().unwrap();

View File

@ -333,32 +333,47 @@ fn test_validate_json_schema_oneof_validation_errors() {
// --- Test case 1: Fails string maxLength (in branch 0) AND missing number_prop (in branch 1) ---
let invalid_string_instance = json!({ "string_prop": "toolongstring" });
let result_invalid_string = validate_json_schema(schema_id, jsonb(invalid_string_instance));
// Expect 2 leaf errors: one for maxLength (branch 0), one for missing prop (branch 1)
// Check the first error message reported by boon (maxLength).
assert_failure_with_json!(result_invalid_string, 2, "length must be <=5", "Validation with invalid string length should have 2 leaf errors");
// Expect 2 leaf errors. Check count only with the macro.
assert_failure_with_json!(result_invalid_string, 2);
// Explicitly check that both expected errors are present, ignoring order
let errors_string = result_invalid_string.0["error"].as_array().expect("Expected error array for invalid string");
assert!(errors_string.iter().any(|e| e["instance_path"] == "/string_prop" && e["message"].as_str().unwrap().contains("length must be <=5")), "Missing maxLength error");
assert!(errors_string.iter().any(|e| e["instance_path"] == "" && e["message"].as_str().unwrap().contains("missing properties 'number_prop'")), "Missing number_prop required error");
// --- Test case 2: Fails number minimum (in branch 1) AND missing string_prop (in branch 0) ---
let invalid_number_instance = json!({ "number_prop": 5 });
let result_invalid_number = validate_json_schema(schema_id, jsonb(invalid_number_instance));
// Expect 2 leaf errors: one for minimum (branch 1), one for missing prop (branch 0)
// Check the first error message reported by boon (missing prop).
assert_failure_with_json!(result_invalid_number, 2, "missing properties 'string_prop'", "Validation with invalid number should have 2 leaf errors");
// Expect 2 leaf errors. Check count only with the macro.
assert_failure_with_json!(result_invalid_number, 2);
// Explicitly check that both expected errors are present, ignoring order
let errors_number = result_invalid_number.0["error"].as_array().expect("Expected error array for invalid number");
assert!(errors_number.iter().any(|e| e["instance_path"] == "/number_prop" && e["message"].as_str().unwrap().contains("must be >=10")), "Missing minimum error");
assert!(errors_number.iter().any(|e| e["instance_path"] == "" && e["message"].as_str().unwrap().contains("missing properties 'string_prop'")), "Missing string_prop required error");
// --- Test case 3: Fails type check (not object) for both branches ---
// Input: boolean, expected object for both branches
let invalid_bool_instance = json!(true); // Not an object
let result_invalid_bool = validate_json_schema(schema_id, jsonb(invalid_bool_instance));
// Expect 2 leaf errors, one "Type" error for each branch
// Check the first error reported by boon (want object).
assert_failure_with_json!(result_invalid_bool, 2, "want object", "Validation with invalid bool should have 2 leaf errors");
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
assert_failure_with_json!(result_invalid_bool, 1);
// Explicitly check that the single remaining error is the type error for the root instance path
let errors_bool = result_invalid_bool.0["error"].as_array().expect("Expected error array for invalid bool");
assert_eq!(errors_bool.iter().filter(|e| e["instance_path"] == "" && e["message"].as_str().unwrap().contains("want object")).count(), 1, "Expected one 'want object' error at root after filtering");
// --- Test case 4: Fails missing required for both branches ---
// Input: empty object, expected string_prop (branch 0) OR number_prop (branch 1)
let invalid_empty_obj = json!({});
let result_empty_obj = validate_json_schema(schema_id, jsonb(invalid_empty_obj));
// Expect 2 leaf errors: one required error for branch 0, one required error for branch 1
// Check the first error reported by boon (missing string_prop).
assert_failure_with_json!(result_empty_obj, 2, "missing properties 'string_prop'", "Validation with empty object should have 2 leaf errors");
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
assert_failure_with_json!(result_empty_obj, 1);
// Explicitly check that the single remaining error is one of the expected missing properties errors
let errors_empty = result_empty_obj.0["error"].as_array().expect("Expected error array for empty object");
assert_eq!(errors_empty.len(), 1, "Expected exactly one error after filtering empty object");
let the_error = &errors_empty[0];
assert_eq!(the_error["instance_path"], "", "Expected instance_path to be empty string");
let message = the_error["message"].as_str().unwrap();
assert!(message.contains("missing properties 'string_prop'") || message.contains("missing properties 'number_prop'"),
"Error message should indicate missing string_prop or number_prop, got: {}", message);
}
#[pg_test]