Skip to content

Commit

Permalink
feat: couple classes and their serializes (#757)
Browse files Browse the repository at this point in the history
Deprecates `.serialization.BomRefHelper` and
`.serialization.LicenseRepositoryHelper`

fixes #756

---------

Signed-off-by: Jan Kowalleck <[email protected]>
  • Loading branch information
jkowalleck authored Jan 20, 2025
1 parent fb9a42e commit 6003feb
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 124 deletions.
6 changes: 3 additions & 3 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@
SchemaVersion1Dot5,
SchemaVersion1Dot6,
)
from ..serialization import LicenseRepositoryHelper, UrnUuidHelper
from ..serialization import UrnUuidHelper
from . import _BOM_LINK_PREFIX, ExternalReference, Property
from .bom_ref import BomRef
from .component import Component
from .contact import OrganizationalContact, OrganizationalEntity
from .definition import Definitions
from .dependency import Dependable, Dependency
from .license import License, LicenseExpression, LicenseRepository
from .license import License, LicenseExpression, LicenseRepository, _LicenseRepositorySerializationHelper
from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper
from .service import Service
from .tool import Tool, ToolRepository, _ToolRepositoryHelper
Expand Down Expand Up @@ -254,7 +254,7 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.type_mapping(LicenseRepositoryHelper)
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(9)
def licenses(self) -> LicenseRepository:
"""
Expand Down
36 changes: 34 additions & 2 deletions cyclonedx/model/bom_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.


from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional

import serializable

class BomRef:
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException

if TYPE_CHECKING: # pragma: no cover
from typing import Type, TypeVar

_T_BR = TypeVar('_T_BR', bound='BomRef')


@serializable.serializable_class
class BomRef(serializable.helpers.BaseHelper):
"""
An identifier that can be used to reference objects elsewhere in the BOM.
Expand All @@ -33,6 +43,8 @@ def __init__(self, value: Optional[str] = None) -> None:
self.value = value

@property
@serializable.json_name('.')
@serializable.xml_name('.')
def value(self) -> Optional[str]:
return self._value

Expand Down Expand Up @@ -67,3 +79,23 @@ def __str__(self) -> str:

def __bool__(self) -> bool:
return self._value is not None

# region impl BaseHelper

@classmethod
def serialize(cls, o: Any) -> Optional[str]:
if isinstance(o, cls):
return o.value
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-BomRef: {o!r}')

@classmethod
def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR':
try:
return cls(value=str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'BomRef string supplied does not parse: {o!r}'
) from err

