diff --git a/cyclonedx/_internal/bom_ref.py b/cyclonedx/_internal/bom_ref.py index c0943da5..b6fefd22 100644 --- a/cyclonedx/_internal/bom_ref.py +++ b/cyclonedx/_internal/bom_ref.py @@ -21,13 +21,31 @@ Everything might change without any notice. """ -from typing import Optional, Union +from typing import Literal, Optional, Union, overload from ..model.bom_ref import BomRef -def bom_ref_from_str(bom_ref: Optional[Union[str, BomRef]]) -> BomRef: +@overload +def bom_ref_from_str(bom_ref: BomRef, optional: bool = ...) -> BomRef: + ... # pragma: no cover + + +@overload +def bom_ref_from_str(bom_ref: Optional[str], optional: Literal[False] = False) -> BomRef: + ... # pragma: no cover + + +@overload +def bom_ref_from_str(bom_ref: Optional[str], optional: Literal[True] = ...) -> Optional[BomRef]: + ... # pragma: no cover + + +def bom_ref_from_str(bom_ref: Optional[Union[str, BomRef]], optional: bool = False) -> Optional[BomRef]: if isinstance(bom_ref, BomRef): return bom_ref - else: - return BomRef(value=str(bom_ref) if bom_ref else None) + if bom_ref: + return BomRef(value=str(bom_ref)) + return None \ + if optional \ + else BomRef() diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 89e7020d..5b292d25 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -26,6 +26,7 @@ from packageurl import PackageURL from sortedcontainers import SortedSet +from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparablePackageURL as _ComparablePackageURL, ComparableTuple as _ComparableTuple from .._internal.hash import file_sha1sum as _file_sha1sum from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException, NoPropertiesProvidedException @@ -1097,10 +1098,7 @@ def __init__( ) -> None: self.type = type self.mime_type = mime_type - if isinstance(bom_ref, BomRef): - self._bom_ref = bom_ref - else: - self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None) + self._bom_ref = _bom_ref_from_str(bom_ref) self.supplier = supplier self.manufacturer = manufacturer self.authors = authors or [] # type:ignore[assignment] diff --git a/cyclonedx/model/contact.py b/cyclonedx/model/contact.py index a3cc2ed4..5a004f33 100644 --- a/cyclonedx/model/contact.py +++ b/cyclonedx/model/contact.py @@ -21,6 +21,7 @@ import serializable from sortedcontainers import SortedSet +from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import NoPropertiesProvidedException from ..schema.schema import SchemaVersion1Dot6 @@ -49,8 +50,7 @@ def __init__( postal_code: Optional[str] = None, street_address: Optional[str] = None, ) -> None: - self._bom_ref = bom_ref if isinstance(bom_ref, BomRef) else BomRef( - value=bom_ref) if bom_ref else None + self._bom_ref = _bom_ref_from_str(bom_ref, optional=True) self.country = country self.region = region self.locality = locality diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index 46ce6c29..d4a89fe4 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -31,6 +31,7 @@ from cyclonedx.serialization import BomRefHelper, LicenseRepositoryHelper +from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6 from . import DataClassification, ExternalReference, Property, XsUri @@ -68,10 +69,7 @@ def __init__( services: Optional[Iterable['Service']] = None, release_notes: Optional[ReleaseNotes] = None, ) -> None: - if isinstance(bom_ref, BomRef): - self._bom_ref = bom_ref - else: - self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None) + self._bom_ref = _bom_ref_from_str(bom_ref) self.provider = provider self.group = group self.name = name diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py index 1a64cdf6..ae859d95 100644 --- a/cyclonedx/model/vulnerability.py +++ b/cyclonedx/model/vulnerability.py @@ -38,6 +38,7 @@ import serializable from sortedcontainers import SortedSet +from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6 @@ -959,10 +960,7 @@ def __init__( affects: Optional[Iterable[BomTarget]] = None, properties: Optional[Iterable[Property]] = None, ) -> None: - if isinstance(bom_ref, BomRef): - self._bom_ref: BomRef = bom_ref - else: - self._bom_ref = BomRef(value=str(bom_ref) if bom_ref else None) + self._bom_ref = _bom_ref_from_str(bom_ref) self.id = id self.source = source self.references = references or [] # type:ignore[assignment] diff --git a/tests/test_internal/__init__.py b/tests/test_internal/__init__.py new file mode 100644 index 00000000..671a2188 --- /dev/null +++ b/tests/test_internal/__init__.py @@ -0,0 +1,16 @@ +# This file is part of CycloneDX Python Library +# +# 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. diff --git a/tests/test_internal/test_bom_ref.py b/tests/test_internal/test_bom_ref.py new file mode 100644 index 00000000..45bfdc67 --- /dev/null +++ b/tests/test_internal/test_bom_ref.py @@ -0,0 +1,57 @@ +# This file is part of CycloneDX Python Library +# +# 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 unittest import TestCase + +from cyclonedx._internal.bom_ref import bom_ref_from_str +from cyclonedx.model.bom_ref import BomRef + + +class TestInternalBomRefFromStr(TestCase): + + def test_bomref_io(self) -> None: + i = BomRef() + o = bom_ref_from_str(i) + self.assertIs(i, o) + + def test_none_optional_is_none(self) -> None: + o = bom_ref_from_str(None, optional=True) + self.assertIsNone(o) + + def test_none_mandatory_is_something(self) -> None: + o = bom_ref_from_str(None, optional=False) + self.assertIsInstance(o, BomRef) + self.assertIsNone(o.value) + + def test_nothing_optional_is_none(self) -> None: + o = bom_ref_from_str('', optional=True) + self.assertIsNone(o) + + def test_nothing_mandatory_is_something(self) -> None: + o = bom_ref_from_str('', optional=False) + self.assertIsInstance(o, BomRef) + self.assertIsNone(o.value) + + def test_something_optional(self) -> None: + o = bom_ref_from_str('foobar', optional=True) + self.assertIsInstance(o, BomRef) + self.assertEqual('foobar', o.value) + + def test_something_mandatory(self) -> None: + o = bom_ref_from_str('foobar', optional=False) + self.assertIsInstance(o, BomRef) + self.assertEqual('foobar', o.value)