Skip to content

Commit 23a0f72

Browse files
authored
feat: validators return specific error classes (#840)
part of - #827 in preparation for - #836 --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 34a11aa commit 23a0f72

File tree

4 files changed

+48
-28
lines changed

4 files changed

+48
-28
lines changed

cyclonedx/validation/json.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818

19-
__all__ = ['JsonValidator', 'JsonStrictValidator']
19+
__all__ = ['JsonValidator', 'JsonStrictValidator', 'JsonValidationError']
2020

2121
from abc import ABC
2222
from collections.abc import Iterable
@@ -40,6 +40,7 @@
4040
from referencing.jsonschema import DRAFT7
4141

4242
if TYPE_CHECKING: # pragma: no cover
43+
from jsonschema.exceptions import ValidationError as JsonSchemaValidationError # type:ignore[import-untyped]
4344
from jsonschema.protocols import Validator as JsonSchemaValidator # type:ignore[import-untyped]
4445
except ImportError as err:
4546
_missing_deps_error = MissingOptionalDependencyException(
@@ -48,6 +49,14 @@
4849
), err
4950

5051

52+
class JsonValidationError(ValidationError):
53+
@classmethod
54+
def _make_from_jsve(cls, e: 'JsonSchemaValidationError') -> 'JsonValidationError':
55+
"""⚠️ This is an internal API. It is not part of the public interface and may change without notice."""
56+
# in preparation for https://github.com/CycloneDX/cyclonedx-python-lib/pull/836
57+
return cls(e)
58+
59+
5160
class _BaseJsonValidator(BaseSchemabasedValidator, ABC):
5261
@property
5362
def output_format(self) -> Literal[OutputFormat.JSON]:
@@ -60,16 +69,16 @@ def __init__(self, schema_version: 'SchemaVersion') -> None:
6069
# region typing-relevant copy from parent class - needed for mypy and doc tools
6170

6271
@overload
63-
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
72+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[JsonValidationError]:
6473
... # pragma: no cover
6574

6675
@overload
67-
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
76+
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[JsonValidationError]]:
6877
... # pragma: no cover
6978

7079
def validate_str(
7180
self, data: str, *, all_errors: bool = False
72-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
81+
) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]:
7382
... # pragma: no cover
7483

7584
# endregion
@@ -79,22 +88,22 @@ def validate_str(
7988

8089
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
8190
self, data: str, *, all_errors: bool = False
82-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
91+
) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]:
8392
raise self.__MDERROR[0] from self.__MDERROR[1]
8493

8594
else:
8695

8796
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
8897
self, data: str, *, all_errors: bool = False
89-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
98+
) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]:
9099
validator = self._validator # may throw on error that MUST NOT be caught
91100
structure = json_loads(data)
92101
errors = validator.iter_errors(structure)
93102
first_error = next(errors, None)
94103
if first_error is None:
95104
return None
96-
first_error = ValidationError(first_error)
97-
return chain((first_error,), map(ValidationError, errors)) \
105+
first_error = JsonValidationError._make_from_jsve(first_error)
106+
return chain((first_error,), map(JsonValidationError._make_from_jsve, errors)) \
98107
if all_errors \
99108
else first_error
100109

cyclonedx/validation/xml.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818

19-
__all__ = ['XmlValidator']
19+
__all__ = ['XmlValidator', 'XmlValidationError']
2020

2121
from abc import ABC
2222
from collections.abc import Iterable
@@ -37,13 +37,24 @@
3737
XMLSchema,
3838
fromstring as xml_fromstring,
3939
)
40+
41+
if TYPE_CHECKING: # pragma: no cover
42+
from lxml.etree import _LogEntry as _XmlLogEntry
4043
except ImportError as err:
4144
_missing_deps_error = MissingOptionalDependencyException(
4245
'This functionality requires optional dependencies.\n'
4346
'Please install `cyclonedx-python-lib` with the extra "xml-validation".\n'
4447
), err
4548

4649

