Skip to content

Commit

Permalink
feat: easy access validators
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Sep 21, 2023
1 parent 39e0eb9 commit cc5de38
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 19 deletions.
27 changes: 25 additions & 2 deletions cyclonedx/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
5 changes: 5 additions & 0 deletions cyclonedx/validation/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions cyclonedx/validation/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
17 changes: 10 additions & 7 deletions examples/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -55,18 +55,21 @@
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)
print('JSON valid')
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)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
53 changes: 53 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 10 additions & 4 deletions tests/test_validation_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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'))
Expand Down Expand Up @@ -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'))
Expand Down
12 changes: 9 additions & 3 deletions tests/test_validation_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.xml import XmlValidator
from tests import TESTDATA_DIRECTORY

Expand All @@ -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'))
Expand Down

0 comments on commit cc5de38

Please sign in to comment.