Skip to content

Commit cc5de38

Browse files
committed
feat: easy access validators
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 39e0eb9 commit cc5de38

File tree

8 files changed

+120
-19
lines changed

8 files changed

+120
-19
lines changed

cyclonedx/validation/__init__.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
# SPDX-License-Identifier: Apache-2.0
1616

1717
from abc import ABC, abstractmethod
18-
from typing import TYPE_CHECKING, Any, Optional, Protocol
18+
from importlib import import_module
19+
from typing import TYPE_CHECKING, Any, Optional, Protocol, Type
20+
21+
from ..schema import OutputFormat
1922

2023
if TYPE_CHECKING:
2124
from ..schema import SchemaVersion
@@ -59,15 +62,35 @@ class BaseValidator(ABC, Validator):
5962
def __init__(self, schema_version: 'SchemaVersion') -> None:
6063
self.__schema_version = schema_version
6164
if not self._schema_file:
62-
raise ValueError(f'unsupported schema: {schema_version}')
65+
raise ValueError(f'unsupported schema_version: {schema_version}')
6366

6467
@property
6568
def schema_version(self) -> 'SchemaVersion':
6669
"""get the schema version."""
6770
return self.__schema_version
6871

72+
@property
73+
@abstractmethod
74+
def output_format(self) -> OutputFormat:
75+
"""get the format."""
76+
...
77+
6978
@property
7079
@abstractmethod
7180
def _schema_file(self) -> Optional[str]:
7281
"""get the schema file according to schema version."""
7382
...
83+
84+
85+
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion') -> BaseValidator:
86+
"""get the default validator for a certain `OutputFormat`"""
87+
if not isinstance(output_format, OutputFormat):
88+
raise TypeError(f"unexpected output_format: {output_format!r}")
89+
try:
90+
module = import_module(f'.{output_format.name.lower()}', __package__)
91+
except ImportError as error: # pragma: no cover
92+
raise ValueError(f'Unknown output_format: {output_format.name}') from error
93+
klass: Optional[Type[BaseValidator]] = getattr(module, f'{output_format.name.capitalize()}Validator', None)
94+
if klass is None: # pragma: no cover
95+
raise ValueError(f'Missing Validator for {output_format.name}')
96+
return klass(schema_version)

cyclonedx/validation/json.py

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from json import loads as json_loads
2121
from typing import TYPE_CHECKING, Any, Optional, Tuple
2222

23+
from ..schema import OutputFormat
24+
2325
if TYPE_CHECKING:
2426
from ..schema import SchemaVersion
2527

@@ -44,6 +46,9 @@
4446

4547

4648
class _BaseJsonValidator(BaseValidator, ABC):
49+
@property
50+
def output_format(self) -> OutputFormat:
51+
return OutputFormat.JSON
4752

4853
def __init__(self, schema_version: 'SchemaVersion') -> None:
4954
# this is the def that is used for generating the documentation

cyclonedx/validation/xml.py

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import TYPE_CHECKING, Any, Optional, Tuple
2121

2222
from ..exception import MissingOptionalDependencyException
23+
from ..schema import OutputFormat
2324
from ..schema._res import BOM_XML as _S_BOM
2425
from . import BaseValidator, ValidationError, Validator
2526

@@ -38,6 +39,10 @@
3839

3940
class _BaseXmlValidator(BaseValidator, ABC):
4041

42+
@property
43+
def output_format(self) -> OutputFormat:
44+
return OutputFormat.XML
45+
4146
def __init__(self, schema_version: 'SchemaVersion') -> None:
4247
# this is the def that is used for generating the documentation
4348
super().__init__(schema_version)

examples/complex.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
from cyclonedx.model import OrganizationalEntity, XsUri
88
from cyclonedx.model.bom import Bom
99
from cyclonedx.model.component import Component, ComponentType
10+
from cyclonedx.output import get_instance as get_outputter
1011
from cyclonedx.output.json import JsonV1Dot4
11-
from cyclonedx.output.xml import XmlV1Dot4
12-
from cyclonedx.schema import SchemaVersion
13-
from cyclonedx.validation.json import JsonValidator
14-
from cyclonedx.validation.xml import XmlValidator
12+
from cyclonedx.schema import SchemaVersion, OutputFormat
13+
from cyclonedx.validation.json import JsonStrictValidator
14+
from cyclonedx.validation import get_instance as get_validator
1515

1616
lc_factory = LicenseChoiceFactory(license_factory=LicenseFactory())
1717