50+
class XmlValidationError(ValidationError):
51+
@classmethod
52+
def _make_from_xle(cls, e: '_XmlLogEntry') -> 'XmlValidationError':
53+
"""⚠️ This is an internal API. It is not part of the public interface and may change without notice."""
54+
# in preparation for https://github.com/CycloneDX/cyclonedx-python-lib/pull/836
55+
return cls(e)
56+
57+
4758
class _BaseXmlValidator(BaseSchemabasedValidator, ABC):
4859

4960
@property
@@ -57,16 +68,16 @@ def __init__(self, schema_version: 'SchemaVersion') -> None:
5768
# region typing-relevant copy from parent class - needed for mypy and doc tools
5869

5970
@overload
60-
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
71+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[XmlValidationError]:
6172
... # pragma: no cover
6273

6374
@overload
64-
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
75+
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[XmlValidationError]]:
6576
... # pragma: no cover
6677

6778
def validate_str(
6879
self, data: str, *, all_errors: bool = False
69-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
80+
) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]:
7081
... # pragma: no cover
7182

7283
# endregion typing-relevant
@@ -76,13 +87,13 @@ def validate_str(
7687

7788
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
7889
self, data: str, *, all_errors: bool = False
79-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
90+
) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]:
8091
raise self.__MDERROR[0] from self.__MDERROR[1]
8192

8293
else:
8394
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
8495
self, data: str, *, all_errors: bool = False
85-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
96+
) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]:
8697
validator = self._validator # may throw on error that MUST NOT be caught
8798
valid = validator.validate(
8899
xml_fromstring( # nosec B320 -- we use a custom prepared safe parser
@@ -91,9 +102,9 @@ def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers
91102
if valid:
92103
return None
93104
errors = validator.error_log
94-
return map(ValidationError, errors) \
105+
return map(XmlValidationError._make_from_xle, errors) \
95106
if all_errors \
96-
else ValidationError(errors.last_error)
107+
else XmlValidationError._make_from_xle(errors.last_error)
97108

98109
__validator: Optional['XMLSchema'] = None
99110

examples/complex_deserialize.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@
147147
}"""
148148
my_json_validator = JsonStrictValidator(SchemaVersion.V1_6)
149149
try:
150-
validation_errors = my_json_validator.validate_str(json_data)
151-
if validation_errors:
152-
print('JSON invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr)
150+
json_validation_errors = my_json_validator.validate_str(json_data)
151+
if json_validation_errors:
152+
print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr)
153153
sys.exit(2)
154154
print('JSON valid')
155155
except MissingOptionalDependencyException as error:
@@ -248,9 +248,9 @@
248248
</bom>"""
249249
my_xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_6)
250250
try:
251-
validation_errors = my_xml_validator.validate_str(xml_data)
252-
if validation_errors:
253-
print('XML invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr)
251+
xml_validation_errors = my_xml_validator.validate_str(xml_data)
252+
if xml_validation_errors:
253+
print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr)
254254
sys.exit(2)
255255
print('XML valid')
256256
except MissingOptionalDependencyException as error:

examples/complex_serialize.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@
9191
print(serialized_json)
9292
my_json_validator = JsonStrictValidator(SchemaVersion.V1_6)
9393
try:
94-
validation_errors = my_json_validator.validate_str(serialized_json)
95-
if validation_errors:
96-
print('JSON invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr)
94+
json_validation_errors = my_json_validator.validate_str(serialized_json)
95+
if json_validation_errors:
96+
print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr)
9797
sys.exit(2)
9898
print('JSON valid')
9999
except MissingOptionalDependencyException as error:
@@ -112,9 +112,9 @@
112112
my_xml_validator: 'XmlValidator' = make_schemabased_validator(
113113
my_xml_outputter.output_format, my_xml_outputter.schema_version)
114114
try:
115-
validation_errors = my_xml_validator.validate_str(serialized_xml)
116-
if validation_errors:
117-
print('XML invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr)
115+
xml_validation_errors = my_xml_validator.validate_str(serialized_xml)
116+
if xml_validation_errors:
117+
print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr)
118118
sys.exit(2)
119119
print('XML valid')
120120
except MissingOptionalDependencyException as error:

0 commit comments

Comments
 (0)