Skip to content

Commit

Permalink
oneOf validator will read a new "oneOfEnumSelectorField" option in sc…
Browse files Browse the repository at this point in the history
…hema and use that to pick subschema.

openownership/lib-cove-bods#64
  • Loading branch information
jarofgreen committed Oct 28, 2021
1 parent e4488ba commit 8a6617e
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 23 additions & 13 deletions libcove/lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
]
}
}

94 changes: 94 additions & 0 deletions tests/lib/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

0 comments on commit 8a6617e

Please sign in to comment.