Compare commits

..

5 Commits

7 changed files with 111 additions and 23 deletions

13
flow
View File

@ -97,11 +97,16 @@ install() {
fi fi
} }
test() { test-jspg() {
info "Running jspg tests..." info "Running jspg tests..."
cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $? cargo pgrx test "pg${POSTGRES_VERSION}" "$@" || return $?
} }
test-validator() {
info "Running validator tests..."
cargo test -p boon --features "pgrx/pg${POSTGRES_VERSION}" "$@" || return $?
}
clean() { clean() {
info "Cleaning build artifacts..." info "Cleaning build artifacts..."
cargo clean || return $? cargo clean || return $?
@ -111,7 +116,8 @@ jspg-usage() {
printf "prepare\tCheck OS, Cargo, and PGRX dependencies.\n" printf "prepare\tCheck OS, Cargo, and PGRX dependencies.\n"
printf "install\tBuild and install the extension locally (after prepare).\n" printf "install\tBuild and install the extension locally (after prepare).\n"
printf "reinstall\tClean, build, and install the extension locally (after prepare).\n" printf "reinstall\tClean, build, and install the extension locally (after prepare).\n"
printf "test\t\tRun pgrx integration tests.\n" printf "test-jspg\t\tRun pgrx integration tests.\n"
printf "test-validator\t\tRun validator integration tests.\n"
printf "clean\t\tRemove pgrx build artifacts.\n" printf "clean\t\tRemove pgrx build artifacts.\n"
} }
@ -121,7 +127,8 @@ jspg-flow() {
build) build; return $?;; build) build; return $?;;
install) install; return $?;; install) install; return $?;;
reinstall) clean && install; return $?;; reinstall) clean && install; return $?;;
test) test "${@:2}"; return $?;; test-jspg) test-jspg "${@:2}"; return $?;;
test-validator) test-validator "${@:2}"; return $?;;
clean) clean; return $?;; clean) clean; return $?;;
*) return 1 ;; *) return 1 ;;
esac esac

View File

