Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: schema based validator #468

Merged
merged 4 commits into from
Oct 13, 2023
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
26 changes: 13 additions & 13 deletions cyclonedx/output/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@

import os
from abc import ABC, abstractmethod
from importlib import import_module
from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional, Type, Union, overload
from typing import TYPE_CHECKING, Any, Iterable, Literal, Mapping, Optional, Type, Union, overload

from ..schema import OutputFormat, SchemaVersion

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

klass = BY_SCHEMA_VERSION.get(schema_version, None)
if klass is None:
raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version.name}')
return klass(bom=bom)
raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version!r}')
return klass(bom)
51 changes: 6 additions & 45 deletions cyclonedx/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@


from abc import ABC, abstractmethod
from importlib import import_module
from typing import TYPE_CHECKING, Any, Literal, Optional, Protocol, Type, Union, overload
from typing import TYPE_CHECKING, Any, Optional, Protocol

from ..schema import OutputFormat

if TYPE_CHECKING: # pragma: no cover
from ..schema import SchemaVersion
from .json import JsonValidator
from .xml import XmlValidator


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


class Validator(Protocol):
"""Validator protocol"""
class SchemaBasedValidator(Protocol):
"""Schema based Validator protocol"""

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


class BaseValidator(ABC, Validator):
"""BaseValidator"""
class BaseSchemaBasedValidator(ABC, SchemaBasedValidator):
"""Base Schema based Validator"""

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

@property
def schema_version(self) -> 'SchemaVersion':
Expand All @@ -82,39 +79,3 @@ def output_format(self) -> OutputFormat:
def _schema_file(self) -> Optional[str]:
"""get the schema file according to schema version."""
...


@overload
def get_instance(output_format: Literal[OutputFormat.JSON], schema_version: 'SchemaVersion'
) -> 'JsonValidator':
...


@overload
def get_instance(output_format: Literal[OutputFormat.XML], schema_version: 'SchemaVersion'
) -> 'XmlValidator':
...


@overload
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion'
) -> Union['JsonValidator', 'XmlValidator']:
...


def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion') -> BaseValidator:
"""get the default validator for a certain `OutputFormat`

Raises error when no instance could be built.
"""
# all exceptions are undocumented, as they are pure functional, and should be prevented by correct typing...
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:
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:
raise ValueError(f'Missing Validator for {output_format.name}')
return klass(schema_version)
8 changes: 4 additions & 4 deletions cyclonedx/validation/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

from ..exception import MissingOptionalDependencyException
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
from . import BaseValidator, ValidationError, Validator
from . import BaseSchemaBasedValidator, SchemaBasedValidator, ValidationError

_missing_deps_error: Optional[Tuple[MissingOptionalDependencyException, ImportError]] = None
try:
Expand All @@ -45,7 +45,7 @@
), err


class _BaseJsonValidator(BaseValidator, ABC):
class _BaseJsonValidator(BaseSchemaBasedValidator, ABC):
@property
def output_format(self) -> Literal[OutputFormat.JSON]:
return OutputFormat.JSON
Expand Down Expand Up @@ -98,15 +98,15 @@ def __make_validator_registry() -> Registry[Any]:
])


class JsonValidator(_BaseJsonValidator, Validator):
class JsonValidator(_BaseJsonValidator, BaseSchemaBasedValidator, SchemaBasedValidator):
"""Validator for CycloneDX documents in JSON format."""

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


class JsonStrictValidator(_BaseJsonValidator, Validator):
class JsonStrictValidator(_BaseJsonValidator, BaseSchemaBasedValidator, SchemaBasedValidator):
"""Strict validator for CycloneDX documents in JSON format.

In contrast to :class:`~JsonValidator`,
Expand Down
20 changes: 20 additions & 0 deletions cyclonedx/validation/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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.


# nothing here, yet.
# in the future this could be the place where model validation is done.
# like the current `model.bom.Bom.validate()`
# see also: https://github.com/CycloneDX/cyclonedx-python-lib/issues/455
61 changes: 61 additions & 0 deletions cyclonedx/validation/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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 typing import TYPE_CHECKING, Literal, Union, overload

from ..schema import OutputFormat

if TYPE_CHECKING: # pragma: no cover
from ..schema import SchemaVersion
from . import BaseSchemaBasedValidator
from .json import JsonValidator
from .xml import XmlValidator


@overload
def get_instance(output_format: Literal[OutputFormat.JSON], schema_version: 'SchemaVersion'
) -> 'JsonValidator':
...


@overload
def get_instance(output_format: Literal[OutputFormat.XML], schema_version: 'SchemaVersion'
) -> 'XmlValidator':
...


@overload
def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion'
) -> Union['JsonValidator', 'XmlValidator']:
...


def get_instance(output_format: OutputFormat, schema_version: 'SchemaVersion') -> 'BaseSchemaBasedValidator':
"""get the default schema-based validator for a certain `OutputFormat`

