Skip to content

Commit 8e5deee

Browse files
committed
feat: validators return specific error classes
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 34a11aa commit 8e5deee

File tree

2 files changed

+36
-16
lines changed

2 files changed

+36
-16
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

0 commit comments

Comments
 (0)