diff --git a/seedcase_sprout/core/sprout_checks/check_id_in_resource_path.py b/seedcase_sprout/core/sprout_checks/check_id_in_resource_path.py new file mode 100644 index 00000000..619515f7 --- /dev/null +++ b/seedcase_sprout/core/sprout_checks/check_id_in_resource_path.py @@ -0,0 +1,37 @@ +from pathlib import Path + +from seedcase_sprout.core.checks.check_error import CheckError +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + + +def check_id_in_resource_path( + properties: dict, index: int | None = None +) -> list[CheckError]: + """Checks if the data path in resource properties is well-formed. + + Ignores a missing data path or a path of the wrong type. + + Args: + properties: The resource properties to check. + index: The index of the resource properties. Defaults to None. + + Returns: + The properties, if the data path is well-formed. + """ + data_path = properties.get("path") + if not isinstance(data_path, str): + return [] + + data_path = Path(data_path) + if len(data_path.parts) == 3 and data_path.parts[1].isdigit(): + return [] + + return [ + CheckError( + message="'path' should contain the resource ID", + json_path=get_json_path_to_resource_field("path", index), + validator="pattern", + ) + ] diff --git a/seedcase_sprout/core/sprout_checks/check_no_inline_data.py b/seedcase_sprout/core/sprout_checks/check_no_inline_data.py new file mode 100644 index 00000000..2bad937d --- /dev/null +++ b/seedcase_sprout/core/sprout_checks/check_no_inline_data.py @@ -0,0 +1,31 @@ +from seedcase_sprout.core.checks.check_error import CheckError +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + + +def check_no_inline_data( + properties: dict, index: int | None = None +) -> list[CheckError]: + """Checks that the `data` field of a set of resource properties is not set. + + Args: + properties: The resource properties. + index: The index of the resource properties. Defaults to None. + + Returns: + A list of errors. The empty list if no error was found. + """ + if properties.get("data") is None: + return [] + + return [ + CheckError( + message=( + "Sprout doesn't use the 'data' field, instead it expects data " + "in separate files that are given in the 'path' field." + ), + json_path=get_json_path_to_resource_field("data", index), + validator="inline-data", + ) + ] diff --git a/seedcase_sprout/core/sprout_checks/check_resource_path_string.py b/seedcase_sprout/core/sprout_checks/check_resource_path_string.py new file mode 100644 index 00000000..0d92b7cc --- /dev/null +++ b/seedcase_sprout/core/sprout_checks/check_resource_path_string.py @@ -0,0 +1,33 @@ +from seedcase_sprout.core.checks.check_error import CheckError +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + +CHECKS_TYPE_ERROR_MESSAGE = "{field_value} is not of type '{field_type}'" + + +def check_resource_path_string( + properties: dict, index: int | None = None +) -> list[CheckError]: + """Checks that the `path` field of a set of resource properties is of type string. + + Args: + properties: The resource properties. + index: The index of the resource properties. Defaults to None. + + Returns: + A list of errors. An empty list if no error was found. + """ + path = properties.get("path", "") + if isinstance(path, str): + return [] + + return [ + CheckError( + message=CHECKS_TYPE_ERROR_MESSAGE.format( + field_value=path, field_type="string" + ), + json_path=get_json_path_to_resource_field("path", index), + validator="type", + ) + ] diff --git a/seedcase_sprout/core/sprout_checks/exclude_non_sprout_resource_errors.py b/seedcase_sprout/core/sprout_checks/exclude_non_sprout_resource_errors.py new file mode 100644 index 00000000..67fc5991 --- /dev/null +++ b/seedcase_sprout/core/sprout_checks/exclude_non_sprout_resource_errors.py @@ -0,0 +1,28 @@ +from seedcase_sprout.core.checks.check_error import CheckError + + +def exclude_non_sprout_resource_errors( + errors: list[CheckError], +) -> list[CheckError]: + """Filters out resource errors that are not relevant for Sprout. + + Errors filtered out: + - inline `data` required but missing + - `path` is not of type array + + Args: + errors: The full error list. + + Returns: + The filtered error list. + """ + return [ + error + for error in errors + if not (error.validator == "required" and error.json_path.endswith(".data")) + and not ( + error.validator == "type" + and error.json_path.endswith(".path") + and error.message.endswith("not of type 'array'") + ) + ] diff --git a/seedcase_sprout/core/sprout_checks/get_blank_value_for_type.py b/seedcase_sprout/core/sprout_checks/get_blank_value_for_type.py new file mode 100644 index 00000000..8340042c --- /dev/null +++ b/seedcase_sprout/core/sprout_checks/get_blank_value_for_type.py @@ -0,0 +1,19 @@ +from seedcase_sprout.core.checks.required_fields import RequiredFieldType + + +def get_blank_value_for_type(type: RequiredFieldType) -> str | list | None: + """Returns the blank value for each type of (required) field. + + Args: + type: The type of the field. + + Returns: + The corresponding blank value. + """ + match type: + case RequiredFieldType.str: + return "" + case RequiredFieldType.list: + return [] + case _: + return None diff --git a/seedcase_sprout/core/sprout_checks/get_json_path_to_resource_field.py b/seedcase_sprout/core/sprout_checks/get_json_path_to_resource_field.py new file mode 100644 index 00000000..3fa4496d --- /dev/null +++ b/seedcase_sprout/core/sprout_checks/get_json_path_to_resource_field.py @@ -0,0 +1,14 @@ +def get_json_path_to_resource_field(field: str, index: int | None = None) -> str: + """Creates the JSON path to the specified field of a set of resource properties. + + Optionally adds the index of the resource properties, if they are part of a set of + package properties. + + Args: + field: The name of the field. + index: The index of the resource properties. Defaults to None. + + Returns: + The JSON path. + """ + return "$." + ("" if index is None else f"resources[{index}].") + field diff --git a/tests/core/sprout_checks/test_check_id_in_resource_path.py b/tests/core/sprout_checks/test_check_id_in_resource_path.py new file mode 100644 index 00000000..f431cec7 --- /dev/null +++ b/tests/core/sprout_checks/test_check_id_in_resource_path.py @@ -0,0 +1,55 @@ +from pathlib import Path + +from pytest import mark + +from seedcase_sprout.core.sprout_checks.check_id_in_resource_path import ( + check_id_in_resource_path, +) +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + + +@mark.parametrize("index", [None, 2]) +def test_passes_if_data_path_well_formed(index): + """Should pass if the path contains a resource ID.""" + properties = {"path": str(Path("resources", "1", "data.parquet"))} + + assert check_id_in_resource_path(properties, index) == [] + + +@mark.parametrize("index", [None, 2]) +def test_passes_if_data_path_not_present(index): + """Should pass if the path is not set.""" + assert check_id_in_resource_path({}, index) == [] + + +@mark.parametrize("index", [None, 2]) +@mark.parametrize("data_path", [123, []]) +def test_passes_if_path_of_wrong_type(index, data_path): + """Should pass if path is of the wrong type.""" + properties = {"path": data_path} + + assert check_id_in_resource_path(properties, index) == [] + + +@mark.parametrize("index", [None, 2]) +@mark.parametrize( + "data_path", + [ + "", + Path("resources", "x", "data.parquet"), + Path("1", "data.parquet"), + Path("resources", "1", "data.parquet", "1"), + ], +) +def test_returns_error_if_data_path_is_malformed(index, data_path): + """Returns list of `CheckError`s if the data path does not contain a resource ID.""" + properties = {"path": str(data_path)} + + errors = check_id_in_resource_path(properties, index) + + assert len(errors) == 1 + assert errors[0].message + assert errors[0].json_path == get_json_path_to_resource_field("path", index) + assert errors[0].validator == "pattern" diff --git a/tests/core/sprout_checks/test_check_no_inline_data.py b/tests/core/sprout_checks/test_check_no_inline_data.py new file mode 100644 index 00000000..cac1d1ff --- /dev/null +++ b/tests/core/sprout_checks/test_check_no_inline_data.py @@ -0,0 +1,25 @@ +from pytest import mark + +from seedcase_sprout.core.sprout_checks.check_no_inline_data import check_no_inline_data +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + + +@mark.parametrize("index", [None, 2]) +def test_passes_if_data_not_set(index): + """Should pass if inline data is not set.""" + assert check_no_inline_data({}, index) == [] + + +@mark.parametrize("index", [None, 2]) +def test_error_found_if_data_is_set(index): + """Should find an error if inline data is set.""" + properties = {"data": "some data"} + + errors = check_no_inline_data(properties, index) + + assert len(errors) == 1 + assert errors[0].message + assert errors[0].json_path == get_json_path_to_resource_field("data", index) + assert errors[0].validator == "inline-data" diff --git a/tests/core/sprout_checks/test_check_resource_path_string.py b/tests/core/sprout_checks/test_check_resource_path_string.py new file mode 100644 index 00000000..19983660 --- /dev/null +++ b/tests/core/sprout_checks/test_check_resource_path_string.py @@ -0,0 +1,35 @@ +from pytest import mark + +from seedcase_sprout.core.sprout_checks.check_resource_path_string import ( + check_resource_path_string, +) +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + + +@mark.parametrize("index", [None, 2]) +def test_passes_if_data_path_string(index): + """Should pass if the path is of type string.""" + properties = {"path": "a string"} + + assert check_resource_path_string(properties, index) == [] + + +@mark.parametrize("index", [None, 2]) +def test_passes_if_data_path_not_present(index): + """Should pass if the path is not set.""" + assert check_resource_path_string({}, index) == [] + + +@mark.parametrize("index", [None, 2]) +def test_error_found_if_path_not_string(index): + """Should find an error if the path is not of type string.""" + properties = {"path": 123} + + errors = check_resource_path_string(properties, index) + + assert len(errors) == 1 + assert "string" in errors[0].message + assert errors[0].json_path == get_json_path_to_resource_field("path", index) + assert errors[0].validator == "type" diff --git a/tests/core/sprout_checks/test_exclude_non_sprout_resource_errors.py b/tests/core/sprout_checks/test_exclude_non_sprout_resource_errors.py new file mode 100644 index 00000000..cdf5c38b --- /dev/null +++ b/tests/core/sprout_checks/test_exclude_non_sprout_resource_errors.py @@ -0,0 +1,52 @@ +from seedcase_sprout.core.checks.check_error import CheckError +from seedcase_sprout.core.sprout_checks.exclude_non_sprout_resource_errors import ( + exclude_non_sprout_resource_errors, +) + + +def test_returns_unaltered_empty_list(): + """Should not alter an empty list.""" + assert exclude_non_sprout_resource_errors([]) == [] + + +def test_returns_only_sprout_related_errors(): + """Should only remove errors not relevant for Sprout.""" + errors = [ + CheckError( + message="'data' is a required property", + json_path="$.data", + validator="required", + ), + CheckError( + message="'name' is a required property", + json_path="$.name", + validator="required", + ), + CheckError( + message="123 is not of type 'array'", json_path="$.path", validator="type" + ), + CheckError( + message="123 is not of type 'string'", json_path="$.path", validator="type" + ), + CheckError( + message="123 is not of type 'array'", + json_path="$.sources", + validator="type", + ), + ] + + assert exclude_non_sprout_resource_errors(errors) == [ + CheckError( + message="'name' is a required property", + json_path="$.name", + validator="required", + ), + CheckError( + message="123 is not of type 'string'", json_path="$.path", validator="type" + ), + CheckError( + message="123 is not of type 'array'", + json_path="$.sources", + validator="type", + ), + ] diff --git a/tests/core/sprout_checks/test_get_blank_value_for_type.py b/tests/core/sprout_checks/test_get_blank_value_for_type.py new file mode 100644 index 00000000..c393392d --- /dev/null +++ b/tests/core/sprout_checks/test_get_blank_value_for_type.py @@ -0,0 +1,21 @@ +from pytest import mark + +from seedcase_sprout.core.checks.required_fields import RequiredFieldType +from seedcase_sprout.core.sprout_checks.get_blank_value_for_type import ( + get_blank_value_for_type, +) + + +@mark.parametrize( + "type,value", + [ + (RequiredFieldType.str, ""), + (RequiredFieldType.list, []), + ("int", None), + (None, None), + ("something else", None), + ], +) +def test_returns_expected_blank_value_for_each_type(type, value): + """Should return the expected blank value for each type.""" + assert get_blank_value_for_type(type) == value diff --git a/tests/core/sprout_checks/test_get_json_path_to_resource_field.py b/tests/core/sprout_checks/test_get_json_path_to_resource_field.py new file mode 100644 index 00000000..815860bf --- /dev/null +++ b/tests/core/sprout_checks/test_get_json_path_to_resource_field.py @@ -0,0 +1,13 @@ +from seedcase_sprout.core.sprout_checks.get_json_path_to_resource_field import ( + get_json_path_to_resource_field, +) + + +def test_returns_expected_json_path_without_index(): + """Should form the correct JSON path with no index supplied.""" + assert get_json_path_to_resource_field("myField") == "$.myField" + + +def test_returns_correct_path_with_index(): + """Should form the correct JSON path with a resource index supplied.""" + assert get_json_path_to_resource_field("myField", 2) == "$.resources[2].myField"