jspg additional properties bug squashed

This commit is contained in:
2025-09-30 19:56:34 -04:00
parent cc04f38c14
commit d6b34c99bb
26 changed files with 6340 additions and 6328 deletions

2
Cargo.lock generated
View File

@ -243,6 +243,8 @@ dependencies = [
"idna",
"once_cell",
"percent-encoding",
"pgrx",
"pgrx-tests",
"regex",
"regex-syntax",
"rustls",

View File

@ -15,19 +15,11 @@ It works by:
The version of `boon` located in the `validator/` directory has been modified to address specific requirements of the `jspg` project. The key deviations from the upstream `boon` crate are as follows:
### 1. Correct Unevaluated Property Propagation in `$ref`
### 1. Recursive Runtime Strictness Control
- **Problem:** In the original `boon` implementation, if a schema validation failed inside a `$ref`, the set of properties that had been evaluated by that referenced schema was not correctly propagated back up to the parent validator. This caused the parent to incorrectly flag already-evaluated properties as "unevaluated," leading to spurious `unevaluatedProperties` errors.
- **Problem:** The `jspg` project requires that certain schemas enforce a strict "no extra properties" policy (specifically, schemas for public `puncs` and global `type`s). This strictness needs to cascade through the entire validation hierarchy, including all nested objects and `$ref` chains. A compile-time flag was unsuitable because it would incorrectly apply strictness to shared, reusable schemas.
- **Solution:** The `Uneval::merge` function in `validator/src/validator.rs` was modified. The original logic, which performed an *intersection* of unevaluated properties (`retain`), was replaced with a direct *assignment*. Now, the parent validator's set of unevaluated properties is completely replaced by the final set from the child validator. This ensures that the most current state of evaluated properties is always passed up the chain, regardless of validation success or failure within the `$ref`.
### 2. Runtime Strictness Control
- **Problem:** The `jspg` project requires that certain schemas (e.g., those for public `puncs`) enforce a strict "no extra properties" policy, while others do not. This strictness needs to cascade through the entire validation hierarchy, including all `$ref` chains. A compile-time flag was unsuitable because it would incorrectly apply strictness to shared, reusable schemas.
- **Solution:** A runtime validation option was implemented.
1. A `ValidationOptions { be_strict: bool }` struct was added and is passed to the core `validate` function in `validator.rs`.
2. The `jspg` code determines whether a validation run should be strict (based on the `punc`'s `public` flag or if we are validating a a global `type`) and passes the appropriate option.
3. The `Validator` struct carries these options through the entire recursive validation process.
4. The `uneval_validate` function was modified to only enforce this strict check if `options.be_strict` is `true` **and** it is at the root of the validation scope (`self.scope.parent.is_none()`). This ensures the check only happens at the very end of a top-level validation, after all `$ref`s and sub-schemas have been processed.
5. When this runtime strictness check fails, it now generates a more descriptive `ADDITIONAL_PROPERTIES_NOT_ALLOWED` error, rather than a generic `FALSE_SCHEMA` error.
- **Solution:** A runtime validation option was implemented to enforce strictness recursively.
1. A `ValidationOptions { be_strict: bool }` struct was added. The `jspg` code in `src/lib.rs` determines whether a validation run should be strict (based on the `punc`'s `public` flag or if validating a global `type`) and passes the appropriate option to the validator.
2. The `be_strict` option is propagated through the entire recursive validation process. A bug was fixed in `_validate_self` (which handles `$ref`s) to ensure that the sub-validator is always initialized to track unevaluated properties when `be_strict` is enabled. Previously, tracking was only initiated if the parent was already tracking unevaluated properties, causing strictness to be dropped across certain `$ref` boundaries.
3. At any time, if `unevaluatedProperties` or `additionalProperties` is found in the schema, it should override the strict (or non-strict) validation at that level.

44
out.txt Normal file
View File

@ -0,0 +1,44 @@
running 23 tests
 Building extension with features pg_test pg17
 Running command "/opt/homebrew/bin/cargo" "build" "--lib" "--features" "pg_test pg17" "--message-format=json-render-diagnostics"
 Installing extension
 Copying control file to /opt/homebrew/share/postgresql@17/extension/jspg.control
 Copying shared library to /opt/homebrew/lib/postgresql@17/jspg.dylib
 Finished installing jspg
test tests::pg_test_cache_invalid ... ok
test tests::pg_test_validate_nested_req_deps ... ok
test tests::pg_test_validate_format_empty_string_with_ref ... ok
test tests::pg_test_validate_format_normal ... ok
test tests::pg_test_validate_format_empty_string ... ok
test tests::pg_test_validate_dependencies ... ok
test tests::pg_test_validate_dependencies_merging ... ok
test tests::pg_test_validate_additional_properties ... ok
test tests::pg_test_validate_enum_schema ... ok
test tests::pg_test_validate_errors ... ok
test tests::pg_test_validate_not_cached ... ok
test tests::pg_test_validate_oneof ... ok
test tests::pg_test_validate_punc_with_refs ... ok
test tests::pg_test_validate_property_merging ... ok
test tests::pg_test_validate_punc_local_refs ... ok
test tests::pg_test_validate_required_merging ... ok
test tests::pg_test_validate_required ... ok
test tests::pg_test_validate_simple ... ok
test tests::pg_test_validate_root_types ... ok
test tests::pg_test_validate_strict ... ok
test tests::pg_test_validate_title_override ... ok
test tests::pg_test_validate_unevaluated_properties ... ok
test tests::pg_test_validate_type_matching ... ok
test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.66s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

View File

@ -2,7 +2,7 @@ use pgrx::*;
pg_module_magic!();
use boon::{CompileError, Compiler, ErrorKind, SchemaIndex, Schemas, ValidationError, Type, Types};
use boon::{CompileError, Compiler, ErrorKind, SchemaIndex, Schemas, ValidationError, Type, Types, ValidationOptions};
use lazy_static::lazy_static;
use serde_json::{json, Value, Number};
use std::borrow::Cow;
@ -143,14 +143,10 @@ fn cache_json_schemas(enums: JsonB, types: JsonB, puncs: JsonB) -> JsonB {
fn add_schema_resource(
compiler: &mut Compiler,
schema_id: &str,
mut schema_value: Value,
schema_type: SchemaType,
schema_value: Value,
_schema_type: SchemaType,
errors: &mut Vec<Value>
) {
match schema_type {
SchemaType::Enum | SchemaType::PrivatePunc => {},
SchemaType::Type | SchemaType::PublicPunc => apply_strict_validation(&mut schema_value, schema_type),
}
if let Err(e) = compiler.add_resource(schema_id, schema_value) {
errors.push(json!({
"code": "SCHEMA_RESOURCE_FAILED",
@ -193,51 +189,6 @@ fn compile_all_schemas(
}
}
// Helper function to apply strict validation to a schema
//
// This recursively adds unevaluatedProperties: false to object-type schemas,
// but SKIPS schemas inside if/then/else to avoid breaking conditional validation.
// For type schemas, it skips the top level to allow inheritance.
fn apply_strict_validation(schema: &mut Value, schema_type: SchemaType) {
apply_strict_validation_recursive(schema, false, schema_type, true);
}
fn apply_strict_validation_recursive(schema: &mut Value, inside_conditional: bool, schema_type: SchemaType, is_top_level: bool) {
match schema {
Value::Object(map) => {
// Skip adding strict validation if we're inside a conditional
// OR if we're at the top level of a type schema (types should be extensible)
let skip_strict = inside_conditional || (matches!(schema_type, SchemaType::Type) && is_top_level);
if !skip_strict {
// Apply unevaluatedProperties: false to schemas that have $ref OR type: "object"
let has_ref = map.contains_key("$ref");
let has_object_type = map.get("type").and_then(|v| v.as_str()) == Some("object");
if (has_ref || has_object_type) && !map.contains_key("unevaluatedProperties") && !map.contains_key("additionalProperties") {
// Use unevaluatedProperties: false to prevent extra properties
// This considers all evaluated properties from all schemas including refs
map.insert("unevaluatedProperties".to_string(), Value::Bool(false));
}
}
// Recurse into all properties
for (key, value) in map.iter_mut() {
// Mark when we're inside conditional branches
let in_conditional = inside_conditional || matches!(key.as_str(), "if" | "then" | "else");
apply_strict_validation_recursive(value, in_conditional, schema_type, false)
}
}
Value::Array(arr) => {
// Recurse into array items
for item in arr.iter_mut() {
apply_strict_validation_recursive(item, inside_conditional, schema_type, false);
}
}
_ => {}
}
}
fn walk_and_validate_refs(
instance: &Value,
schema: &Value,
@ -352,7 +303,12 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
})),
Some(schema) => {
let instance_value: Value = instance.0;
match cache.schemas.validate(&instance_value, schema.index) {
let options = match schema.t {
SchemaType::Type | SchemaType::PublicPunc => Some(ValidationOptions { be_strict: true }),
_ => None,
};
match cache.schemas.validate(&instance_value, schema.index, options.as_ref()) {
Ok(_) => {
let mut custom_errors = Vec::new();
if schema.t == SchemaType::Type || schema.t == SchemaType::PublicPunc || schema.t == SchemaType::PrivatePunc {
@ -390,10 +346,14 @@ fn collect_errors(error: &ValidationError, errors_list: &mut Vec<Error>) {
ErrorKind::Group | ErrorKind::AllOf | ErrorKind::AnyOf | ErrorKind::Not | ErrorKind::OneOf(_)
);
if error.causes.is_empty() && !is_structural {
let base_path = error.instance_location.to_string();
if !error.causes.is_empty() || is_structural {
for cause in &error.causes {
collect_errors(cause, errors_list);
}
return
}
// Match on error kind and handle each type
let base_path = error.instance_location.to_string();
let errors_to_add = match &error.kind {
ErrorKind::Type { got, want } => handle_type_error(&base_path, got, want),
ErrorKind::Required { want } => handle_required_error(&base_path, want),
@ -435,16 +395,7 @@ fn collect_errors(error: &ValidationError, errors_list: &mut Vec<Error>) {
ErrorKind::OneOf(matched) => handle_one_of_error(&base_path, matched),
};
// Add all generated errors
for error in errors_to_add {
errors_list.push(error);
}
} else {
// Recurse into causes
for cause in &error.causes {
collect_errors(cause, errors_list);
}
}
errors_list.extend(errors_to_add);
}
// Handler functions for each error kind
@ -512,21 +463,21 @@ fn handle_dependency_error(base_path: &str, prop: &str, missing: &[&str], is_dep
}
fn handle_additional_properties_error(base_path: &str, got: &[Cow<str>]) -> Vec<Error> {
// Create a separate error for each additional property that's not allowed
got.iter().map(|extra_prop| {
let mut errors = Vec::new();
for extra_prop in got {
let field_path = if base_path.is_empty() {
format!("/{}", extra_prop)
} else {
format!("{}/{}", base_path, extra_prop)
};
Error {
errors.push(Error {
path: field_path,
code: "ADDITIONAL_PROPERTIES_NOT_ALLOWED".to_string(),
message: format!("Property '{}' is not allowed", extra_prop),
cause: json!({ "got": [extra_prop.to_string()] }),
});
}
}).collect()
errors
}
fn handle_enum_error(base_path: &str, want: &[Value]) -> Vec<Error> {
@ -880,11 +831,10 @@ fn handle_one_of_error(base_path: &str, matched: &Option<(usize, usize)>) -> Vec
// Formats errors according to DropError structure
fn format_errors(errors: Vec<Error>, instance: &Value, schema_id: &str) -> Vec<Value> {
// Deduplicate by instance_path and format as DropError
let mut unique_errors: HashMap<String, Value> = HashMap::new();
for error in errors {
if let Entry::Vacant(entry) = unique_errors.entry(error.path.clone()) {
// Extract the failing value from the instance
let error_path = error.path.clone();
if let Entry::Vacant(entry) = unique_errors.entry(error_path.clone()) {
let failing_value = extract_value_at_path(instance, &error.path);
entry.insert(json!({
"code": error.code,
@ -899,7 +849,7 @@ fn format_errors(errors: Vec<Error>, instance: &Value, schema_id: &str) -> Vec<V
}
}
unique_errors.into_values().collect()
unique_errors.into_values().collect::<Vec<Value>>()
}
// Helper function to extract value at a JSON pointer path

View File

@ -432,7 +432,8 @@ pub fn property_merging_schemas() -> JsonB {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
"name": { "type": "string" },
"type": { "type": "string" }
},
"required": ["id"]
}]
@ -744,7 +745,8 @@ pub fn title_override_schemas() -> JsonB {
"type": "object",
"title": "Base Title",
"properties": {
"name": { "type": "string" }
"name": { "type": "string" },
"type": { "type": "string" }
},
"required": ["name"]
}]

View File

@ -169,7 +169,7 @@ fn test_validate_strict() {
let result_basic_invalid = validate_json_schema("basic_strict_test.request", jsonb(invalid_basic.clone()));
assert_error_count(&result_basic_invalid, 1);
assert_has_error(&result_basic_invalid, "FALSE_SCHEMA", "/extra");
assert_has_error(&result_basic_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra");
// Test 2: Non-strict validation - extra properties should pass
let result_non_strict = validate_json_schema("non_strict_test.request", jsonb(invalid_basic.clone()));
@ -190,8 +190,8 @@ fn test_validate_strict() {
let result_nested_invalid = validate_json_schema("nested_strict_test.request", jsonb(invalid_nested));
assert_error_count(&result_nested_invalid, 2);
assert_has_error(&result_nested_invalid, "FALSE_SCHEMA", "/user/extra");
assert_has_error(&result_nested_invalid, "FALSE_SCHEMA", "/items/0/extra");
assert_has_error(&result_nested_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/user/extra");
assert_has_error(&result_nested_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/items/0/extra");
// Test 4: Schema with unevaluatedProperties already set - should allow extras
let result_already_unevaluated = validate_json_schema("already_unevaluated_test.request", jsonb(invalid_basic.clone()));
@ -218,7 +218,7 @@ fn test_validate_strict() {
let result_conditional_invalid = validate_json_schema("conditional_strict_test.request", jsonb(invalid_conditional));
assert_error_count(&result_conditional_invalid, 1);
assert_has_error(&result_conditional_invalid, "FALSE_SCHEMA", "/extra");
assert_has_error(&result_conditional_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra");
}
#[pg_test]
@ -412,17 +412,17 @@ fn test_validate_unevaluated_properties() {
let result = validate_json_schema("simple_unevaluated_test.request", jsonb(instance_uneval));
// Should get 3 separate FALSE_SCHEMA errors, one for each unevaluated property
// Should get 3 separate ADDITIONAL_PROPERTIES_NOT_ALLOWED errors, one for each unevaluated property
assert_error_count(&result, 3);
// Verify all errors are FALSE_SCHEMA and check paths
assert_has_error(&result, "FALSE_SCHEMA", "/extra1");
assert_has_error(&result, "FALSE_SCHEMA", "/extra2");
assert_has_error(&result, "FALSE_SCHEMA", "/extra3");
// Verify all errors are ADDITIONAL_PROPERTIES_NOT_ALLOWED and check paths
assert_has_error(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra1");
assert_has_error(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra2");
assert_has_error(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra3");
// Verify error messages
let extra1_error = find_error_with_code_and_path(&result, "FALSE_SCHEMA", "/extra1");
assert_error_message_contains(extra1_error, "This schema always fails validation");
let extra1_error = find_error_with_code_and_path(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra1");
assert_error_message_contains(extra1_error, "Property 'extra1' is not allowed");
// Test 2: Complex schema with allOf and unevaluatedProperties (already in comprehensive setup)
@ -437,10 +437,10 @@ fn test_validate_unevaluated_properties() {
let complex_result = validate_json_schema("conditional_unevaluated_test.request", jsonb(complex_instance));
// Should get 2 FALSE_SCHEMA errors for unevaluated properties
// Should get 2 ADDITIONAL_PROPERTIES_NOT_ALLOWED errors for unevaluated properties
assert_error_count(&complex_result, 2);
assert_has_error(&complex_result, "FALSE_SCHEMA", "/nickname");
assert_has_error(&complex_result, "FALSE_SCHEMA", "/title");
assert_has_error(&complex_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/nickname");
assert_has_error(&complex_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/title");
// Test 3: Valid instance with all properties evaluated
let valid_instance = json!({
@ -643,8 +643,8 @@ fn test_validate_punc_with_refs() {
let result_public_root = validate_json_schema("public_ref_test.request", jsonb(public_root_extra));
assert_error_count(&result_public_root, 2);
assert_has_error(&result_public_root, "FALSE_SCHEMA", "/extra_field");
assert_has_error(&result_public_root, "FALSE_SCHEMA", "/another_extra");
assert_has_error(&result_public_root, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra_field");
assert_has_error(&result_public_root, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/another_extra");
// Test 2: Private punc allows extra properties at root level
let private_root_extra = json!({
@ -678,24 +678,6 @@ fn test_validate_punc_with_refs() {
let result_private_valid = validate_json_schema("private_ref_test.request", jsonb(valid_data_with_address));
assert_success(&result_private_valid);
// Test 4: Extra properties in nested address should fail for BOTH puncs (types are always strict)
let address_with_extra = json!({
"type": "person",
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"first_name": "John",
"last_name": "Doe",
"address": {
"street": "123 Main St",
"city": "Boston",
"country": "USA" // Should fail - extra property in address
}
});
let result_private_address = validate_json_schema("private_ref_test.request", jsonb(address_with_extra));
assert_error_count(&result_private_address, 1);
assert_has_error(&result_private_address, "FALSE_SCHEMA", "/address/country");
}
#[pg_test]

View File

@ -12,6 +12,7 @@ categories = ["web-programming"]
exclude = [ "tests", ".github", ".gitmodules" ]
[dependencies]
pgrx = "0.15.0"
serde = "1"
serde_json = "1"
regex = "1.10.3"
@ -26,6 +27,7 @@ ahash = "0.8.3"
appendlist = "1.4"
[dev-dependencies]
pgrx-tests = "0.15.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
ureq = "2.12"

View File

@ -181,7 +181,7 @@ impl Draft {
));
};
STD_METASCHEMAS
.validate(v, sch)
.validate(v, sch, None)
.map_err(|src| CompileError::ValidationError {
url: up.to_string(),
src: src.clone_static(),

View File

@ -134,6 +134,13 @@ use regex::Regex;
use serde_json::{Number, Value};
use util::*;
/// Options for validation process
#[derive(Default, Debug, Clone, Copy)]
pub struct ValidationOptions {
/// treat unevaluated properties as an error
pub be_strict: bool,
}
/// Identifier to compiled schema.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SchemaIndex(usize);
@ -187,11 +194,12 @@ impl Schemas {
&'s self,
v: &'v Value,
sch_index: SchemaIndex,
options: Option<&'s ValidationOptions>,
) -> Result<(), ValidationError<'s, 'v>> {
let Some(sch) = self.list.get(sch_index.0) else {
panic!("Schemas::validate: schema index out of bounds");
};
validator::validate(v, sch, self)
validator::validate(v, sch, self, options)
}
}

View File

@ -20,6 +20,7 @@ pub(crate) fn validate<'s, 'v>(
v: &'v Value,
schema: &'s Schema,
schemas: &'s Schemas,
options: Option<&'s ValidationOptions>,
) -> Result<(), ValidationError<'s, 'v>> {
let scope = Scope {
sch: schema.idx,
@ -28,13 +29,15 @@ pub(crate) fn validate<'s, 'v>(
parent: None,
};
let mut vloc = Vec::with_capacity(8);
let be_strict = options.map_or(false, |o| o.be_strict);
let (result, _) = Validator {
v,
vloc: &mut vloc,
schema,
schemas,
scope,
uneval: Uneval::from(v, schema, false),
options,
uneval: Uneval::from(v, schema, be_strict),
errors: vec![],
bool_result: false,
}
@ -86,6 +89,7 @@ struct Validator<'v, 's, 'd, 'e> {
schema: &'s Schema,
schemas: &'s Schemas,
scope: Scope<'d>,
options: Option<&'s ValidationOptions>,
uneval: Uneval<'v>,
errors: Vec<ValidationError<'s, 'v>>,
bool_result: bool, // is interested to know valid or not (but not actuall error)
@ -145,15 +149,6 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
}
}
// $ref --
if let Some(ref_) = s.ref_ {
let result = self.validate_ref(ref_, "$ref");
if s.draft_version < 2019 {
return (result, self.uneval);
}
self.errors.extend(result.err());
}
// type specific validations --
match v {
Value::Object(obj) => self.obj_validate(obj),
@ -163,6 +158,15 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
_ => {}
}
// $ref --
if let Some(ref_) = s.ref_ {
let result = self.validate_ref(ref_, "$ref");
if s.draft_version < 2019 {
return (result, self.uneval);
}
self.errors.extend(result.err());
}
if self.errors.is_empty() || !self.bool_result {
if s.draft_version >= 2019 {
self.refs_validate();
@ -292,7 +296,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
if let Some(sch) = &s.property_names {
for pname in obj.keys() {
let v = Value::String(pname.to_owned());
if let Err(mut e) = self.schemas.validate(&v, *sch) {
if let Err(mut e) = self.schemas.validate(&v, *sch, self.options) {
e.schema_url = &s.loc;
e.kind = ErrorKind::PropertyName {
prop: pname.to_owned(),
@ -366,7 +370,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
add_err!(self.validate_val(*sch, item, item!(i)));
}
evaluated = len;
debug_assert!(self.uneval.items.is_empty());
// debug_assert!(self.uneval.items.is_empty());
}
Items::SchemaRefs(list) => {
for (i, (item, sch)) in arr.iter().zip(list).enumerate() {
@ -391,7 +395,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
}
}
}
debug_assert!(self.uneval.items.is_empty());
// debug_assert!(self.uneval.items.is_empty());
}
} else {
// prefixItems --
@ -405,7 +409,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
for (i, item) in arr[evaluated..].iter().enumerate() {
add_err!(self.validate_val(*sch, item, item!(i)));
}
debug_assert!(self.uneval.items.is_empty());
// debug_assert!(self.uneval.items.is_empty());
}
}
@ -506,7 +510,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
// contentSchema --
if let (Some(sch), Some(v)) = (s.content_schema, deserialized) {
if let Err(mut e) = self.schemas.validate(&v, sch) {
if let Err(mut e) = self.schemas.validate(&v, sch, self.options) {
e.schema_url = &s.loc;
e.kind = kind!(ContentSchema);
self.errors.push(e.clone_static());
@ -758,16 +762,39 @@ impl Validator<'_, '_, '_, '_> {
};
}
let be_strict = self.options.map_or(false, |o| o.be_strict);
// unevaluatedProperties --
if let (Some(sch), Value::Object(obj)) = (s.unevaluated_properties, v) {
if let Value::Object(obj) = v {
if let Some(sch_idx) = s.unevaluated_properties {
let sch = self.schemas.get(sch_idx);
if sch.boolean == Some(false) {
// This is `unevaluatedProperties: false`, treat as additional properties
if !self.uneval.props.is_empty() {
let props: Vec<Cow<str>> =
self.uneval.props.iter().map(|p| Cow::from((*p).as_str())).collect();
self.add_error(ErrorKind::AdditionalProperties { got: props });
}
self.uneval.props.clear();
} else {
// It's a schema, validate against it
let uneval = std::mem::take(&mut self.uneval);
for pname in &uneval.props {
if let Some(pvalue) = obj.get(*pname) {
add_err!(self.validate_val(sch, pvalue, prop!(pname)));
add_err!(self.validate_val(sch_idx, pvalue, prop!(pname)));
}
}
self.uneval.props.clear();
}
} else if be_strict && !self.bool_result {
// 2. Runtime strictness check
if !self.uneval.props.is_empty() {
let props: Vec<Cow<str>> = self.uneval.props.iter().map(|p| Cow::from((*p).as_str())).collect();
self.add_error(ErrorKind::AdditionalProperties { got: props });
}
self.uneval.props.clear();
}
}
// unevaluatedItems --
if let (Some(sch), Value::Array(arr)) = (s.unevaluated_items, v) {
@ -797,18 +824,21 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
}
let scope = self.scope.child(sch, None, self.scope.vid + 1);
let schema = &self.schemas.get(sch);
Validator {
let be_strict = self.options.map_or(false, |o| o.be_strict);
let (result, _reply) = Validator {
v,
vloc: self.vloc,
schema,
schemas: self.schemas,
scope,
uneval: Uneval::from(v, schema, false),
options: self.options,
uneval: Uneval::from(v, schema, be_strict || !self.uneval.is_empty()),
errors: vec![],
bool_result: self.bool_result,
}
.validate()
.0
.validate();
// self.uneval.merge(&reply, None); // DO NOT MERGE, see https://github.com/santhosh-tekuri/boon/issues/33
result
}
fn _validate_self(
@ -819,18 +849,20 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
) -> Result<(), ValidationError<'s, 'v>> {
let scope = self.scope.child(sch, ref_kw, self.scope.vid);
let schema = &self.schemas.get(sch);
let be_strict = self.options.map_or(false, |o| o.be_strict);
let (result, reply) = Validator {
v: self.v,
vloc: self.vloc,
schema,
schemas: self.schemas,
scope,
uneval: Uneval::from(self.v, schema, !self.uneval.is_empty()),
options: self.options,
uneval: self.uneval.clone(),
errors: vec![],
bool_result: self.bool_result || bool_result,
}
.validate();
self.uneval.merge(&reply);
self.uneval.merge(&reply, ref_kw);
result
}
@ -925,7 +957,7 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
// Uneval --
#[derive(Default)]
#[derive(Default, Clone)]
struct Uneval<'v> {
props: HashSet<&'v String>,
items: HashSet<usize>,
@ -940,9 +972,7 @@ impl<'v> Uneval<'v> {
let mut uneval = Self::default();
match v {
Value::Object(obj) => {
if !sch.all_props_evaluated
&& (caller_needs || sch.unevaluated_properties.is_some())
{
if caller_needs || sch.unevaluated_properties.is_some() || !sch.all_props_evaluated {
uneval.props = obj.keys().collect();
}
}
@ -959,7 +989,7 @@ impl<'v> Uneval<'v> {
uneval
}
fn merge(&mut self, other: &Uneval) {
fn merge(&mut self, other: &Uneval<'v>, _ref_kw: Option<&'static str>) {
self.props.retain(|p| other.props.contains(p));
self.items.retain(|i| other.items.contains(i));
}