diff --git a/cyclonedx/output/__init__.py b/cyclonedx/output/__init__.py index 0a25d2b1..a9a09f1a 100644 --- a/cyclonedx/output/__init__.py +++ b/cyclonedx/output/__init__.py @@ -108,7 +108,7 @@ def get_instance(bom: Bom, output_format: OutputFormat = OutputFormat.XML, module = import_module(f'.{output_format.name.lower()}', __package__) except ImportError as error: # pragma: no cover raise ValueError(f'Unknown output_format: {output_format.name}') from error - output_klass: Optional[Type[BaseOutput]] = module.BY_SCHEMA_VERSION.get(schema_version, None) - if output_klass is None: # pragma: no cover + klass: Optional[Type[BaseOutput]] = module.BY_SCHEMA_VERSION.get(schema_version, None) + if klass is None: # pragma: no cover raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version.name}') - return output_klass(bom=bom) + return klass(bom=bom) diff --git a/cyclonedx/validation/__init__.py b/cyclonedx/validation/__init__.py index c6eada0a..445613f3 100644 --- a/cyclonedx/validation/__init__.py +++ b/cyclonedx/validation/__init__.py @@ -15,7 +15,10 @@ # SPDX-License-Identifier: Apache-2.0 from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Optional, Protocol +from importlib import import_module +from typing import TYPE_CHECKING, Any, Optional, Protocol, Type + +from ..schema import OutputFormat if TYPE_CHECKING: from ..schema import SchemaVersion @@ -59,15 +62,35 @@ class BaseValidator(ABC, Validator): def __init__(self, schema_version: 'SchemaVersion') -> None: self.__schema_version = schema_version if not self._schema_file: - raise ValueError(f'unsupported schema: {schema_version}') + raise ValueError(f'unsupported schema_version: {schema_version}') @property def schema_version(self) -> 'SchemaVersion': """get the schema version.""" return self.__schema_version + @property + @abstractmethod + def output_format(self) -> OutputFormat: + """get the format.""" + ... + @property @abstractmethod def _schema_file(self) -> Optional[str]: """get the schema file according to schema version.""" ... + + +def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion') -> BaseValidator: + """get the default validator for a certain `OutputFormat`""" + if not isinstance(output_format, OutputFormat): + raise TypeError(f"unexpected output_format: {output_format!r}") + try: + module = import_module(f'.{output_format.name.lower()}', __package__) + except ImportError as error: # pragma: no cover + raise ValueError(f'Unknown output_format: {output_format.name}') from error + klass: Optional[Type[BaseValidator]] = getattr(module, f'{output_format.name.capitalize()}Validator', None) + if klass is None: # pragma: no cover + raise ValueError(f'Missing Validator for {output_format.name}') + return klass(schema_version) diff --git a/cyclonedx/validation/json.py b/cyclonedx/validation/json.py index 34c1aca6..edf4616f 100644 --- a/cyclonedx/validation/json.py +++ b/cyclonedx/validation/json.py @@ -20,6 +20,8 @@ from json import loads as json_loads from typing import TYPE_CHECKING, Any, Optional, Tuple +from ..schema import OutputFormat + if TYPE_CHECKING: from ..schema import SchemaVersion @@ -44,6 +46,9 @@ class _BaseJsonValidator(BaseValidator, ABC): + @property + def output_format(self) -> OutputFormat: + return OutputFormat.JSON def __init__(self, schema_version: 'SchemaVersion') -> None: # this is the def that is used for generating the documentation diff --git a/cyclonedx/validation/xml.py b/cyclonedx/validation/xml.py index f49a75f4..18a55061 100644 --- a/cyclonedx/validation/xml.py +++ b/cyclonedx/validation/xml.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any, Optional, Tuple from ..exception import MissingOptionalDependencyException +from ..schema import OutputFormat from ..schema._res import BOM_XML as _S_BOM from . import BaseValidator, ValidationError, Validator @@ -38,6 +39,10 @@ class _BaseXmlValidator(BaseValidator, ABC): + @property + def output_format(self) -> OutputFormat: + return OutputFormat.XML + def __init__(self, schema_version: 'SchemaVersion') -> None: # this is the def that is used for generating the documentation super().__init__(schema_version) diff --git a/examples/complex.py b/examples/complex.py index dede34f5..5d75eb40 100644 --- a/examples/complex.py +++ b/examples/complex.py @@ -7,11 +7,11 @@ from cyclonedx.model import OrganizationalEntity, XsUri from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component, ComponentType +from cyclonedx.output import get_instance as get_outputter from cyclonedx.output.json import JsonV1Dot4 -from cyclonedx.output.xml import XmlV1Dot4 -from cyclonedx.schema import SchemaVersion -from cyclonedx.validation.json import JsonValidator -from cyclonedx.validation.xml import XmlValidator +from cyclonedx.schema import SchemaVersion, OutputFormat +from cyclonedx.validation.json import JsonStrictValidator +from cyclonedx.validation import get_instance as get_validator lc_factory = LicenseChoiceFactory(license_factory=LicenseFactory()) @@ -55,7 +55,7 @@ serialized_json = JsonV1Dot4(bom).output_as_string() print(serialized_json) try: - validation_errors = JsonValidator(SchemaVersion.V1_4).validate_str(serialized_json) + validation_errors = JsonStrictValidator(SchemaVersion.V1_4).validate_str(serialized_json) if validation_errors: print('JSON valid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr) sys.exit(2) @@ -63,10 +63,13 @@ except MissingOptionalDependencyException as error: print('JSON-validation was skipped due to', error) -serialized_xml = XmlV1Dot4(bom).output_as_string() +my_outputter = get_outputter(bom, OutputFormat.XML, SchemaVersion.V1_4) +serialized_xml = my_outputter.output_as_string() print(serialized_xml) try: - validation_errors = XmlValidator(SchemaVersion.V1_4).validate_str(serialized_xml) + validation_errors = get_validator(my_outputter.output_format, + my_outputter.schema_version + ).validate_str(serialized_xml) if validation_errors: print('XML invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr) sys.exit(2) diff --git a/tests/test_output.py b/tests/test_output.py index afecbc20..e9737180 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -31,11 +31,11 @@ @ddt -class Test(TestCase): +class TestTestGetInstance(TestCase): @named_data(*([f'{x[0].name} {x[1].name}', *x] for x in product(OutputFormat, SchemaVersion))) @unpack - def test_get_instance_expected(self, of: OutputFormat, sv: SchemaVersion) -> None: + def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None: bom = Mock(spec=Bom) outputter = get_outputter(bom, of, sv) self.assertIs(outputter.get_bom(), bom) @@ -47,7 +47,7 @@ def test_get_instance_expected(self, of: OutputFormat, sv: SchemaVersion) -> Non *(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion), ) @unpack - def test_get_instance_fails(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None: + def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None: bom = Mock(spec=Bom) with self.assertRaisesRegexp(*raisesRegex): get_outputter(bom, of, sv) diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 00000000..cc221714 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,53 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + + +from itertools import product +from typing import Tuple +from unittest import TestCase + +from ddt import data, ddt, named_data, unpack + +from cyclonedx.schema import OutputFormat, SchemaVersion +from cyclonedx.validation import get_instance as get_validator + +UndefinedFormatVersion = {(OutputFormat.JSON, SchemaVersion.V1_1), (OutputFormat.JSON, SchemaVersion.V1_0), } + + +@ddt +class TestGetInstance(TestCase): + + @named_data(*([f'{f.name} {v.name}', f, v] + for f, v + in product(OutputFormat, SchemaVersion) + if (f, v) not in UndefinedFormatVersion)) + @unpack + def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None: + validator = get_validator(of, sv) + self.assertIs(validator.output_format, of) + self.assertIs(validator.schema_version, sv) + + @data( + *(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion), + *((f, v, (ValueError, f'unsupported schema_version: {v}')) for f, v in UndefinedFormatVersion) + ) + @unpack + def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None: + with self.assertRaisesRegexp(*raisesRegex): + get_validator(of, sv) diff --git a/tests/test_validation_json.py b/tests/test_validation_json.py index c5242b46..18edb10b 100644 --- a/tests/test_validation_json.py +++ b/tests/test_validation_json.py @@ -25,7 +25,7 @@ from ddt import data, ddt, idata, unpack from cyclonedx.exception import MissingOptionalDependencyException -from cyclonedx.schema import SchemaVersion +from cyclonedx.schema import OutputFormat, SchemaVersion from cyclonedx.validation.json import JsonStrictValidator, JsonValidator from tests import TESTDATA_DIRECTORY @@ -44,9 +44,15 @@ def _dp(prefix: str) -> Generator: @ddt class TestJsonValidator(TestCase): - @data(*UNSUPPORTED_SCHEMA_VERSIONS) + @idata(sv for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS) + def test_validator_as_expected(self, schema_version: SchemaVersion) -> None: + validator = JsonValidator(schema_version) + self.assertIs(validator.schema_version, schema_version) + self.assertIs(validator.output_format, OutputFormat.JSON) + + @idata(UNSUPPORTED_SCHEMA_VERSIONS) def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None: - with self.assertRaisesRegex(ValueError, 'unsupported schema:'): + with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'): JsonValidator(schema_version) @idata(_dp('valid')) @@ -80,7 +86,7 @@ class TestJsonStrictValidator(TestCase): @data(*UNSUPPORTED_SCHEMA_VERSIONS) def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None: - with self.assertRaisesRegex(ValueError, 'unsupported schema:'): + with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'): JsonStrictValidator(schema_version) @idata(_dp('valid')) diff --git a/tests/test_validation_xml.py b/tests/test_validation_xml.py index c74fef22..60bc749c 100644 --- a/tests/test_validation_xml.py +++ b/tests/test_validation_xml.py @@ -22,10 +22,10 @@ from typing import Generator from unittest import TestCase -from ddt import data, ddt, idata, unpack +from ddt import ddt, idata, unpack from cyclonedx.exception import MissingOptionalDependencyException -from cyclonedx.schema import SchemaVersion +from cyclonedx.schema import OutputFormat, SchemaVersion from cyclonedx.validation.xml import XmlValidator from tests import TESTDATA_DIRECTORY @@ -44,9 +44,15 @@ def _dp(prefix: str) -> Generator: @ddt class TestXmlValidator(TestCase): - @data(*UNSUPPORTED_SCHEMA_VERSIONS) + @idata(sv for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS) + def test_validator_as_expected(self, schema_version: SchemaVersion) -> None: + validator = XmlValidator(schema_version) + self.assertIs(validator.schema_version, schema_version) + self.assertIs(validator.output_format, OutputFormat.XML) + + @idata(UNSUPPORTED_SCHEMA_VERSIONS) def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None: - with self.assertRaisesRegex(ValueError, 'unsupported schema'): + with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'): XmlValidator(schema_version) @idata(_dp('valid'))