|
| 1 | +import re |
1 | 2 | from dataclasses import dataclass |
2 | 3 | from typing import Any, Callable |
3 | 4 |
|
4 | 5 | from check_datapackage.internals import ( |
| 6 | + DescriptorField, |
5 | 7 | _filter, |
6 | 8 | _flat_map, |
| 9 | + _get_direct_jsonpaths, |
7 | 10 | _get_fields_at_jsonpath, |
8 | 11 | _map, |
9 | 12 | ) |
10 | 13 | from check_datapackage.issue import Issue |
11 | 14 |
|
12 | 15 |
|
13 | | -@dataclass |
| 16 | +@dataclass(frozen=True) |
14 | 17 | class CustomCheck: |
15 | | - """A custom check to be done on a Data Package descriptor. |
| 18 | + """A custom check to be done on Data Package metadata. |
16 | 19 |
|
17 | 20 | Attributes: |
18 | 21 | jsonpath (str): The location of the field or fields the custom check applies to, |
@@ -45,49 +48,101 @@ class CustomCheck: |
45 | 48 | check: Callable[[Any], bool] |
46 | 49 | type: str = "custom" |
47 | 50 |
|
| 51 | + def apply(self, properties: dict[str, Any]) -> list[Issue]: |
| 52 | + """Applies the custom check to the properties. |
48 | 53 |
|
49 | | -def apply_custom_checks( |
50 | | - custom_checks: list[CustomCheck], descriptor: dict[str, Any] |
51 | | -) -> list[Issue]: |
52 | | - """Checks the descriptor for all custom checks and creates issues if any fail. |
| 54 | + Args: |
| 55 | + properties: The properties to check. |
53 | 56 |
|
54 | | - Args: |
55 | | - custom_checks: The custom checks to apply to the descriptor. |
56 | | - descriptor: The descriptor to check. |
| 57 | + Returns: |
| 58 | + A list of `Issue`s. |
| 59 | + """ |
| 60 | + fields: list[DescriptorField] = _get_fields_at_jsonpath( |
| 61 | + self.jsonpath, |
| 62 | + properties, |
| 63 | + ) |
| 64 | + matches: list[DescriptorField] = _filter( |
| 65 | + fields, |
| 66 | + lambda field: not self.check(field.value), |
| 67 | + ) |
| 68 | + return _map( |
| 69 | + matches, |
| 70 | + lambda field: Issue( |
| 71 | + jsonpath=field.jsonpath, type=self.type, message=self.message |
| 72 | + ), |
| 73 | + ) |
57 | 74 |
|
58 | | - Returns: |
59 | | - A list of `Issue`s. |
| 75 | + |
| 76 | +@dataclass(frozen=True) |
| 77 | +class RequiredCheck: |
| 78 | + """Set a specific property as required. |
| 79 | +
|
| 80 | + Attributes: |
| 81 | + jsonpath (str): The location of the field or fields, expressed in [JSON |
| 82 | + path](https://jg-rp.github.io/python-jsonpath/syntax/) notation, to which |
| 83 | + the check applies (e.g., `$.resources[*].name`). |
| 84 | + message (str): The message that is shown when the check fails. |
| 85 | +
|
| 86 | + Examples: |
| 87 | + ```{python} |
| 88 | + import check_datapackage as cdp |
| 89 | + required_title_check = cdp.RequiredCheck( |
| 90 | + jsonpath="$.title", |
| 91 | + message="A title is required.", |
| 92 | + ) |
| 93 | + ``` |
60 | 94 | """ |
61 | | - return _flat_map( |
62 | | - custom_checks, |
63 | | - lambda custom_check: _apply_custom_check(custom_check, descriptor), |
64 | | - ) |
65 | 95 |
|
| 96 | + jsonpath: str |
| 97 | + message: str |
| 98 | + |
| 99 | + def apply(self, properties: dict[str, Any]) -> list[Issue]: |
| 100 | + """Applies the required check to the properties. |
| 101 | +
|
| 102 | + Args: |
| 103 | + properties: The properties to check. |
| 104 | +
|
| 105 | + Returns: |
| 106 | + A list of `Issue`s. |
| 107 | + """ |
| 108 | + # TODO: check jsonpath when checking other user input |
| 109 | + field_name_match = re.search(r"(?<!\.)(\.\w+)$", self.jsonpath) |
| 110 | + if not field_name_match: |
| 111 | + return [] |
| 112 | + field_name = field_name_match.group(1) |
| 113 | + |
| 114 | + matching_paths = _get_direct_jsonpaths(self.jsonpath, properties) |
| 115 | + indirect_parent_path = self.jsonpath.removesuffix(field_name) |
| 116 | + direct_parent_paths = _get_direct_jsonpaths(indirect_parent_path, properties) |
| 117 | + missing_paths = _filter( |
| 118 | + direct_parent_paths, |
| 119 | + lambda path: f"{path}{field_name}" not in matching_paths, |
| 120 | + ) |
| 121 | + return _map( |
| 122 | + missing_paths, |
| 123 | + lambda path: Issue( |
| 124 | + jsonpath=path + field_name, |
| 125 | + type="required", |
| 126 | + message=self.message, |
| 127 | + ), |
| 128 | + ) |
66 | 129 |
|
67 | | -def _apply_custom_check( |
68 | | - custom_check: CustomCheck, descriptor: dict[str, Any] |
69 | | -) -> list[Issue]: |
70 | | - """Applies the custom check to the descriptor. |
71 | 130 |
|
72 | | - If any fields fail the custom check, this function creates a list of issues |
73 | | - for those fields. |
| 131 | +def apply_extensions( |
| 132 | + properties: dict[str, Any], |
| 133 | + # TODO: extensions: Extensions once Extensions implemented |
| 134 | + extensions: list[CustomCheck | RequiredCheck], |
| 135 | +) -> list[Issue]: |
| 136 | + """Applies the extension checks to the properties. |
74 | 137 |
|
75 | 138 | Args: |
76 | | - custom_check: The custom check to apply to the descriptor. |
77 | | - descriptor: The descriptor to check. |
| 139 | + properties: The properties to check. |
| 140 | + extensions: The user-defined extensions to apply to the properties. |
78 | 141 |
|
79 | 142 | Returns: |
80 | 143 | A list of `Issue`s. |
81 | 144 | """ |
82 | | - matching_fields = _get_fields_at_jsonpath(custom_check.jsonpath, descriptor) |
83 | | - failed_fields = _filter( |
84 | | - matching_fields, lambda field: not custom_check.check(field.value) |
85 | | - ) |
86 | | - return _map( |
87 | | - failed_fields, |
88 | | - lambda field: Issue( |
89 | | - jsonpath=field.jsonpath, |
90 | | - type=custom_check.type, |
91 | | - message=custom_check.message, |
92 | | - ), |
| 145 | + return _flat_map( |
| 146 | + extensions, |
| 147 | + lambda extension: extension.apply(properties), |
93 | 148 | ) |
0 commit comments