diff --git a/src/lib.rs b/src/lib.rs index 8f95b3a..0f56329 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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) { } } +// Filters collected errors, e.g., deduplicating by instance_path +fn filter_boon_errors(raw_errors: Vec) -> Vec { + 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 = 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(); diff --git a/src/tests.rs b/src/tests.rs index e0ff638..5bb39ff 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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]