@@ -55,18 +55,21 @@
5555
serialized_json = JsonV1Dot4(bom).output_as_string()
5656
print(serialized_json)
5757
try:
58-
validation_errors = JsonValidator(SchemaVersion.V1_4).validate_str(serialized_json)
58+
validation_errors = JsonStrictValidator(SchemaVersion.V1_4).validate_str(serialized_json)
5959
if validation_errors:
6060
print('JSON valid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr)
6161
sys.exit(2)
6262
print('JSON valid')
6363
except MissingOptionalDependencyException as error:
6464
print('JSON-validation was skipped due to', error)
6565

66-
serialized_xml = XmlV1Dot4(bom).output_as_string()
66+
my_outputter = get_outputter(bom, OutputFormat.XML, SchemaVersion.V1_4)
67+
serialized_xml = my_outputter.output_as_string()
6768
print(serialized_xml)
6869
try:
69-
validation_errors = XmlValidator(SchemaVersion.V1_4).validate_str(serialized_xml)
70+
validation_errors = get_validator(my_outputter.output_format,
71+
my_outputter.schema_version
72+
).validate_str(serialized_xml)
7073
if validation_errors:
7174
print('XML invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr)
7275
sys.exit(2)

tests/test_output.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131

3232

3333
@ddt
34-
class Test(TestCase):
34+
class TestTestGetInstance(TestCase):
3535

3636
@named_data(*([f'{x[0].name} {x[1].name}', *x] for x in product(OutputFormat, SchemaVersion)))
3737
@unpack
38-
def test_get_instance_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
38+
def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
3939
bom = Mock(spec=Bom)
4040
outputter = get_outputter(bom, of, sv)
4141
self.assertIs(outputter.get_bom(), bom)
@@ -47,7 +47,7 @@ def test_get_instance_expected(self, of: OutputFormat, sv: SchemaVersion) -> Non
4747
*(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion),
4848
)
4949
@unpack
50-
def test_get_instance_fails(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:
50+
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:
5151
bom = Mock(spec=Bom)
5252
with self.assertRaisesRegexp(*raisesRegex):
5353
get_outputter(bom, of, sv)

tests/test_validation.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# encoding: utf-8
2+
3+
# This file is part of CycloneDX Python Lib
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
# Copyright (c) OWASP Foundation. All Rights Reserved.
19+
20+
21+
from itertools import product
22+
from typing import Tuple
23+
from unittest import TestCase
24+
25+
from ddt import data, ddt, named_data, unpack
26+
27+
from cyclonedx.schema import OutputFormat, SchemaVersion
28+
from cyclonedx.validation import get_instance as get_validator
29+
30+
UndefinedFormatVersion = {(OutputFormat.JSON, SchemaVersion.V1_1), (OutputFormat.JSON, SchemaVersion.V1_0), }
31+
32+
33+
@ddt
34+
class TestGetInstance(TestCase):
35+
36+
@named_data(*([f'{f.name} {v.name}', f, v]
37+
for f, v
38+
in product(OutputFormat, SchemaVersion)
39+
if (f, v) not in UndefinedFormatVersion))
40+
@unpack
41+
def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
42+
validator = get_validator(of, sv)
43+
self.assertIs(validator.output_format, of)
44+
self.assertIs(validator.schema_version, sv)
45+
46+
@data(
47+
*(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion),
48+
*((f, v, (ValueError, f'unsupported schema_version: {v}')) for f, v in UndefinedFormatVersion)
49+
)
50+
@unpack
51+
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:
52+
with self.assertRaisesRegexp(*raisesRegex):
53+
get_validator(of, sv)

tests/test_validation_json.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ddt import data, ddt, idata, unpack
2626

2727
from cyclonedx.exception import MissingOptionalDependencyException
28-
from cyclonedx.schema import SchemaVersion
28+
from cyclonedx.schema import OutputFormat, SchemaVersion
2929
from cyclonedx.validation.json import JsonStrictValidator, JsonValidator
3030
from tests import TESTDATA_DIRECTORY
3131

@@ -44,9 +44,15 @@ def _dp(prefix: str) -> Generator:
4444
@ddt
4545
class TestJsonValidator(TestCase):
4646

47-
@data(*UNSUPPORTED_SCHEMA_VERSIONS)
47+
@idata(sv for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS)
48+
def test_validator_as_expected(self, schema_version: SchemaVersion) -> None:
49+
validator = JsonValidator(schema_version)
50+
self.assertIs(validator.schema_version, schema_version)
51+
self.assertIs(validator.output_format, OutputFormat.JSON)
52+
53+
@idata(UNSUPPORTED_SCHEMA_VERSIONS)
4854
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
49-
with self.assertRaisesRegex(ValueError, 'unsupported schema:'):
55+
with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'):
5056
JsonValidator(schema_version)
5157

5258
@idata(_dp('valid'))
@@ -80,7 +86,7 @@ class TestJsonStrictValidator(TestCase):
8086

8187
@data(*UNSUPPORTED_SCHEMA_VERSIONS)
8288
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
83-
with self.assertRaisesRegex(ValueError, 'unsupported schema:'):
89+
with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'):
8490
JsonStrictValidator(schema_version)
8591

8692
@idata(_dp('valid'))

tests/test_validation_xml.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ddt import data, ddt, idata, unpack
2626

2727
from cyclonedx.exception import MissingOptionalDependencyException
28-
from cyclonedx.schema import SchemaVersion
28+
from cyclonedx.schema import OutputFormat, SchemaVersion
2929
from cyclonedx.validation.xml import XmlValidator
3030
from tests import TESTDATA_DIRECTORY
3131

@@ -44,9 +44,15 @@ def _dp(prefix: str) -> Generator:
4444
@ddt
4545
class TestXmlValidator(TestCase):
4646

47-
@data(*UNSUPPORTED_SCHEMA_VERSIONS)
47+
@idata(sv for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS)
48+
def test_validator_as_expected(self, schema_version: SchemaVersion) -> None:
49+
validator = XmlValidator(schema_version)
50+
self.assertIs(validator.schema_version, schema_version)
51+
self.assertIs(validator.output_format, OutputFormat.XML)
52+
53+
@idata(UNSUPPORTED_SCHEMA_VERSIONS)
4854
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
49-
with self.assertRaisesRegex(ValueError, 'unsupported schema'):
55+
with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'):
5056
XmlValidator(schema_version)
5157

5258
@idata(_dp('valid'))

0 commit comments

Comments
 (0)