Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions cyclonedx/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,42 @@ def __str__(self) -> str:
class SchemabasedValidator(Protocol):
"""Schema-based Validator protocol"""

def validate_str(self, data: str) -> Optional[ValidationError]:
@overload
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
"""Validate a string

:param data: the data string to validate
:param all_errors: whether to return all errors or only (any)one - if any
:return: validation error
:retval None: if ``data`` is valid
:retval ValidationError: if ``data`` is invalid
"""
... # pragma: no cover

def iterate_errors(self, data: str) -> Iterable[ValidationError]:
"""Validate a string, enumerating all the problems.
@overload
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
"""Validate a string

:param data: the data string to validate
:param all_errors: whether to return all errors or only (any)one - if any
:return: validation error
:retval None: if ``data`` is valid
:retval Iterable[ValidationError]: if ``data`` is invalid
"""
... # pragma: no cover

def validate_str(
self, data: str, *,
all_errors: bool = False
) -> Union[None, ValidationError, Iterable[ValidationError]]:
"""Validate a string

:param data: the data string to validate
:return: iterator over the errors
:param all_errors: whether to return all errors or only (any)one - if any
:return: validation error
:retval None: if ``data`` is valid
:retval ValidationError: if ``data`` is invalid and ``all_errors`` is ``False``
:retval Iterable[ValidationError]: if ``data`` is invalid and ``all_errors`` is ``True``
"""
... # pragma: no cover

Expand Down
52 changes: 34 additions & 18 deletions cyclonedx/validation/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

from abc import ABC
from collections.abc import Iterable
from itertools import chain
from json import loads as json_loads
from typing import TYPE_CHECKING, Any, Literal, Optional
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, overload

from ..schema import OutputFormat

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

# region typing-relevant copy from parent class - needed for mypy and doc tools

@overload
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
... # pragma: no cover

@overload
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
... # pragma: no cover

def validate_str(
self, data: str, *, all_errors: bool = False
) -> Union[None, ValidationError, Iterable[ValidationError]]:
... # pragma: no cover

# endregion

if _missing_deps_error: # noqa:C901
__MDERROR = _missing_deps_error

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

def iterate_errors(self, data: str) -> Iterable[ValidationError]:
raise self.__MDERROR[0] from self.__MDERROR[1]
else:
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
json_data = json_loads(data)
validator = self._validator # may throw on error that MUST NOT be caught
yield from validator.iter_errors(json_data)

def validate_str(self, data: str) -> Optional[ValidationError]:
return self._validate_data(
json_loads(data))

def _validate_data(self, data: Any) -> Optional[ValidationError]:
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
self, data: str, *, all_errors: bool = False
) -> Union[None, ValidationError, Iterable[ValidationError]]:
validator = self._validator # may throw on error that MUST NOT be caught
try:
validator.validate(data)
except JsonSchemaValidationError as error:
return _JsonValidationError(error)
return None
structure = json_loads(data)
errors = validator.iter_errors(structure)
first_error = next(errors, None)
if first_error is None:
return None
first_error = _JsonValidationError(first_error)
return chain((first_error,), map(_JsonValidationError, errors)) \
if all_errors \
else first_error

__validator: Optional['JsonSchemaValidator'] = None

Expand Down
53 changes: 32 additions & 21 deletions cyclonedx/validation/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from abc import ABC
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Literal, Optional
from typing import TYPE_CHECKING, Literal, Optional, Union, overload

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

# region typing-relevant copy from parent class - needed for mypy and doc tools

@overload
def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]:
... # pragma: no cover

@overload
def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]:
... # pragma: no cover

def validate_str(
self, data: str, *, all_errors: bool = False
) -> Union[None, ValidationError, Iterable[ValidationError]]:
... # pragma: no cover

# endregion typing-relevant

if _missing_deps_error: # noqa:C901
__MDERROR = _missing_deps_error

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

def iterate_errors(self, data: str) -> Iterable[ValidationError]:
raise self.__MDERROR[0] from self.__MDERROR[1]
else:
def iterate_errors(self, data: str) -> Iterable[ValidationError]:
xml_data = xml_fromstring( # nosec B320
bytes(data, encoding='utf8'),
parser=self.__xml_parser)
def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first
self, data: str, *, all_errors: bool = False
) -> Union[None, ValidationError, Iterable[ValidationError]]:
validator = self._validator # may throw on error that MUST NOT be caught
validator.validate(xml_data)
for error in validator.error_log:
yield ValidationError(error)

def validate_str(self, data: str) -> Optional[ValidationError]:
return self._validate_data(
xml_fromstring( # nosec B320
valid = validator.validate(
xml_fromstring( # nosec B320 -- we use a custom prepared safe parser
bytes(data, encoding='utf8'),
parser=self.__xml_parser))

def _validate_data(self, data: Any) -> Optional[ValidationError]:
validator = self._validator # may throw on error that MUST NOT be caught
if not validator.validate(data):
return ValidationError(validator.error_log.last_error)
return None
if valid:
return None
errors = validator.error_log
return map(ValidationError, errors) \
if all_errors \
else ValidationError(errors.last_error)

__validator: Optional['XMLSchema'] = None

Expand Down
44 changes: 39 additions & 5 deletions tests/test_validation_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_validate_no_none(self, schema_version: SchemaVersion, test_data_file: s
_dp_sv_own(False)
))
@unpack
def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_file: str) -> None:
def test_validate_expected_error_one(self, schema_version: SchemaVersion, test_data_file: str) -> None:
validator = JsonValidator(schema_version)
with open(join(test_data_file)) as tdfh:
test_data = tdfh.read()
Expand All @@ -93,6 +93,25 @@ def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_
self.assertIsNotNone(validation_error)
self.assertIsNotNone(validation_error.data)

@idata(chain(
_dp_sv_tf(False),
_dp_sv_own(False)
))
@unpack
def test_validate_expected_error_iterator(self, schema_version: SchemaVersion, test_data_file: str) -> None:
validator = JsonValidator(schema_version)
with open(join(test_data_file)) as tdfh:
test_data = tdfh.read()
try:
validation_errors = validator.validate_str(test_data, all_errors=True)
except MissingOptionalDependencyException:
self.skipTest('MissingOptionalDependencyException')
self.assertIsNotNone(validation_errors)
validation_errors = tuple(validation_errors)
self.assertGreater(len(validation_errors), 0)
for validation_error in validation_errors:
self.assertIsNotNone(validation_error.data)


@ddt
class TestJsonStrictValidator(TestCase):
Expand All @@ -117,14 +136,12 @@ def test_validate_no_none(self, schema_version: SchemaVersion, test_data_file: s
self.skipTest('MissingOptionalDependencyException')
self.assertIsNone(validation_error)

self.assertEqual(list(validator.iterate_errors(test_data)), [])

@idata(chain(
_dp_sv_tf(False),
_dp_sv_own(False)
))
@unpack
def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_file: str) -> None:
def test_validate_expected_error_one(self, schema_version: SchemaVersion, test_data_file: str) -> None:
validator = JsonStrictValidator(schema_version)
with open(join(test_data_file)) as tdfh:
test_data = tdfh.read()
Expand All @@ -141,4 +158,21 @@ def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_
squeezed_message = validation_error.get_squeezed_message(max_size=100)
self.assertLessEqual(len(squeezed_message), 100, squeezed_message)

self.assertNotEqual(list(validator.iterate_errors(test_data)), [])
@idata(chain(
_dp_sv_tf(False),
_dp_sv_own(False)
))
@unpack
def test_validate_expected_error_iterator(self, schema_version: SchemaVersion, test_data_file: str) -> None:
validator = JsonValidator(schema_version)
with open(join(test_data_file)) as tdfh:
test_data = tdfh.read()
try:
validation_errors = validator.validate_str(test_data, all_errors=True)
except MissingOptionalDependencyException:
self.skipTest('MissingOptionalDependencyException')
self.assertIsNotNone(validation_errors)
validation_errors = tuple(validation_errors)
self.assertGreater(len(validation_errors), 0)
for validation_error in validation_errors:
self.assertIsNotNone(validation_error.data)
23 changes: 19 additions & 4 deletions tests/test_validation_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,12 @@ def test_validate_no_none(self, schema_version: SchemaVersion, test_data_file: s
self.skipTest('MissingOptionalDependencyException')
self.assertIsNone(validation_error)

self.assertEqual(list(validator.iterate_errors(test_data)), [])

@idata(chain(
_dp_sv_tf(False),
_dp_sv_own(False)
))
@unpack
def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_file: str) -> None:
def test_validate_expected_error_one(self, schema_version: SchemaVersion, test_data_file: str) -> None:
validator = XmlValidator(schema_version)
with open(join(test_data_file)) as tdfh:
test_data = tdfh.read()
Expand All @@ -101,4 +99,21 @@ def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_
squeezed_message = validation_error.get_squeezed_message(max_size=100)
self.assertLessEqual(len(squeezed_message), 100, squeezed_message)

self.assertNotEqual(list(validator.iterate_errors(test_data)), [])
@idata(chain(
_dp_sv_tf(False),
_dp_sv_own(False)
))
@unpack
def test_validate_expected_error_iterator(self, schema_version: SchemaVersion, test_data_file: str) -> None:
validator = XmlValidator(schema_version)
with open(join(test_data_file)) as tdfh:
test_data = tdfh.read()
try:
validation_errors = validator.validate_str(test_data, all_errors=True)
except MissingOptionalDependencyException:
self.skipTest('MissingOptionalDependencyException')
self.assertIsNotNone(validation_errors)
validation_errors = tuple(validation_errors)
self.assertGreater(len(validation_errors), 0)
for validation_error in validation_errors:
self.assertIsNotNone(validation_error.data)