Skip to content

Commit a40d7f1

Browse files
committed
feat!: useful validation errors
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 0bdc6d6 commit a40d7f1

File tree

5 files changed

+55
-39
lines changed

5 files changed

+55
-39
lines changed

cyclonedx/validation/__init__.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from abc import ABC, abstractmethod
2020
from collections.abc import Iterable
21-
from typing import TYPE_CHECKING, Any, Literal, Optional, Protocol, Union, overload
21+
from typing import TYPE_CHECKING, Literal, Optional, Protocol, Union, overload
2222

2323
from ..schema import OutputFormat
2424

@@ -29,22 +29,20 @@
2929

3030

3131
class ValidationError:
32-
"""Validation failed with this specific error.
32+
"""Validation failed with this specific error. """
3333

34-
Use :attr:`~data` to access the content.
35-
"""
36-
37-
data: Any
38-
"""Raw error data from one of the underlying validation methods."""
34+
def __init__(self, message: str) -> None:
35+
self._message = message
3936

40-
def __init__(self, data: Any) -> None:
41-
self.data = data
37+
@property
38+
def message(self) -> str:
39+
return self._message
4240

4341
def __repr__(self) -> str:
44-
return repr(self.data)
42+
return f'<{self.__class__.__qualname__} {self._message!r}>'
4543

4644
def __str__(self) -> str:
47-
return str(self.data)
45+
return self._message
4846

4947

5048
class SchemabasedValidator(Protocol):

cyclonedx/validation/json.py

Lines changed: 16 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,13 @@
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+
return cls(e.message) # TODO: shorten and more useful message?
57+
58+
5159
class _BaseJsonValidator(BaseSchemabasedValidator, ABC):
5260
@property
5361
def output_format(self) -> Literal[OutputFormat.JSON]:
@@ -60,16 +68,16 @@ def __init__(self, schema_version: 'SchemaVersion') -> None:
6068
# region typing-relevant copy from parent class - needed for mypy and doc tools
6169

6270
@overload
63-
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
71+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[JsonValidationError]:
6472
... # pragma: no cover
6573

6674
@overload
67-
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[JsonValidationError]]:
6876
... # pragma: no cover
6977

7078
def validate_str(
7179
self, data: str, *, all_errors: bool = False
72-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
80+
) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]:
7381
... # pragma: no cover
7482

7583
# endregion
@@ -79,22 +87,22 @@ def validate_str(
7987

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

8593
else:
8694

8795
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
8896
self, data: str, *, all_errors: bool = False
89-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
97+
) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]:
9098
validator = self._validator # may throw on error that MUST NOT be caught
9199
structure = json_loads(data)
92100
errors = validator.iter_errors(structure)
93101
first_error = next(errors, None)
94102
if first_error is None:
95103
return None
96-
first_error = ValidationError(first_error)
97-
return chain((first_error,), map(ValidationError, errors)) \
104+
first_error = JsonValidationError._make_from_jsve(first_error)
105+
return chain((first_error,), map(JsonValidationError._make_from_jsve, errors)) \
98106
if all_errors \
99107
else first_error
100108

cyclonedx/validation/xml.py

Lines changed: 18 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,23 @@
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+
return cls(e.message) # TODO: shorten and more useful message?
55+
56+
4757
class _BaseXmlValidator(BaseSchemabasedValidator, ABC):
4858

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

5969
@overload
60-
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
70+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[XmlValidationError]:
6171
... # pragma: no cover
6272

6373
@overload
64-
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
74+
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[XmlValidationError]]:
6575
... # pragma: no cover
6676

6777
def validate_str(
6878
self, data: str, *, all_errors: bool = False
69-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
79+
) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]:
7080
... # pragma: no cover
7181

7282
# endregion typing-relevant
@@ -76,13 +86,13 @@ def validate_str(
7686

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

8292
else:
8393
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
8494
self, data: str, *, all_errors: bool = False
85-
) -> Union[None, ValidationError, Iterable[ValidationError]]:
95+
) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]:
8696
validator = self._validator # may throw on error that MUST NOT be caught
8797
valid = validator.validate(
8898
xml_fromstring( # nosec B320 -- we use a custom prepared safe parser
@@ -91,9 +101,9 @@ def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers
91101
if valid:
92102
return None
93103
errors = validator.error_log
94-
return map(ValidationError, errors) \
104+
return map(XmlValidationError._make_from_xle, errors) \
95105
if all_errors \
96-
else ValidationError(errors.last_error)
106+
else XmlValidationError._make_from_xle(errors.last_error)
97107

98108
__validator: Optional['XMLSchema'] = None
99109

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)