Skip to content

Commit c5e6df9

Browse files
committed
chore: Returns all the errors at ones instead of raising early. Improve tests
Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com>
1 parent b202325 commit c5e6df9

File tree

3 files changed

+172
-138
lines changed

3 files changed

+172
-138
lines changed

src/cloudevents/core/v1/event.py

Lines changed: 85 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,19 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
from typing import Any, Optional, Final
16-
from datetime import datetime
1715
import re
16+
from datetime import datetime
17+
from typing import Any, Final, Optional
18+
19+
from cloudevents.core.v1.exceptions import CloudEventValidationError
1820

19-
REQUIRED_ATTRIBUTES: Final[set[str]] = {"id", "source", "type", "specversion"}
20-
OPTIONAL_ATTRIBUTES: Final[set[str]] = {
21+
REQUIRED_ATTRIBUTES: Final[list[str]] = ["id", "source", "type", "specversion"]
22+
OPTIONAL_ATTRIBUTES: Final[list[str]] = [
2123
"datacontenttype",
2224
"dataschema",
2325
"subject",
2426
"time",
25-
}
27+
]
2628

2729

2830
class CloudEvent:
@@ -55,102 +57,129 @@ def _validate_attribute(attributes: dict[str, Any]) -> None:
5557
5658
See https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#required-attributes
5759
"""
58-
CloudEvent._validate_required_attributes(attributes)
59-
CloudEvent._validate_attribute_types(attributes)
60-
CloudEvent._validate_optional_attributes(attributes)
61-
CloudEvent._validate_extension_attributes(attributes)
60+
errors = {}
61+
errors.update(CloudEvent._validate_required_attributes(attributes))
62+
errors.update(CloudEvent._validate_attribute_types(attributes))
63+
errors.update(CloudEvent._validate_optional_attributes(attributes))
64+
errors.update(CloudEvent._validate_extension_attributes(attributes))
65+
if errors:
66+
raise CloudEventValidationError(errors)
6267

6368
@staticmethod
64-
def _validate_required_attributes(attributes: dict[str, Any]) -> None:
69+
def _validate_required_attributes(
70+
attributes: dict[str, Any],
71+
) -> dict[str, list[str]]:
6572
"""
6673
Validates that all required attributes are present.
6774
6875
:param attributes: The attributes of the CloudEvent instance.
69-
:raises ValueError: If any of the required attributes are missing.
76+
:return: A dictionary of validation error messages.
7077
"""
78+
errors = {}
7179
missing_attributes = [
7280
attr for attr in REQUIRED_ATTRIBUTES if attr not in attributes
7381
]
7482
if missing_attributes:
75-
raise ValueError(
83+
errors["required"] = [
7684
f"Missing required attribute(s): {', '.join(missing_attributes)}"
77-
)
85+
]
86+
return errors
7887

7988
@staticmethod
80-
def _validate_attribute_types(attributes: dict[str, Any]) -> None:
89+
def _validate_attribute_types(attributes: dict[str, Any]) -> dict[str, list[str]]:
8190
"""
8291
Validates the types of the required attributes.
8392
8493
:param attributes: The attributes of the CloudEvent instance.
85-
:raises ValueError: If any of the required attributes have invalid values.
86-
:raises TypeError: If any of the required attributes have invalid types.
87-
"""
88-
if attributes["id"] is None:
89-
raise ValueError("Attribute 'id' must not be None")
90-
if not isinstance(attributes["id"], str):
91-
raise TypeError("Attribute 'id' must be a string")
92-
if not isinstance(attributes["source"], str):
93-
raise TypeError("Attribute 'source' must be a string")
94-
if not isinstance(attributes["type"], str):
95-
raise TypeError("Attribute 'type' must be a string")
96-
if not isinstance(attributes["specversion"], str):
97-
raise TypeError("Attribute 'specversion' must be a string")
98-
if attributes["specversion"] != "1.0":
99-
raise ValueError("Attribute 'specversion' must be '1.0'")
94+
:return: A dictionary of validation error messages.
95+
"""
96+
errors = {}
97+
type_errors = []
98+
if attributes.get("id") is None:
99+
type_errors.append("Attribute 'id' must not be None")
100+
if not isinstance(attributes.get("id"), str):
101+
type_errors.append("Attribute 'id' must be a string")
102+
if not isinstance(attributes.get("source"), str):
103+
type_errors.append("Attribute 'source' must be a string")
104+
if not isinstance(attributes.get("type"), str):
105+
type_errors.append("Attribute 'type' must be a string")
106+
if not isinstance(attributes.get("specversion"), str):
107+
type_errors.append("Attribute 'specversion' must be a string")
108+
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
112+
return errors
100113

