Skip to content

Commit 65e79cf

Browse files
authored
refactor: schema based validator (#468)
- restructured validators, to enable possible non-schema-based validation. - optimized `validation.schema.get_instance()` - optimized `output.get_instance()` --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent a911106 commit 65e79cf

File tree

10 files changed

+115
-73
lines changed

10 files changed

+115
-73
lines changed

cyclonedx/output/__init__.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121

2222
import os
2323
from abc import ABC, abstractmethod
24-
from importlib import import_module
25-
from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional, Type, Union, overload
24+
from typing import TYPE_CHECKING, Any, Iterable, Literal, Mapping, Optional, Type, Union, overload
2625

2726
from ..schema import OutputFormat, SchemaVersion
2827

@@ -130,15 +129,16 @@ def get_instance(bom: 'Bom', output_format: OutputFormat = OutputFormat.XML,
130129
:return: BaseOutput
131130
"""
132131
# all exceptions are undocumented, as they are pure functional, and should be prevented by correct typing...
133-
if not isinstance(output_format, OutputFormat):
134-
raise TypeError(f"unexpected output_format: {output_format!r}")
135-
if not isinstance(schema_version, SchemaVersion):
136-
raise TypeError(f"unexpected schema_version: {schema_version!r}")
137-
try:
138-
module = import_module(f'.{output_format.name.lower()}', __package__)
139-
except ImportError as error:
140-
raise ValueError(f'Unknown output_format: {output_format.name}') from error
141-
klass: Optional[Type[BaseOutput]] = module.BY_SCHEMA_VERSION.get(schema_version, None)
132+
if TYPE_CHECKING: # pragma: no cover
133+
BY_SCHEMA_VERSION: Mapping[SchemaVersion, Type[BaseOutput]]
134+
if OutputFormat.JSON is output_format:
135+
from .json import BY_SCHEMA_VERSION
136+
elif OutputFormat.XML is output_format:
137+
from .xml import BY_SCHEMA_VERSION
138+
else:
139+
raise ValueError(f"Unexpected output_format: {output_format!r}")
140+
141+
klass = BY_SCHEMA_VERSION.get(schema_version, None)
142142
if klass is None:
143-
raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version.name}')
144-
return klass(bom=bom)
143+
raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version!r}')
144+
return klass(bom)

cyclonedx/validation/__init__.py

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@
1515

1616

1717
from abc import ABC, abstractmethod
18-
from importlib import import_module
19-
from typing import TYPE_CHECKING, Any, Literal, Optional, Protocol, Type, Union, overload
18+
from typing import TYPE_CHECKING, Any, Optional, Protocol
2019

2120
from ..schema import OutputFormat
2221

2322
if TYPE_CHECKING: # pragma: no cover
2423
from ..schema import SchemaVersion
25-
from .json import JsonValidator
26-
from .xml import XmlValidator
2724

2825

2926
class ValidationError:
@@ -44,8 +41,8 @@ def __str__(self) -> str:
4441
return str(self.data)
4542

4643

47-
class Validator(Protocol):
48-
"""Validator protocol"""
44+
class SchemaBasedValidator(Protocol):
45+
"""Schema based Validator protocol"""
4946

5047
def validate_str(self, data: str) -> Optional[ValidationError]:
5148
"""Validate a string
@@ -58,13 +55,13 @@ def validate_str(self, data: str) -> Optional[ValidationError]:
5855
...
5956

6057

61-
class BaseValidator(ABC, Validator):
62-
"""BaseValidator"""
58+
class BaseSchemaBasedValidator(ABC, SchemaBasedValidator):
59+
"""Base Schema based Validator"""
6360

6461
def __init__(self, schema_version: 'SchemaVersion') -> None:
6562
self.__schema_version = schema_version
6663
if not self._schema_file:
67-
raise ValueError(f'unsupported schema_version: {schema_version}')
64+
raise ValueError(f'Unsupported schema_version: {schema_version!r}')
6865

6966
@property
7067
def schema_version(self) -> 'SchemaVersion':
@@ -82,39 +79,3 @@ def output_format(self) -> OutputFormat:
8279
def _schema_file(self) -> Optional[str]:
8380
"""get the schema file according to schema version."""
8481
...
85-
86-
87-
@overload
88-
def get_instance(output_format: Literal[OutputFormat.JSON], schema_version: 'SchemaVersion'
89-
) -> 'JsonValidator':
90-
...
91-
92-
93-
@overload
94-
def get_instance(output_format: Literal[OutputFormat.XML], schema_version: 'SchemaVersion'
95-
) -> 'XmlValidator':
96-
...
97-
98-
99-
@overload
100-
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion'
101-
) -> Union['JsonValidator', 'XmlValidator']:
102-
...
103-
104-
105-
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion') -> BaseValidator:
106-
"""get the default validator for a certain `OutputFormat`
107-
108-
Raises error when no instance could be built.
109-
"""
110-
# all exceptions are undocumented, as they are pure functional, and should be prevented by correct typing...
111-
if not isinstance(output_format, OutputFormat):
112-
raise TypeError(f"unexpected output_format: {output_format!r}")
113-
try:
114-
module = import_module(f'.{output_format.name.lower()}', __package__)
115-
except ImportError as error:
116-
raise ValueError(f'Unknown output_format: {output_format.name}') from error
117-
klass: Optional[Type[BaseValidator]] = getattr(module, f'{output_format.name.capitalize()}Validator', None)
118-
if klass is None:
119-
raise ValueError(f'Missing Validator for {output_format.name}')
120-
return klass(schema_version)

cyclonedx/validation/json.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from ..exception import MissingOptionalDependencyException
2929
from ..schema._res import BOM_JSON as _S_BOM, BOM_JSON_STRICT as _S_BOM_STRICT, JSF as _S_JSF, SPDX_JSON as _S_SPDX
30-
from . import BaseValidator, ValidationError, Validator
30+
from . import BaseSchemaBasedValidator, SchemaBasedValidator, ValidationError
3131

3232
_missing_deps_error: Optional[Tuple[MissingOptionalDependencyException, ImportError]] = None
3333
try:
@@ -45,7 +45,7 @@
4545
), err
4646