@ -304,11 +304,11 @@ fn validate_json_schema(schema_id: &str, instance: JsonB) -> JsonB {
Some(schema) => { Some(schema) => {
let instance_value: Value = instance.0; let instance_value: Value = instance.0;
let options = match schema.t { let options = match schema.t {
SchemaType::Type | SchemaType::PublicPunc => Some(ValidationOptions { be_strict: true }), SchemaType::PublicPunc => Some(ValidationOptions { be_strict: true }),
_ => None, _ => None,
}; };
match cache.schemas.validate(&instance_value, schema.index, options.as_ref()) { match cache.schemas.validate(&instance_value, schema.index, options) {
Ok(_) => { Ok(_) => {
let mut custom_errors = Vec::new(); let mut custom_errors = Vec::new();
if schema.t == SchemaType::Type || schema.t == SchemaType::PublicPunc || schema.t == SchemaType::PrivatePunc { if schema.t == SchemaType::Type || schema.t == SchemaType::PublicPunc || schema.t == SchemaType::PrivatePunc {
@ -394,7 +394,7 @@ fn collect_errors(error: &ValidationError, errors_list: &mut Vec<Error>) {
ErrorKind::AnyOf => handle_any_of_error(&base_path), ErrorKind::AnyOf => handle_any_of_error(&base_path),
ErrorKind::OneOf(matched) => handle_one_of_error(&base_path, matched), ErrorKind::OneOf(matched) => handle_one_of_error(&base_path, matched),
}; };
errors_list.extend(errors_to_add); errors_list.extend(errors_to_add);
} }

View File

@ -355,7 +355,16 @@ pub fn additional_properties_schemas() -> JsonB {
pub fn unevaluated_properties_schemas() -> JsonB { pub fn unevaluated_properties_schemas() -> JsonB {
let enums = json!([]); let enums = json!([]);
let types = json!([]); let types = json!([{
"name": "nested_for_uneval",
"schemas": [{
"$id": "nested_for_uneval",
"type": "object",
"properties": {
"deep_prop": { "type": "string" }
}
}]
}]);
let puncs = json!([ let puncs = json!([
{ {
"name": "simple_unevaluated_test", "name": "simple_unevaluated_test",
@ -396,6 +405,29 @@ pub fn unevaluated_properties_schemas() -> JsonB {
}, },
"unevaluatedProperties": false "unevaluatedProperties": false
}] }]
},
{
"name": "nested_unevaluated_test",
"public": true, // To trigger strict mode
"schemas": [{
"$id": "nested_unevaluated_test.request",
"type": "object",
"properties": {
"non_strict_branch": {
"type": "object",
"unevaluatedProperties": true, // The magic switch
"properties": {
"some_prop": { "$ref": "nested_for_uneval" }
}
},
"strict_branch": {
"type": "object",
"properties": {
"another_prop": { "type": "string" }
}
}
}
}]
} }
]); ]);

View File

@ -452,6 +452,33 @@ fn test_validate_unevaluated_properties() {
let valid_result = validate_json_schema("simple_unevaluated_test.request", jsonb(valid_instance)); let valid_result = validate_json_schema("simple_unevaluated_test.request", jsonb(valid_instance));
assert_success(&valid_result); assert_success(&valid_result);
// Test 4: Test that unevaluatedProperties: true cascades down refs
let cascading_instance = json!({
"strict_branch": {
"another_prop": "is_ok"
},
"non_strict_branch": {
"extra_at_toplevel": "is_ok", // Extra property at this level
"some_prop": {
"deep_prop": "is_ok",
"extra_in_ref": "is_also_ok" // Extra property in the $ref'd schema
}
}
});
let cascading_result = validate_json_schema("nested_unevaluated_test.request", jsonb(cascading_instance));
assert_success(&cascading_result);
// Test 5: For good measure, test that the strict branch is still strict
let strict_fail_instance = json!({
"strict_branch": {
"another_prop": "is_ok",
"extra_in_strict": "is_not_ok"
}
});
let strict_fail_result = validate_json_schema("nested_unevaluated_test.request", jsonb(strict_fail_instance));
assert_error_count(&strict_fail_result, 1);
assert_has_error(&strict_fail_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/strict_branch/extra_in_strict");
} }
#[pg_test] #[pg_test]

View File

@ -194,7 +194,7 @@ impl Schemas {
&'s self, &'s self,
v: &'v Value, v: &'v Value,
sch_index: SchemaIndex, sch_index: SchemaIndex,
options: Option<&'s ValidationOptions>, options: Option<ValidationOptions>,
) -> Result<(), ValidationError<'s, 'v>> { ) -> Result<(), ValidationError<'s, 'v>> {
let Some(sch) = self.list.get(sch_index.0) else { let Some(sch) = self.list.get(sch_index.0) else {
panic!("Schemas::validate: schema index out of bounds"); panic!("Schemas::validate: schema index out of bounds");

View File

@ -20,7 +20,7 @@ pub(crate) fn validate<'s, 'v>(
v: &'v Value, v: &'v Value,
schema: &'s Schema, schema: &'s Schema,
schemas: &'s Schemas, schemas: &'s Schemas,
options: Option<&'s ValidationOptions>, options: Option<ValidationOptions>,
) -> Result<(), ValidationError<'s, 'v>> { ) -> Result<(), ValidationError<'s, 'v>> {
let scope = Scope { let scope = Scope {
sch: schema.idx, sch: schema.idx,
@ -29,7 +29,7 @@ pub(crate) fn validate<'s, 'v>(
parent: None, parent: None,
}; };
let mut vloc = Vec::with_capacity(8); let mut vloc = Vec::with_capacity(8);
let be_strict = options.map_or(false, |o| o.be_strict); let options = options.unwrap_or_default();
let (result, _) = Validator { let (result, _) = Validator {
v, v,
vloc: &mut vloc, vloc: &mut vloc,
@ -37,7 +37,7 @@ pub(crate) fn validate<'s, 'v>(
schemas, schemas,
scope, scope,
options, options,
uneval: Uneval::from(v, schema, be_strict), uneval: Uneval::from(v, schema, options.be_strict),
errors: vec![], errors: vec![],
bool_result: false, bool_result: false,
} }
@ -89,7 +89,7 @@ struct Validator<'v, 's, 'd, 'e> {
schema: &'s Schema, schema: &'s Schema,
schemas: &'s Schemas, schemas: &'s Schemas,
scope: Scope<'d>, scope: Scope<'d>,
options: Option<&'s ValidationOptions>, options: ValidationOptions,
uneval: Uneval<'v>, uneval: Uneval<'v>,
errors: Vec<ValidationError<'s, 'v>>, errors: Vec<ValidationError<'s, 'v>>,
bool_result: bool, // is interested to know valid or not (but not actuall error) bool_result: bool, // is interested to know valid or not (but not actuall error)
@ -296,7 +296,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
if let Some(sch) = &s.property_names { if let Some(sch) = &s.property_names {
for pname in obj.keys() { for pname in obj.keys() {
let v = Value::String(pname.to_owned()); let v = Value::String(pname.to_owned());
if let Err(mut e) = self.schemas.validate(&v, *sch, self.options) { if let Err(mut e) = self.schemas.validate(&v, *sch, Some(self.options)) {
e.schema_url = &s.loc; e.schema_url = &s.loc;
e.kind = ErrorKind::PropertyName { e.kind = ErrorKind::PropertyName {
prop: pname.to_owned(), prop: pname.to_owned(),
@ -510,7 +510,7 @@ impl<'v> Validator<'v, '_, '_, '_> {
// contentSchema -- // contentSchema --
if let (Some(sch), Some(v)) = (s.content_schema, deserialized) { if let (Some(sch), Some(v)) = (s.content_schema, deserialized) {
if let Err(mut e) = self.schemas.validate(&v, sch, self.options) { if let Err(mut e) = self.schemas.validate(&v, sch, Some(self.options)) {
e.schema_url = &s.loc; e.schema_url = &s.loc;
e.kind = kind!(ContentSchema); e.kind = kind!(ContentSchema);
self.errors.push(e.clone_static()); self.errors.push(e.clone_static());
@ -762,8 +762,6 @@ impl Validator<'_, '_, '_, '_> {
}; };
} }
let be_strict = self.options.map_or(false, |o| o.be_strict);
// unevaluatedProperties -- // unevaluatedProperties --
if let Value::Object(obj) = v { if let Value::Object(obj) = v {
if let Some(sch_idx) = s.unevaluated_properties { if let Some(sch_idx) = s.unevaluated_properties {
@ -786,7 +784,7 @@ impl Validator<'_, '_, '_, '_> {
} }
self.uneval.props.clear(); self.uneval.props.clear();
} }
} else if be_strict && !self.bool_result { } else if self.options.be_strict && !self.bool_result {
// 2. Runtime strictness check // 2. Runtime strictness check
if !self.uneval.props.is_empty() { if !self.uneval.props.is_empty() {
let props: Vec<Cow<str>> = self.uneval.props.iter().map(|p| Cow::from((*p).as_str())).collect(); let props: Vec<Cow<str>> = self.uneval.props.iter().map(|p| Cow::from((*p).as_str())).collect();
@ -824,15 +822,27 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
} }
let scope = self.scope.child(sch, None, self.scope.vid + 1); let scope = self.scope.child(sch, None, self.scope.vid + 1);
let schema = &self.schemas.get(sch); let schema = &self.schemas.get(sch);
let be_strict = self.options.map_or(false, |o| o.be_strict);
// Check if the new schema turns off strictness
let allows_unevaluated = schema.boolean == Some(true) ||
if let Some(idx) = schema.unevaluated_properties {
self.schemas.get(idx).boolean == Some(true)
} else {
false
};
let mut new_options = self.options;
if allows_unevaluated {
new_options.be_strict = false;
}
let (result, _reply) = Validator { let (result, _reply) = Validator {
v, v,
vloc: self.vloc, vloc: self.vloc,
schema, schema,
schemas: self.schemas, schemas: self.schemas,
scope, scope,
options: self.options, options: new_options,
uneval: Uneval::from(v, schema, be_strict || !self.uneval.is_empty()), uneval: Uneval::from(v, schema, new_options.be_strict || !self.uneval.is_empty()),
errors: vec![], errors: vec![],
bool_result: self.bool_result, bool_result: self.bool_result,
} }
@ -849,14 +859,26 @@ impl<'v, 's> Validator<'v, 's, '_, '_> {
) -> Result<(), ValidationError<'s, 'v>> { ) -> Result<(), ValidationError<'s, 'v>> {
let scope = self.scope.child(sch, ref_kw, self.scope.vid); let scope = self.scope.child(sch, ref_kw, self.scope.vid);
let schema = &self.schemas.get(sch); let schema = &self.schemas.get(sch);
let be_strict = self.options.map_or(false, |o| o.be_strict);
// Check if the new schema turns off strictness
let allows_unevaluated = schema.boolean == Some(true) ||
if let Some(idx) = schema.unevaluated_properties {
self.schemas.get(idx).boolean == Some(true)
} else {
false
};
let mut new_options = self.options;
if allows_unevaluated {
new_options.be_strict = false;
}
let (result, reply) = Validator { let (result, reply) = Validator {
v: self.v, v: self.v,
vloc: self.vloc, vloc: self.vloc,
schema, schema,
schemas: self.schemas, schemas: self.schemas,
scope, scope,
options: self.options, options: new_options,
uneval: self.uneval.clone(), uneval: self.uneval.clone(),
errors: vec![], errors: vec![],
bool_result: self.bool_result || bool_result, bool_result: self.bool_result || bool_result,

View File

@ -1 +1 @@
1.0.38 1.0.40