101114
@staticmethod
102-
def _validate_optional_attributes(attributes: dict[str, Any]) -> None:
115+
def _validate_optional_attributes(
116+
attributes: dict[str, Any],
117+
) -> dict[str, list[str]]:
103118
"""
104119
Validates the types and values of the optional attributes.
105120
106121
:param attributes: The attributes of the CloudEvent instance.
107-
:raises ValueError: If any of the optional attributes have invalid values.
108-
:raises TypeError: If any of the optional attributes have invalid types.
122+
:return: A dictionary of validation error messages.
109123
"""
124+
errors = {}
125+
optional_errors = []
110126
if "time" in attributes:
111127
if not isinstance(attributes["time"], datetime):
112-
raise TypeError("Attribute 'time' must be a datetime object")
113-
if not attributes["time"].tzinfo:
114-
raise ValueError("Attribute 'time' must be timezone aware")
128+
optional_errors.append("Attribute 'time' must be a datetime object")
129+
if hasattr(attributes["time"], "tzinfo") and not attributes["time"].tzinfo:
130+
optional_errors.append("Attribute 'time' must be timezone aware")
115131
if "subject" in attributes:
116132
if not isinstance(attributes["subject"], str):
117-
raise TypeError("Attribute 'subject' must be a string")
133+
optional_errors.append("Attribute 'subject' must be a string")
118134
if not attributes["subject"]:
119-
raise ValueError("Attribute 'subject' must not be empty")
135+
optional_errors.append("Attribute 'subject' must not be empty")
120136
if "datacontenttype" in attributes:
121137
if not isinstance(attributes["datacontenttype"], str):
122-
raise TypeError("Attribute 'datacontenttype' must be a string")
138+
optional_errors.append("Attribute 'datacontenttype' must be a string")
123139
if not attributes["datacontenttype"]:
124-
raise ValueError("Attribute 'datacontenttype' must not be empty")
140+
optional_errors.append("Attribute 'datacontenttype' must not be empty")
125141
if "dataschema" in attributes:
126142
if not isinstance(attributes["dataschema"], str):
127-
raise TypeError("Attribute 'dataschema' must be a string")
143+
optional_errors.append("Attribute 'dataschema' must be a string")
128144
if not attributes["dataschema"]:
129-
raise ValueError("Attribute 'dataschema' must not be empty")
145+
optional_errors.append("Attribute 'dataschema' must not be empty")
146+
if optional_errors:
147+
errors["optional"] = optional_errors
148+
return errors
130149

131150
@staticmethod
132-
def _validate_extension_attributes(attributes: dict[str, Any]) -> None:
151+
def _validate_extension_attributes(
152+
attributes: dict[str, Any],
153+
) -> dict[str, list[str]]:
133154
"""
134155
Validates the extension attributes.
135156
136157
:param attributes: The attributes of the CloudEvent instance.
137-
:raises ValueError: If any of the extension attributes have invalid values.
138-
"""
139-
for extension_attributes in (
140-
set(attributes.keys()) - REQUIRED_ATTRIBUTES - OPTIONAL_ATTRIBUTES
141-
):
142-
if extension_attributes == "data":
143-
raise ValueError(
158+
:return: A dictionary of validation error messages.
159+
"""
160+
errors = {}
161+
extension_errors = []
162+
extension_attributes = [
163+
key
164+
for key in attributes.keys()
165+
if key not in REQUIRED_ATTRIBUTES and key not in OPTIONAL_ATTRIBUTES
166+
]
167+
for extension_attribute in extension_attributes:
168+
if extension_attribute == "data":
169+
extension_errors.append(
144170
"Extension attribute 'data' is reserved and must not be used"
145171
)
146-
if not (1 <= len(extension_attributes) <= 20):
147-
raise ValueError(
148-
f"Extension attribute '{extension_attributes}' should be between 1 and 20 characters long"
172+
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"
149175
)
150-
if not re.match(r"^[a-z0-9]+$", extension_attributes):
151-
raise ValueError(
152-
f"Extension attribute '{extension_attributes}' should only contain lowercase letters and numbers"
176+
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"
153179
)
180+
if extension_errors:
181+
errors["extensions"] = extension_errors
182+
return errors
154183

155184
def get_id(self) -> str:
156185
"""
@@ -215,7 +244,7 @@ def get_time(self) -> Optional[datetime]:
215244
:return: The time of the event.
216245
"""
217246
return self._attributes.get("time")
218-
247+
219248
def get_extension(self, extension_name: str) -> Any:
220249
"""
221250
Retrieve an extension attribute of the event.

src/cloudevents/core/v1/exceptions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2018-Present The CloudEvents Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
class CloudEventValidationError(Exception):
15+
"""
16+
Custom exception for validation errors.
17+
"""
18+
19+
def __init__(self, errors: dict[str, list[str]]) -> None:
20+
super().__init__("Validation errors occurred")
21+
self.errors = errors
22+
23+
def __str__(self) -> str:
24+
error_messages = [
25+
f"{key}: {', '.join(value)}" for key, value in self.errors.items()
26+
]
27+
return f"{super().__str__()}: {', '.join(error_messages)}"

0 commit comments

Comments
 (0)