Skip to content

Commit 6e68224

Browse files
jkowallecke3krisztian
authored andcommitted
feat: SchemabasedValidator.validate_str can return an iterator over all errors
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 9890561 commit 6e68224

File tree

3 files changed

+91
-43
lines changed

3 files changed

+91
-43
lines changed

cyclonedx/validation/__init__.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,42 @@ def __str__(self) -> str:
111111
class SchemabasedValidator(Protocol):
112112
"""Schema-based Validator protocol"""
113113

114-
def validate_str(self, data: str) -> Optional[ValidationError]:
114+
@overload
115+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
115116
"""Validate a string
116117
117118
:param data: the data string to validate
119+
:param all_errors: whether to return all errors or only the last error - if any
118120
:return: validation error
119121
:retval None: if ``data`` is valid
120122
:retval ValidationError: if ``data`` is invalid
121123
"""
122124
... # pragma: no cover
123125

124-
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
125-
"""Validate a string, enumerating all the problems.
126+
@overload
127+
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
128+
"""Validate a string
129+
130+
:param data: the data string to validate
131+
:param all_errors: whether to return all errors or only the last error - if any
132+
:return: validation error
133+
:retval None: if ``data`` is valid
134+
:retval Iterable[ValidationError]: if ``data`` is invalid
135+
"""
136+
... # pragma: no cover
137+
138+
def validate_str(
139+
self, data: str, *,
140+
all_errors: bool = False
141+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
142+
"""Validate a string
126143
127144
:param data: the data string to validate
128-
:return: iterator over the errors
145+
:param all_errors: whether to return all errors or only the last error - if any
146+
:return: validation error
147+
:retval None: if ``data`` is valid
148+
:retval ValidationError: if ``data`` is invalid and ``all_errors`` is ``False``
149+
:retval Iterable[ValidationError]: if ``data`` is invalid and ``all_errors`` is ``True``
129150
"""
130151
... # pragma: no cover
131152

cyclonedx/validation/json.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020

2121
from abc import ABC
2222
from collections.abc import Iterable
23+
from itertools import chain
2324
from json import loads as json_loads
24-
from typing import TYPE_CHECKING, Any, Literal, Optional
25+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, overload
2526

2627
from ..schema import OutputFormat
2728

@@ -102,31 +103,46 @@ def __init__(self, schema_version: 'SchemaVersion') -> None:
102103
# this is the def that is used for generating the documentation
103104
super().__init__(schema_version)
104105

106+
# region typing-relevant copy from parent class - needed for mypy and doc tools
107+
108+
@overload
109+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
110+
... # pragma: no cover
111+
112+
@overload
113+
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
114+
... # pragma: no cover
115+
116+
def validate_str(
117+
self, data: str, *, all_errors: bool = False
118+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
119+
... # pragma: no cover
120+
121+
# endregion
122+
105123
if _missing_deps_error: # noqa:C901
106124
__MDERROR = _missing_deps_error
107125

108-
def validate_str(self, data: str) -> Optional[ValidationError]:
126+
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
127+
self, data: str, *, all_errors: bool = False
128+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
109129
raise self.__MDERROR[0] from self.__MDERROR[1]
110130

111-
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
112-
raise self.__MDERROR[0] from self.__MDERROR[1]
113131
else:
114-
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
115-
json_data = json_loads(data)
116-
validator = self._validator # may throw on error that MUST NOT be caught
117-
yield from validator.iter_errors(json_data)
118-
119-
def validate_str(self, data: str) -> Optional[ValidationError]:
120-
return self._validate_data(
121-
json_loads(data))
122132

123-
def _validate_data(self, data: Any) -> Optional[ValidationError]:
133+
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
134+
self, data: str, *, all_errors: bool = False
135+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
124136
validator = self._validator # may throw on error that MUST NOT be caught
125-
try:
126-
validator.validate(data)
127-
except JsonSchemaValidationError as error:
128-
return _JsonValidationError(error)
129-
return None
137+
structure = json_loads(data)
138+
errors = validator.iter_errors(structure)
139+
first_error = next(errors, None)
140+
if first_error is None:
141+
return None
142+
first_error = _JsonValidationError(first_error)
143+
return chain((first_error,), map(_JsonValidationError, errors)) \
144+
if all_errors \
145+
else first_error
130146

131147
__validator: Optional['JsonSchemaValidator'] = None
132148

cyclonedx/validation/xml.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from abc import ABC
2222
from collections.abc import Iterable
23-
from typing import TYPE_CHECKING, Any, Literal, Optional
23+
from typing import TYPE_CHECKING, Literal, Optional, Union, overload
2424

2525
from ..exception import MissingOptionalDependencyException
2626
from ..schema import OutputFormat
@@ -54,35 +54,46 @@ def __init__(self, schema_version: 'SchemaVersion') -> None:
5454
# this is the def that is used for generating the documentation
5555
super().__init__(schema_version)
5656

57+
# region typing-relevant copy from parent class - needed for mypy and doc tools
58+
59+
@overload
60+
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
61+
... # pragma: no cover
62+
63+
@overload
64+
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
65+
... # pragma: no cover
66+
67+
def validate_str(
68+
self, data: str, *, all_errors: bool = False
69+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
70+
... # pragma: no cover
71+
72+
# endregion typing-relevant
73+
5774
if _missing_deps_error: # noqa:C901
5875
__MDERROR = _missing_deps_error
5976

60-
def validate_str(self, data: str) -> Optional[ValidationError]:
77+
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
78+
self, data: str, *, all_errors: bool = False
79+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
6180
raise self.__MDERROR[0] from self.__MDERROR[1]
6281

63-
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
64-
raise self.__MDERROR[0] from self.__MDERROR[1]
6582
else:
66-
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
67-
xml_data = xml_fromstring( # nosec B320
68-
bytes(data, encoding='utf8'),
69-
parser=self.__xml_parser)
83+
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
84+
self, data: str, *, all_errors: bool = False
85+
) -> Union[None, ValidationError, Iterable[ValidationError]]:
7086
validator = self._validator # may throw on error that MUST NOT be caught
71-
validator.validate(xml_data)
72-
for error in validator.error_log:
73-
yield ValidationError(error)
74-
75-
def validate_str(self, data: str) -> Optional[ValidationError]:
76-
return self._validate_data(
77-
xml_fromstring( # nosec B320
87+
valid = validator.validate(
88+
xml_fromstring( # nosec B320 -- we use a custom prepared safe parser
7889
bytes(data, encoding='utf8'),
7990
parser=self.__xml_parser))
80-
81-
def _validate_data(self, data: Any) -> Optional[ValidationError]:
82-
validator = self._validator # may throw on error that MUST NOT be caught
83-
if not validator.validate(data):
84-
return ValidationError(validator.error_log.last_error)
85-
return None
91+
if valid:
92+
return None
93+
errors = validator.error_log
94+
return map(ValidationError, errors) \
95+
if all_errors \
96+
else ValidationError(errors.last_error)
8697

8798
__validator: Optional['XMLSchema'] = None
8899

0 commit comments

Comments
 (0)