From 8a6617e96b6538a938868ca626faf6b3911fe475 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 27 Oct 2021 10:51:36 +0100 Subject: [PATCH] oneOf validator will read a new "oneOfEnumSelectorField" option in schema and use that to pick subschema. https://github.com/openownership/lib-cove-bods/issues/64 --- CHANGELOG.md | 5 + libcove/lib/common.py | 36 ++++--- ...chema_with_one_of_enum_selector_field.json | 48 ++++++++++ ...eld_inside_one_of_enum_selector_field.json | 75 +++++++++++++++ tests/lib/test_common.py | 94 +++++++++++++++++++ 5 files changed, 245 insertions(+), 13 deletions(-) create mode 100644 tests/lib/fixtures/common/schema_with_one_of_enum_selector_field.json create mode 100644 tests/lib/fixtures/common/schema_with_one_of_enum_selector_field_inside_one_of_enum_selector_field.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3629b..9922f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## Added + +- oneOf validator will read a new "oneOfEnumSelectorField" option in schema and use that to pick subschema. + (Previously this worked for "statementType" only, for BODS) + ## [0.26.1] - 2021-10-01 ## Changed diff --git a/libcove/lib/common.py b/libcove/lib/common.py index 222155d..e3df640 100644 --- a/libcove/lib/common.py +++ b/libcove/lib/common.py @@ -162,40 +162,50 @@ def oneOf_draft4(validator, oneOf, instance, schema): Modified to: - sort the instance JSON, so we get a reproducible output that we can can test more easily - - If `statementType` is available, use that pick the correct - sub-schema, and to yield those ValidationErrors. (Only - applicable for BODS). + - If `oneOfEnumSelectorField` is set in schema, use that field to pick the correct + sub-schema, and to yield those ValidationErrors. + - If `oneOfEnumSelectorField` is not set in schema, assume it should be "statementType" and use the same logic. + (This is historical code for BODS) """ + + one_of_enum_selector_field = schema.get("oneOfEnumSelectorField", "statementType") subschemas = enumerate(oneOf) all_errors = [] - validStatementTypes = [] + valid_selector_field_values = [] for index, subschema in subschemas: errs = list(validator.descend(instance, subschema, schema_path=index)) if not errs: first_valid = subschema break properties = subschema.get("properties", {}) - if "statementType" in properties: - if "statementType" in instance: + if one_of_enum_selector_field in properties: + if one_of_enum_selector_field in instance: try: - validStatementType = properties["statementType"].get("enum", [])[0] + selector_field_values_in_this_subschema = properties[ + one_of_enum_selector_field + ].get("enum", []) except IndexError: continue - if instance["statementType"] == validStatementType: + if ( + instance[one_of_enum_selector_field] + in selector_field_values_in_this_subschema + ): for err in errs: yield err return else: - validStatementTypes.append(validStatementType) + valid_selector_field_values.extend( + selector_field_values_in_this_subschema + ) else: - yield ValidationError("statementType", validator="required") + yield ValidationError(one_of_enum_selector_field, validator="required") break all_errors.extend(errs) else: - if validStatementTypes: + if valid_selector_field_values: yield ValidationError( - "Invalid code found in statementType", - instance=instance["statementType"], + "Invalid code found in " + one_of_enum_selector_field, + instance=instance[one_of_enum_selector_field], path=("statementType",), validator="enum", ) diff --git a/tests/lib/fixtures/common/schema_with_one_of_enum_selector_field.json b/tests/lib/fixtures/common/schema_with_one_of_enum_selector_field.json new file mode 100644 index 0000000..a0fb8e4 --- /dev/null +++ b/tests/lib/fixtures/common/schema_with_one_of_enum_selector_field.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "pet": { + "type": "string", + "enum": [ + "cat", + "dog" + ] + }, + "waggy": { + "type": "string" + }, + "purry": { + "type": "string" + } + }, + "oneOfEnumSelectorField": "pet", + "oneOf": [ + { + "properties": { + "pet": { + "enum": [ + "cat" + ] + } + }, + "required": [ + "pet", + "purry" + ] + }, + { + "properties": { + "pet": { + "enum": [ + "dog" + ] + } + }, + "required": [ + "pet", + "waggy" + ] + } + ] +} \ No newline at end of file diff --git a/tests/lib/fixtures/common/schema_with_one_of_enum_selector_field_inside_one_of_enum_selector_field.json b/tests/lib/fixtures/common/schema_with_one_of_enum_selector_field_inside_one_of_enum_selector_field.json new file mode 100644 index 0000000..c0245c1 --- /dev/null +++ b/tests/lib/fixtures/common/schema_with_one_of_enum_selector_field_inside_one_of_enum_selector_field.json @@ -0,0 +1,75 @@ +{ + "id": "bods-package.json", + "$schema": "http://json-schema.org/draft-04/schema#", + "version": "0.2", + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "statementType": { + "type": "string", + "enum": [ + "animal" + ] + }, + "pet": { + "type": "string", + "enum": [ + "cat", + "dog" + ] + }, + "waggy": { + "type": "string" + }, + "purry": { + "type": "string" + } + }, + "oneOfEnumSelectorField": "pet", + "oneOf": [ + { + "properties": { + "pet": { + "enum": [ + "cat" + ] + } + }, + "required": [ + "pet", + "purry" + ] + }, + { + "properties": { + "pet": { + "enum": [ + "dog" + ] + } + }, + "required": [ + "pet", + "waggy" + ] + } + ] + }, + { + "type": "object", + "properties": { + "statementType": { + "type": "string", + "enum": [ + "property" + ] + } + } + } + ] + } +} + diff --git a/tests/lib/test_common.py b/tests/lib/test_common.py index 0c19c52..454f31d 100644 --- a/tests/lib/test_common.py +++ b/tests/lib/test_common.py @@ -1203,3 +1203,97 @@ def test_get_field_coverage_oc4ids(): ) == open(common_fixtures("oc4ids_example_coverage.json")).read() ) + + +@pytest.mark.parametrize( + ("data", "count", "errors"), + ( + # Good cat + ({"pet": "cat", "purry": "Very"}, 0, []), + # Good dog + ({"pet": "dog", "waggy": "Very"}, 0, []), + # A cat with a wrong required field + ( + {"pet": "cat", "waggy": "Yes!"}, + 1, + [{"message": "'purry' is missing but required"}], + ), + # A dog with a wrong required field + ( + {"pet": "dog", "purry": "Yes!"}, + 1, + [{"message": "'waggy' is missing but required"}], + ), + ), +) +def test_oneOfEnumSelectorField(data, count, errors): + + with open(common_fixtures("schema_with_one_of_enum_selector_field.json")) as fp: + schema = json.load(fp) + + class DummySchemaObj: + config = None + schema_host = None + + def get_pkg_schema_obj(self): + return schema + + validation_errors = get_schema_validation_errors(data, DummySchemaObj(), "", {}, {}) + + assert count == len(validation_errors) + + for i in range(0, len(errors)): + validation_error_json = json.loads(list(validation_errors.keys())[i]) + assert validation_error_json["message"] == errors[i]["message"] + + +@pytest.mark.parametrize( + ("data", "count", "errors"), + ( + # Good cat + ([{"statementType": "animal", "pet": "cat", "purry": "Very"}], 0, []), + # Good dog + ([{"statementType": "animal", "pet": "dog", "waggy": "Very"}], 0, []), + # A cat with a wrong required field + ( + [{"statementType": "animal", "pet": "cat", "waggy": "Yes!"}], + 1, + [{"message": "'purry' is missing but required"}], + ), + # A dog with a wrong required field + ( + [{"statementType": "animal", "pet": "dog", "purry": "Yes!"}], + 1, + [{"message": "'waggy' is missing but required"}], + ), + # A house + ([{"statementType": "property"}], 0, []), + ), +) +def test_one_of_enum_selector_field_inside_one_of_enum_selector_field( + data, count, errors +): + """This replicates how this will be used in BODS. + It also tests that the 'statementType' key is checked by default.""" + + with open( + common_fixtures( + "schema_with_one_of_enum_selector_field_inside_one_of_enum_selector_field.json" + ) + ) as fp: + schema = json.load(fp) + + class DummySchemaObj: + config = None + schema_host = None + + def get_pkg_schema_obj(self): + return schema + + validation_errors = get_schema_validation_errors(data, DummySchemaObj(), "", {}, {}) + + assert count == len(validation_errors) + + for i in range(0, len(errors)): + validation_error_json = json.loads(list(validation_errors.keys())[i]) + assert validation_error_json["message"] == errors[i]["message"]