Raises error when no instance could be built.
"""
# all exceptions are undocumented, as they are pure functional, and should be prevented by correct typing...
if TYPE_CHECKING: # pragma: no cover
from typing import Type
Validator: Type[BaseSchemaBasedValidator]
if OutputFormat.JSON is output_format:
from .json import JsonValidator as Validator
elif OutputFormat.XML is output_format:
from .xml import XmlValidator as Validator
else:
raise ValueError(f'Unexpected output_format: {output_format!r}')
return Validator(schema_version)
6 changes: 3 additions & 3 deletions cyclonedx/validation/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ..exception import MissingOptionalDependencyException
from ..schema import OutputFormat
from ..schema._res import BOM_XML as _S_BOM
from . import BaseValidator, ValidationError, Validator
from . import BaseSchemaBasedValidator, SchemaBasedValidator, ValidationError

if TYPE_CHECKING: # pragma: no cover
from ..schema import SchemaVersion
Expand All @@ -37,7 +37,7 @@
), err


class _BaseXmlValidator(BaseValidator, ABC):
class _BaseXmlValidator(BaseSchemaBasedValidator, ABC):

@property
def output_format(self) -> Literal[OutputFormat.XML]:
Expand Down Expand Up @@ -86,7 +86,7 @@ def _validator(self) -> 'XMLSchema':
return self.__validator


class XmlValidator(_BaseXmlValidator, Validator):
class XmlValidator(_BaseXmlValidator, BaseSchemaBasedValidator, SchemaBasedValidator):
"""Validator for CycloneDX documents in XML format."""

@property
Expand Down
2 changes: 1 addition & 1 deletion examples/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from cyclonedx.output.json import JsonV1Dot4
from cyclonedx.schema import SchemaVersion, OutputFormat
from cyclonedx.validation.json import JsonStrictValidator
from cyclonedx.validation import get_instance as get_validator
from cyclonedx.validation.schema import get_instance as get_validator

from typing import TYPE_CHECKING

Expand Down
4 changes: 2 additions & 2 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
self.assertIs(outputter.schema_version, sv)

@data(
*((of, 'foo', (TypeError, "unexpected schema_version: 'foo'")) for of in OutputFormat),
*(('foo', sv, (TypeError, "unexpected output_format: 'foo'")) for sv in SchemaVersion),
*((of, 'foo', (ValueError, f"Unknown {of.name}/schema_version: 'foo'")) for of in OutputFormat),
*(('foo', sv, (ValueError, "Unexpected output_format: 'foo'")) for sv in SchemaVersion),
)
@unpack
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_validation_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_validator_as_expected(self, schema_version: SchemaVersion) -> None:

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

@idata(_dp('valid'))
Expand Down Expand Up @@ -82,7 +82,7 @@ class TestJsonStrictValidator(TestCase):

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

@idata(_dp('valid'))
Expand Down
6 changes: 3 additions & 3 deletions tests/test_validation.py → tests/test_validation_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from ddt import data, ddt, named_data, unpack

from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation import get_instance as get_validator
from cyclonedx.validation.schema import get_instance as get_validator

UNDEFINED_FORMAT_VERSION = {
(OutputFormat.JSON, SchemaVersion.V1_1),
Expand All @@ -45,8 +45,8 @@ def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
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 UNDEFINED_FORMAT_VERSION)
*(('foo', sv, (ValueError, 'Unexpected output_format')) for sv in SchemaVersion),
*((f, v, (ValueError, 'Unsupported schema_version')) for f, v in UNDEFINED_FORMAT_VERSION)
)
@unpack
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raisesRegex: Tuple) -> None:
Expand Down