4747

48-
class _BaseJsonValidator(BaseValidator, ABC):
48+
class _BaseJsonValidator(BaseSchemaBasedValidator, ABC):
4949
@property
5050
def output_format(self) -> Literal[OutputFormat.JSON]:
5151
return OutputFormat.JSON
@@ -98,15 +98,15 @@ def __make_validator_registry() -> Registry[Any]:
9898
])
9999

100100

101-
class JsonValidator(_BaseJsonValidator, Validator):
101+
class JsonValidator(_BaseJsonValidator, BaseSchemaBasedValidator, SchemaBasedValidator):
102102
"""Validator for CycloneDX documents in JSON format."""
103103

104104
@property
105105
def _schema_file(self) -> Optional[str]:
106106
return _S_BOM.get(self.schema_version)
107107

108108

109-
class JsonStrictValidator(_BaseJsonValidator, Validator):
109+
class JsonStrictValidator(_BaseJsonValidator, BaseSchemaBasedValidator, SchemaBasedValidator):
110110
"""Strict validator for CycloneDX documents in JSON format.
111111
112112
In contrast to :class:`~JsonValidator`,

cyclonedx/validation/model.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
#
13+
# SPDX-License-Identifier: Apache-2.0
14+
# Copyright (c) OWASP Foundation. All Rights Reserved.
15+
16+
17+
# nothing here, yet.
18+
# in the future this could be the place where model validation is done.
19+
# like the current `model.bom.Bom.validate()`
20+
# see also: https://github.com/CycloneDX/cyclonedx-python-lib/issues/455

cyclonedx/validation/schema.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
#
13+
# SPDX-License-Identifier: Apache-2.0
14+
# Copyright (c) OWASP Foundation. All Rights Reserved.
15+
16+
17+
from typing import TYPE_CHECKING, Literal, Union, overload
18+
19+
from ..schema import OutputFormat
20+
21+
if TYPE_CHECKING: # pragma: no cover
22+
from ..schema import SchemaVersion
23+
from . import BaseSchemaBasedValidator
24+
from .json import JsonValidator
25+
from .xml import XmlValidator
26+
27+
28+
@overload
29+
def get_instance(output_format: Literal[OutputFormat.JSON], schema_version: 'SchemaVersion'
30+
) -> 'JsonValidator':
31+
...
32+
33+
34+
@overload
35+
def get_instance(output_format: Literal[OutputFormat.XML], schema_version: 'SchemaVersion'
36+
) -> 'XmlValidator':
37+
...
38+
39+
40+
@overload
41+
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion'
42+
) -> Union['JsonValidator', 'XmlValidator']:
43+
...
44+
45+
46+
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion') -> 'BaseSchemaBasedValidator':
47+
"""get the default schema-based validator for a certain `OutputFormat`
48+
49+
Raises error when no instance could be built.
50+
"""
51+
# all exceptions are undocumented, as they are pure functional, and should be prevented by correct typing...
52+
if TYPE_CHECKING: # pragma: no cover
53+
from typing import Type
54+
Validator: Type[BaseSchemaBasedValidator]
55+
if OutputFormat.JSON is output_format:
56+
from .json import JsonValidator as Validator
57+
elif OutputFormat.XML is output_format:
58+
from .xml import XmlValidator as Validator
59+
else:
60+
raise ValueError(f'Unexpected output_format: {output_format!r}')
61+
return Validator(schema_version)

cyclonedx/validation/xml.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from ..exception import MissingOptionalDependencyException
2323
from ..schema import OutputFormat
2424
from ..schema._res import BOM_XML as _S_BOM
25-
from . import BaseValidator, ValidationError, Validator
25+
from . import BaseSchemaBasedValidator, SchemaBasedValidator, ValidationError
2626

2727
if TYPE_CHECKING: # pragma: no cover
2828
from ..schema import SchemaVersion
@@ -37,7 +37,7 @@
3737
), err
3838

3939

40-
class _BaseXmlValidator(BaseValidator, ABC):
40+
class _BaseXmlValidator(BaseSchemaBasedValidator, ABC):
4141

4242
@property
4343
def output_format(self) -> Literal[OutputFormat.XML]:
@@ -86,7 +86,7 @@ def _validator(self) -> 'XMLSchema':
8686
return self.__validator
8787

8888

89-
class XmlValidator(_BaseXmlValidator, Validator):
89+
class XmlValidator(_BaseXmlValidator, BaseSchemaBasedValidator, SchemaBasedValidator):
9090
"""Validator for CycloneDX documents in XML format."""
9191

9292
@property

examples/complex.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from cyclonedx.output.json import JsonV1Dot4
2929
from cyclonedx.schema import SchemaVersion, OutputFormat
3030
from cyclonedx.validation.json import JsonStrictValidator
31-
from cyclonedx.validation import get_instance as get_validator
31+
from cyclonedx.validation.schema import get_instance as get_validator
3232

3333
from typing import TYPE_CHECKING
3434

tests/test_output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
4141
self.assertIs(outputter.schema_version, sv)
4242

4343
@data(
44-
*((of, 'foo', (TypeError, "unexpected schema_version: 'foo'")) for of in OutputFormat),
45-
*(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion),
44+
*((of, 'foo', (ValueError, f"Unknown {of.name}/schema_version: 'foo'")) for of in OutputFormat),
45+
*(('foo', sv, (ValueError, "Unexpected output_format: 'foo'")) for sv in SchemaVersion),
4646
)
4747
@unpack
4848
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:

tests/test_validation_json.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_validator_as_expected(self, schema_version: SchemaVersion) -> None:
4848

4949
@idata(UNSUPPORTED_SCHEMA_VERSIONS)
5050
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
51-
with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'):
51+
with self.assertRaisesRegex(ValueError, 'Unsupported schema_version'):
5252
JsonValidator(schema_version)
5353

5454
@idata(_dp('valid'))
@@ -82,7 +82,7 @@ class TestJsonStrictValidator(TestCase):
8282

8383
@data(*UNSUPPORTED_SCHEMA_VERSIONS)
8484
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
85-
with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'):
85+
with self.assertRaisesRegex(ValueError, 'Unsupported schema_version'):
8686
JsonStrictValidator(schema_version)
8787

8888
@idata(_dp('valid'))

tests/test_validation.py renamed to tests/test_validation_schema.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from ddt import data, ddt, named_data, unpack
2424

2525
from cyclonedx.schema import OutputFormat, SchemaVersion
26-
from cyclonedx.validation import get_instance as get_validator
26+
from cyclonedx.validation.schema import get_instance as get_validator
2727

2828
UNDEFINED_FORMAT_VERSION = {
2929
(OutputFormat.JSON, SchemaVersion.V1_1),
@@ -45,8 +45,8 @@ def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
4545
self.assertIs(validator.schema_version, sv)
4646

4747
@data(
48-
*(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion),
49-
*((f, v, (ValueError, f'unsupported schema_version: {v}')) for f, v in UNDEFINED_FORMAT_VERSION)
48+
*(('foo', sv, (ValueError, 'Unexpected output_format')) for sv in SchemaVersion),
49+
*((f, v, (ValueError, 'Unsupported schema_version')) for f, v in UNDEFINED_FORMAT_VERSION)
5050
)
5151
@unpack
5252
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:

0 commit comments

Comments
 (0)