# endregion impl BaseHelper
12 changes: 6 additions & 6 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
SchemaVersion1Dot5,
SchemaVersion1Dot6,
)
from ..serialization import BomRefHelper, LicenseRepositoryHelper, PackageUrl as PackageUrlSH
from ..serialization import PackageUrl as PackageUrlSH
from . import (
AttachedText,
Copyright,
Expand All @@ -61,7 +61,7 @@
from .crypto import CryptoProperties
from .dependency import Dependable
from .issue import IssueType
from .license import License, LicenseRepository
from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper
from .release_note import ReleaseNotes


Expand Down Expand Up @@ -250,7 +250,7 @@ def __init__(
# ... # TODO since CDX1.5

@property
@serializable.type_mapping(LicenseRepositoryHelper)
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(4)
def licenses(self) -> LicenseRepository:
"""
Expand Down Expand Up @@ -1171,7 +1171,7 @@ def mime_type(self, mime_type: Optional[str]) -> None:

@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
Expand Down Expand Up @@ -1407,7 +1407,7 @@ def hashes(self, hashes: Iterable[HashType]) -> None:
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.type_mapping(LicenseRepositoryHelper)
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(12)
def licenses(self) -> LicenseRepository:
"""
Expand Down Expand Up @@ -1789,4 +1789,4 @@ def __hash__(self) -> int:

def __repr__(self) -> str:
return f'<Component bom-ref={self.bom_ref!r}, group={self.group}, name={self.name}, ' \
f'version={self.version}, type={self.type}>'
f'version={self.version}, type={self.type}>'
3 changes: 1 addition & 2 deletions cyclonedx/model/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.model import NoPropertiesProvidedException
from ..schema.schema import SchemaVersion1Dot6
from ..serialization import BomRefHelper
from . import XsUri
from .bom_ref import BomRef

Expand Down Expand Up @@ -60,7 +59,7 @@ def __init__(

@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> Optional[BomRef]:
Expand Down
9 changes: 4 additions & 5 deletions cyclonedx/model/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.model import InvalidNistQuantumSecurityLevelException, InvalidRelatedCryptoMaterialSizeException
from ..schema.schema import SchemaVersion1Dot6
from ..serialization import BomRefHelper
from .bom_ref import BomRef


Expand Down Expand Up @@ -606,7 +605,7 @@ def not_valid_after(self, not_valid_after: Optional[datetime]) -> None:
self._not_valid_after = not_valid_after

@property
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_sequence(50)
def signature_algorithm_ref(self) -> Optional[BomRef]:
"""
Expand All @@ -622,7 +621,7 @@ def signature_algorithm_ref(self, signature_algorithm_ref: Optional[BomRef]) ->
self._signature_algorithm_ref = signature_algorithm_ref

@property
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_sequence(60)
def subject_public_key_ref(self) -> Optional[BomRef]:
"""
Expand Down Expand Up @@ -775,7 +774,7 @@ def mechanism(self, mechanism: Optional[str]) -> None:
self._mechanism = mechanism

@property
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_sequence(20)
def algorithm_ref(self) -> Optional[BomRef]:
"""
Expand Down Expand Up @@ -888,7 +887,7 @@ def state(self, state: Optional[RelatedCryptoMaterialState]) -> None:
self._state = state

@property
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_sequence(40)
def algorithm_ref(self) -> Optional[BomRef]:
"""
Expand Down
5 changes: 2 additions & 3 deletions cyclonedx/model/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

from .._internal.bom_ref import bom_ref_from_str
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..serialization import BomRefHelper
from . import ExternalReference
from .bom_ref import BomRef

Expand Down Expand Up @@ -71,11 +70,11 @@ def __hash__(self) -> int:

def __repr__(self) -> str:
return f'<Standard bom-ref={self.bom_ref}, name={self.name}, version={self.version}, ' \
f'description={self.description}, owner={self.owner}>'
f'description={self.description}, owner={self.owner}>'

@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> BomRef:
Expand Down
3 changes: 1 addition & 2 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.serialization import SerializationOfUnexpectedValueException
from ..serialization import BomRefHelper
from .bom_ref import BomRef


Expand Down Expand Up @@ -61,7 +60,7 @@ def __init__(self, ref: BomRef, dependencies: Optional[Iterable['Dependency']] =
self.dependencies = dependencies or [] # type:ignore[assignment]

@property
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
def ref(self) -> BomRef:
return self._ref
Expand Down
89 changes: 88 additions & 1 deletion cyclonedx/model/license.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
"""

from enum import Enum
from typing import TYPE_CHECKING, Any, Optional, Union
from json import loads as json_loads
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
from warnings import warn
from xml.etree.ElementTree import Element # nosec B405

import serializable
from sortedcontainers import SortedSet

from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.model import MutuallyExclusivePropertiesException
from ..exception.serialization import CycloneDxDeserializationException
from ..schema.schema import SchemaVersion1Dot6
from . import AttachedText, XsUri

Expand Down Expand Up @@ -350,6 +353,7 @@ class LicenseRepository(SortedSet[License]):
Denormalizers/deserializers will be thankful.
The normalization/serialization process SHOULD take care of these facts and do what is needed.
"""

else:
class LicenseRepository(SortedSet):
"""Collection of :class:`License`.
Expand All @@ -364,3 +368,86 @@ class LicenseRepository(SortedSet):
Denormalizers/deserializers will be thankful.
The normalization/serialization process SHOULD take care of these facts and do what is needed.
"""


class _LicenseRepositorySerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """

@classmethod
def json_normalize(cls, o: LicenseRepository, *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> Any:
if len(o) == 0:
return None
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
if expression:
# mixed license expression and license? this is an invalid constellation according to schema!
# see https://github.com/CycloneDX/specification/pull/205
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
return [json_loads(expression.as_json(view_=view))] # type:ignore[attr-defined]
return [
{'license': json_loads(
li.as_json( # type:ignore[attr-defined]
view_=view)
)}
for li in o
if isinstance(li, DisjunctiveLicense)
]

@classmethod
def json_denormalize(cls, o: List[Dict[str, Any]],
**__: Any) -> LicenseRepository:
repo = LicenseRepository()
for li in o:
if 'license' in li:
repo.add(DisjunctiveLicense.from_json( # type:ignore[attr-defined]
li['license']))
elif 'expression' in li:
repo.add(LicenseExpression.from_json( # type:ignore[attr-defined]
li
))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
return repo

@classmethod
def xml_normalize(cls, o: LicenseRepository, *,
element_name: str,
view: Optional[Type[serializable.ViewType]],
xmlns: Optional[str],
**__: Any) -> Optional[Element]:
if len(o) == 0:
return None
elem = Element(element_name)
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
if expression:
# mixed license expression and license? this is an invalid constellation according to schema!
# see https://github.com/CycloneDX/specification/pull/205
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
elem.append(expression.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='expression', xmlns=xmlns))
else:
elem.extend(
li.as_xml( # type:ignore[attr-defined]
view_=view, as_string=False, element_name='license', xmlns=xmlns)
for li in o
if isinstance(li, DisjunctiveLicense)
)
return elem

@classmethod
def xml_denormalize(cls, o: Element,
default_ns: Optional[str],
**__: Any) -> LicenseRepository:
repo = LicenseRepository()
for li in o:
tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '')
if tag == 'license':
repo.add(DisjunctiveLicense.from_xml( # type:ignore[attr-defined]
li, default_ns))
elif tag == 'expression':
repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined]
li, default_ns))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
return repo
8 changes: 3 additions & 5 deletions cyclonedx/model/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,14 @@
import serializable
from sortedcontainers import SortedSet

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
from .bom_ref import BomRef
from .contact import OrganizationalEntity
from .dependency import Dependable
from .license import License, LicenseRepository
from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper
from .release_note import ReleaseNotes


Expand Down Expand Up @@ -87,7 +85,7 @@ def __init__(

@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> BomRef:
Expand Down Expand Up @@ -263,7 +261,7 @@ def data(self, data: Iterable[DataClassification]) -> None:
self._data = SortedSet(data)

@property
@serializable.type_mapping(LicenseRepositoryHelper)
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(11)
def licenses(self) -> LicenseRepository:
"""
Expand Down
3 changes: 1 addition & 2 deletions cyclonedx/model/vulnerability.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
from .._internal.compare import ComparableTuple as _ComparableTuple
from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
from ..serialization import BomRefHelper
from . import Property, XsUri
from .bom_ref import BomRef
from .contact import OrganizationalContact, OrganizationalEntity
Expand Down Expand Up @@ -982,7 +981,7 @@ def __init__(

@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRefHelper)
@serializable.type_mapping(BomRef)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
def bom_ref(self) -> BomRef:
Expand Down
Loading

0 comments on commit 6003feb

Please sign in to comment.