|
13 | 13 | # under the License. |
14 | 14 |
|
15 | 15 | import re |
| 16 | +from collections import defaultdict |
16 | 17 | from datetime import datetime |
17 | 18 | from typing import Any, Final, Optional |
18 | 19 |
|
19 | | -from cloudevents.core.v1.exceptions import CloudEventValidationError |
| 20 | +from cloudevents.core.v1.exceptions import ( |
| 21 | + BaseCloudEventException, |
| 22 | + CloudEventValidationError, |
| 23 | + CustomExtensionAttributeError, |
| 24 | + InvalidAttributeTypeError, |
| 25 | + MissingRequiredAttributeError, |
| 26 | +) |
20 | 27 |
|
21 | 28 | REQUIRED_ATTRIBUTES: Final[list[str]] = ["id", "source", "type", "specversion"] |
22 | 29 | OPTIONAL_ATTRIBUTES: Final[list[str]] = [ |
@@ -57,128 +64,157 @@ def _validate_attribute(attributes: dict[str, Any]) -> None: |
57 | 64 |
|
58 | 65 | See https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#required-attributes |
59 | 66 | """ |
60 | | - errors = {} |
| 67 | + errors: dict[str, list] = defaultdict(list) |
61 | 68 | errors.update(CloudEvent._validate_required_attributes(attributes)) |
62 | | - errors.update(CloudEvent._validate_attribute_types(attributes)) |
63 | 69 | errors.update(CloudEvent._validate_optional_attributes(attributes)) |
64 | 70 | errors.update(CloudEvent._validate_extension_attributes(attributes)) |
65 | 71 | if errors: |
66 | | - raise CloudEventValidationError(errors) |
| 72 | + raise CloudEventValidationError(dict(errors)) |
67 | 73 |
|
68 | 74 | @staticmethod |
69 | 75 | def _validate_required_attributes( |
70 | 76 | attributes: dict[str, Any], |
71 | | - ) -> dict[str, list[str]]: |
72 | | - """ |
73 | | - Validates that all required attributes are present. |
74 | | -
|
75 | | - :param attributes: The attributes of the CloudEvent instance. |
76 | | - :return: A dictionary of validation error messages. |
77 | | - """ |
78 | | - errors = {} |
79 | | - missing_attributes = [ |
80 | | - attr for attr in REQUIRED_ATTRIBUTES if attr not in attributes |
81 | | - ] |
82 | | - if missing_attributes: |
83 | | - errors["required"] = [ |
84 | | - f"Missing required attribute(s): {', '.join(missing_attributes)}" |
85 | | - ] |
86 | | - return errors |
87 | | - |
88 | | - @staticmethod |
89 | | - def _validate_attribute_types(attributes: dict[str, Any]) -> dict[str, list[str]]: |
| 77 | + ) -> dict[str, list[BaseCloudEventException]]: |
90 | 78 | """ |
91 | 79 | Validates the types of the required attributes. |
92 | 80 |
|
93 | 81 | :param attributes: The attributes of the CloudEvent instance. |
94 | 82 | :return: A dictionary of validation error messages. |
95 | 83 | """ |
96 | | - errors = {} |
97 | | - type_errors = [] |
| 84 | + errors = defaultdict(list) |
| 85 | + |
| 86 | + if "id" not in attributes: |
| 87 | + errors["id"].append(MissingRequiredAttributeError(missing="id")) |
98 | 88 | if attributes.get("id") is None: |
99 | | - type_errors.append("Attribute 'id' must not be None") |
| 89 | + errors["id"].append( |
| 90 | + InvalidAttributeTypeError("Attribute 'id' must not be None") |
| 91 | + ) |
100 | 92 | if not isinstance(attributes.get("id"), str): |
101 | | - type_errors.append("Attribute 'id' must be a string") |
| 93 | + errors["id"].append( |
| 94 | + InvalidAttributeTypeError("Attribute 'id' must be a string") |
| 95 | + ) |
| 96 | + |
| 97 | + if "source" not in attributes: |
| 98 | + errors["source"].append(MissingRequiredAttributeError(missing="source")) |
102 | 99 | if not isinstance(attributes.get("source"), str): |
103 | | - type_errors.append("Attribute 'source' must be a string") |
| 100 | + errors["source"].append( |
| 101 | + InvalidAttributeTypeError("Attribute 'source' must be a string") |
| 102 | + ) |
| 103 | + |
| 104 | + if "type" not in attributes: |
| 105 | + errors["type"].append(MissingRequiredAttributeError(missing="type")) |
104 | 106 | if not isinstance(attributes.get("type"), str): |
105 | | - type_errors.append("Attribute 'type' must be a string") |
| 107 | + errors["type"].append( |
| 108 | + InvalidAttributeTypeError("Attribute 'type' must be a string") |
| 109 | + ) |
| 110 | + |
| 111 | + if "specversion" not in attributes: |
| 112 | + errors["specversion"].append( |
| 113 | + MissingRequiredAttributeError(missing="specversion") |
| 114 | + ) |
106 | 115 | if not isinstance(attributes.get("specversion"), str): |
107 | | - type_errors.append("Attribute 'specversion' must be a string") |
| 116 | + errors["specversion"].append( |
| 117 | + InvalidAttributeTypeError("Attribute 'specversion' must be a string") |
| 118 | + ) |
108 | 119 | if attributes.get("specversion") != "1.0": |
109 | | - type_errors.append("Attribute 'specversion' must be '1.0'") |
110 | | - if type_errors: |
111 | | - errors["type"] = type_errors |
| 120 | + errors["specversion"].append( |
| 121 | + InvalidAttributeTypeError("Attribute 'specversion' must be '1.0'") |
| 122 | + ) |
112 | 123 | return errors |
113 | 124 |
|
114 | 125 | @staticmethod |
115 | 126 | def _validate_optional_attributes( |
116 | 127 | attributes: dict[str, Any], |
117 | | - ) -> dict[str, list[str]]: |
| 128 | + ) -> dict[str, list[BaseCloudEventException]]: |
118 | 129 | """ |
119 | 130 | Validates the types and values of the optional attributes. |
120 | 131 |
|
121 | 132 | :param attributes: The attributes of the CloudEvent instance. |
122 | 133 | :return: A dictionary of validation error messages. |
123 | 134 | """ |
124 | | - errors = {} |
125 | | - optional_errors = [] |
| 135 | + errors = defaultdict(list) |
| 136 | + |
126 | 137 | if "time" in attributes: |
127 | 138 | if not isinstance(attributes["time"], datetime): |
128 | | - optional_errors.append("Attribute 'time' must be a datetime object") |
| 139 | + errors["time"].append( |
| 140 | + InvalidAttributeTypeError( |
| 141 | + "Attribute 'time' must be a datetime object" |
| 142 | + ) |
| 143 | + ) |
129 | 144 | if hasattr(attributes["time"], "tzinfo") and not attributes["time"].tzinfo: |
130 | | - optional_errors.append("Attribute 'time' must be timezone aware") |
| 145 | + errors["time"].append( |
| 146 | + InvalidAttributeTypeError("Attribute 'time' must be timezone aware") |
| 147 | + ) |
131 | 148 | if "subject" in attributes: |
132 | 149 | if not isinstance(attributes["subject"], str): |
133 | | - optional_errors.append("Attribute 'subject' must be a string") |
| 150 | + errors["subject"].append( |
| 151 | + InvalidAttributeTypeError("Attribute 'subject' must be a string") |
| 152 | + ) |
134 | 153 | if not attributes["subject"]: |
135 | | - optional_errors.append("Attribute 'subject' must not be empty") |
| 154 | + errors["subject"].append( |
| 155 | + InvalidAttributeTypeError("Attribute 'subject' must not be empty") |
| 156 | + ) |
136 | 157 | if "datacontenttype" in attributes: |
137 | 158 | if not isinstance(attributes["datacontenttype"], str): |
138 | | - optional_errors.append("Attribute 'datacontenttype' must be a string") |
| 159 | + errors["datacontenttype"].append( |
| 160 | + InvalidAttributeTypeError( |
| 161 | + "Attribute 'datacontenttype' must be a string" |
| 162 | + ) |
| 163 | + ) |
139 | 164 | if not attributes["datacontenttype"]: |
140 | | - optional_errors.append("Attribute 'datacontenttype' must not be empty") |
| 165 | + errors["datacontenttype"].append( |
| 166 | + InvalidAttributeTypeError( |
| 167 | + "Attribute 'datacontenttype' must not be empty" |
| 168 | + ) |
| 169 | + ) |
141 | 170 | if "dataschema" in attributes: |
142 | 171 | if not isinstance(attributes["dataschema"], str): |
143 | | - optional_errors.append("Attribute 'dataschema' must be a string") |
| 172 | + errors["dataschema"].append( |
| 173 | + InvalidAttributeTypeError("Attribute 'dataschema' must be a string") |
| 174 | + ) |
144 | 175 | if not attributes["dataschema"]: |
145 | | - optional_errors.append("Attribute 'dataschema' must not be empty") |
146 | | - if optional_errors: |
147 | | - errors["optional"] = optional_errors |
| 176 | + errors["dataschema"].append( |
| 177 | + InvalidAttributeTypeError( |
| 178 | + "Attribute 'dataschema' must not be empty" |
| 179 | + ) |
| 180 | + ) |
148 | 181 | return errors |
149 | 182 |
|
150 | 183 | @staticmethod |
151 | 184 | def _validate_extension_attributes( |
152 | 185 | attributes: dict[str, Any], |
153 | | - ) -> dict[str, list[str]]: |
| 186 | + ) -> dict[str, list[BaseCloudEventException]]: |
154 | 187 | """ |
155 | 188 | Validates the extension attributes. |
156 | 189 |
|
157 | 190 | :param attributes: The attributes of the CloudEvent instance. |
158 | 191 | :return: A dictionary of validation error messages. |
159 | 192 | """ |
160 | | - errors = {} |
161 | | - extension_errors = [] |
| 193 | + errors = defaultdict(list) |
162 | 194 | extension_attributes = [ |
163 | 195 | key |
164 | 196 | for key in attributes.keys() |
165 | 197 | if key not in REQUIRED_ATTRIBUTES and key not in OPTIONAL_ATTRIBUTES |
166 | 198 | ] |
167 | 199 | for extension_attribute in extension_attributes: |
168 | 200 | if extension_attribute == "data": |
169 | | - extension_errors.append( |
170 | | - "Extension attribute 'data' is reserved and must not be used" |
| 201 | + errors[extension_attribute].append( |
| 202 | + CustomExtensionAttributeError( |
| 203 | + "Extension attribute 'data' is reserved and must not be used" |
| 204 | + ) |
171 | 205 | ) |
172 | 206 | if not (1 <= len(extension_attribute) <= 20): |
173 | | - extension_errors.append( |
174 | | - f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long" |
| 207 | + errors[extension_attribute].append( |
| 208 | + CustomExtensionAttributeError( |
| 209 | + f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long" |
| 210 | + ) |
175 | 211 | ) |
176 | 212 | if not re.match(r"^[a-z0-9]+$", extension_attribute): |
177 | | - extension_errors.append( |
178 | | - f"Extension attribute '{extension_attribute}' should only contain lowercase letters and numbers" |
| 213 | + errors[extension_attribute].append( |
| 214 | + CustomExtensionAttributeError( |
| 215 | + f"Extension attribute '{extension_attribute}' should only contain lowercase letters and numbers" |
| 216 | + ) |
179 | 217 | ) |
180 | | - if extension_errors: |
181 | | - errors["extensions"] = extension_errors |
182 | 218 | return errors |
183 | 219 |
|
184 | 220 | def get_id(self) -> str: |
|
0 commit comments