Skip to content

Commit 6003feb

Browse files
authored
feat: couple classes and their serializes (#757)
Deprecates `.serialization.BomRefHelper` and `.serialization.LicenseRepositoryHelper` fixes #756 --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent fb9a42e commit 6003feb

File tree

11 files changed

+161
-124
lines changed

11 files changed

+161
-124
lines changed

cyclonedx/model/bom.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
SchemaVersion1Dot5,
3737
SchemaVersion1Dot6,
3838
)
39-
from ..serialization import LicenseRepositoryHelper, UrnUuidHelper
39+
from ..serialization import UrnUuidHelper
4040
from . import _BOM_LINK_PREFIX, ExternalReference, Property
4141
from .bom_ref import BomRef
4242
from .component import Component
4343
from .contact import OrganizationalContact, OrganizationalEntity
4444
from .definition import Definitions
4545
from .dependency import Dependable, Dependency
46-
from .license import License, LicenseExpression, LicenseRepository
46+
from .license import License, LicenseExpression, LicenseRepository, _LicenseRepositorySerializationHelper
4747
from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper
4848
from .service import Service
4949
from .tool import Tool, ToolRepository, _ToolRepositoryHelper
@@ -254,7 +254,7 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
254254
@serializable.view(SchemaVersion1Dot4)
255255
@serializable.view(SchemaVersion1Dot5)
256256
@serializable.view(SchemaVersion1Dot6)
257-
@serializable.type_mapping(LicenseRepositoryHelper)
257+
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
258258
@serializable.xml_sequence(9)
259259
def licenses(self) -> LicenseRepository:
260260
"""

cyclonedx/model/bom_ref.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,20 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818

19-
from typing import Any, Optional
19+
from typing import TYPE_CHECKING, Any, Optional
2020

21+
import serializable
2122

22-
class BomRef:
23+
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException
24+
25+
if TYPE_CHECKING: # pragma: no cover
26+
from typing import Type, TypeVar
27+
28+
_T_BR = TypeVar('_T_BR', bound='BomRef')
29+
30+
31+
@serializable.serializable_class
32+
class BomRef(serializable.helpers.BaseHelper):
2333
"""
2434
An identifier that can be used to reference objects elsewhere in the BOM.
2535
@@ -33,6 +43,8 @@ def __init__(self, value: Optional[str] = None) -> None:
3343
self.value = value
3444

3545
@property
46+
@serializable.json_name('.')
47+
@serializable.xml_name('.')
3648
def value(self) -> Optional[str]:
3749
return self._value
3850

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

6880
def __bool__(self) -> bool:
6981
return self._value is not None
82+
83+
# region impl BaseHelper
84+
85+
@classmethod
86+
def serialize(cls, o: Any) -> Optional[str]:
87+
if isinstance(o, cls):
88+
return o.value
89+
raise SerializationOfUnexpectedValueException(
90+
f'Attempt to serialize a non-BomRef: {o!r}')
91+
92+
@classmethod
93+
def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR':
94+
try:
95+
return cls(value=str(o))
96+
except ValueError as err:
97+
raise CycloneDxDeserializationException(
98+
f'BomRef string supplied does not parse: {o!r}'
99+
) from err
100+
101+
# endregion impl BaseHelper

cyclonedx/model/component.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
SchemaVersion1Dot5,
4545
SchemaVersion1Dot6,
4646
)
47-
from ..serialization import BomRefHelper, LicenseRepositoryHelper, PackageUrl as PackageUrlSH
47+
from ..serialization import PackageUrl as PackageUrlSH
4848
from . import (
4949
AttachedText,
5050
Copyright,
@@ -61,7 +61,7 @@
6161
from .crypto import CryptoProperties
6262
from .dependency import Dependable
6363
from .issue import IssueType
64-
from .license import License, LicenseRepository
64+
from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper
6565
from .release_note import ReleaseNotes
6666

6767

@@ -250,7 +250,7 @@ def __init__(
250250
# ... # TODO since CDX1.5
251251

252252
@property
253-
@serializable.type_mapping(LicenseRepositoryHelper)
253+
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
254254
@serializable.xml_sequence(4)
255255
def licenses(self) -> LicenseRepository:
256256
"""
@@ -1171,7 +1171,7 @@ def mime_type(self, mime_type: Optional[str]) -> None:
11711171

11721172
@property
11731173
@serializable.json_name('bom-ref')
1174-
@serializable.type_mapping(BomRefHelper)
1174+
@serializable.type_mapping(BomRef)
11751175
@serializable.view(SchemaVersion1Dot1)
11761176
@serializable.view(SchemaVersion1Dot2)
11771177
@serializable.view(SchemaVersion1Dot3)
@@ -1407,7 +1407,7 @@ def hashes(self, hashes: Iterable[HashType]) -> None:
14071407
@serializable.view(SchemaVersion1Dot4)
14081408
@serializable.view(SchemaVersion1Dot5)
14091409
@serializable.view(SchemaVersion1Dot6)
1410-
@serializable.type_mapping(LicenseRepositoryHelper)
1410+
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
14111411
@serializable.xml_sequence(12)
14121412
def licenses(self) -> LicenseRepository:
14131413
"""
@@ -1789,4 +1789,4 @@ def __hash__(self) -> int:
17891789

17901790
def __repr__(self) -> str:
17911791
return f'<Component bom-ref={self.bom_ref!r}, group={self.group}, name={self.name}, ' \
1792-
f'version={self.version}, type={self.type}>'
1792+
f'version={self.version}, type={self.type}>'

cyclonedx/model/contact.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from .._internal.compare import ComparableTuple as _ComparableTuple
2626
from ..exception.model import NoPropertiesProvidedException
2727
from ..schema.schema import SchemaVersion1Dot6
28-
from ..serialization import BomRefHelper
2928
from . import XsUri
3029
from .bom_ref import BomRef
3130

@@ -60,7 +59,7 @@ def __init__(
6059

6160
@property
6261
@serializable.json_name('bom-ref')
63-
@serializable.type_mapping(BomRefHelper)
62+
@serializable.type_mapping(BomRef)
6463
@serializable.xml_attribute()
6564
@serializable.xml_name('bom-ref')
6665
def bom_ref(self) -> Optional[BomRef]:

cyclonedx/model/crypto.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from .._internal.compare import ComparableTuple as _ComparableTuple
3636
from ..exception.model import InvalidNistQuantumSecurityLevelException, InvalidRelatedCryptoMaterialSizeException
3737
from ..schema.schema import SchemaVersion1Dot6
38-
from ..serialization import BomRefHelper
3938
from .bom_ref import BomRef
4039

4140

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

608607
@property
609-
@serializable.type_mapping(BomRefHelper)
608+
@serializable.type_mapping(BomRef)
610609
@serializable.xml_sequence(50)
611610
def signature_algorithm_ref(self) -> Optional[BomRef]:
612611
"""
@@ -622,7 +621,7 @@ def signature_algorithm_ref(self, signature_algorithm_ref: Optional[BomRef]) ->
622621
self._signature_algorithm_ref = signature_algorithm_ref
623622

624623
@property
625-
@serializable.type_mapping(BomRefHelper)
624+
@serializable.type_mapping(BomRef)
626625
@serializable.xml_sequence(60)
627626
def subject_public_key_ref(self) -> Optional[BomRef]:
628627
"""
@@ -775,7 +774,7 @@ def mechanism(self, mechanism: Optional[str]) -> None:
775774
self._mechanism = mechanism
776775

777776
@property
778-
@serializable.type_mapping(BomRefHelper)
777+
@serializable.type_mapping(BomRef)
779778
@serializable.xml_sequence(20)
780779
def algorithm_ref(self) -> Optional[BomRef]:
781780
"""
@@ -888,7 +887,7 @@ def state(self, state: Optional[RelatedCryptoMaterialState]) -> None:
888887
self._state = state
889888

890889
@property
891-
@serializable.type_mapping(BomRefHelper)
890+
@serializable.type_mapping(BomRef)
892891
@serializable.xml_sequence(40)
893892
def algorithm_ref(self) -> Optional[BomRef]:
894893
"""

cyclonedx/model/definition.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
from .._internal.bom_ref import bom_ref_from_str
2424
from .._internal.compare import ComparableTuple as _ComparableTuple
25-
from ..serialization import BomRefHelper
2625
from . import ExternalReference
2726
from .bom_ref import BomRef
2827

@@ -71,11 +70,11 @@ def __hash__(self) -> int:
7170

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

7675
@property
7776
@serializable.json_name('bom-ref')
78-
@serializable.type_mapping(BomRefHelper)
77+
@serializable.type_mapping(BomRef)
7978
@serializable.xml_attribute()
8079
@serializable.xml_name('bom-ref')
8180
def bom_ref(self) -> BomRef:

cyclonedx/model/dependency.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
from .._internal.compare import ComparableTuple as _ComparableTuple
2626
from ..exception.serialization import SerializationOfUnexpectedValueException
27-
from ..serialization import BomRefHelper
2827
from .bom_ref import BomRef
2928

3029

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

6362
@property
64-
@serializable.type_mapping(BomRefHelper)
63+
@serializable.type_mapping(BomRef)
6564
@serializable.xml_attribute()
6665
def ref(self) -> BomRef:
6766
return self._ref

cyclonedx/model/license.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121
"""
2222

2323
from enum import Enum
24-
from typing import TYPE_CHECKING, Any, Optional, Union
24+
from json import loads as json_loads
25+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
2526
from warnings import warn
27+
from xml.etree.ElementTree import Element # nosec B405
2628

2729
import serializable
2830
from sortedcontainers import SortedSet
2931

3032
from .._internal.compare import ComparableTuple as _ComparableTuple
3133
from ..exception.model import MutuallyExclusivePropertiesException
34+
from ..exception.serialization import CycloneDxDeserializationException
3235
from ..schema.schema import SchemaVersion1Dot6
3336
from . import AttachedText, XsUri
3437

@@ -350,6 +353,7 @@ class LicenseRepository(SortedSet[License]):
350353
Denormalizers/deserializers will be thankful.
351354
The normalization/serialization process SHOULD take care of these facts and do what is needed.
352355
"""
356+
353357
else:
354358
class LicenseRepository(SortedSet):
355359
"""Collection of :class:`License`.
@@ -364,3 +368,86 @@ class LicenseRepository(SortedSet):
364368
Denormalizers/deserializers will be thankful.
365369
The normalization/serialization process SHOULD take care of these facts and do what is needed.
366370
"""
371+
372+
373+
class _LicenseRepositorySerializationHelper(serializable.helpers.BaseHelper):
374+
""" THIS CLASS IS NON-PUBLIC API """
375+
376+
@classmethod
377+
def json_normalize(cls, o: LicenseRepository, *,
378+
view: Optional[Type[serializable.ViewType]],
379+
**__: Any) -> Any:
380+
if len(o) == 0:
381+
return None
382+
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
383+
if expression:
384+
# mixed license expression and license? this is an invalid constellation according to schema!
385+
# see https://github.com/CycloneDX/specification/pull/205
386+
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
387+
return [json_loads(expression.as_json(view_=view))] # type:ignore[attr-defined]
388+
return [
389+
{'license': json_loads(
390+
li.as_json( # type:ignore[attr-defined]
391+
view_=view)
392+
)}
393+
for li in o
394+
if isinstance(li, DisjunctiveLicense)
395+
]
396+
397+
@classmethod
398+
def json_denormalize(cls, o: List[Dict[str, Any]],
399+
**__: Any) -> LicenseRepository:
400+
repo = LicenseRepository()
401+
for li in o:
402+
if 'license' in li:
403+
repo.add(DisjunctiveLicense.from_json( # type:ignore[attr-defined]
404+
li['license']))
405+
elif 'expression' in li:
406+
repo.add(LicenseExpression.from_json( # type:ignore[attr-defined]
407+
li
408+
))
409+
else:
410+
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
411+
return repo
412+
413+
@classmethod
414+
def xml_normalize(cls, o: LicenseRepository, *,
415+
element_name: str,
416+
view: Optional[Type[serializable.ViewType]],
417+
xmlns: Optional[str],
418+
**__: Any) -> Optional[Element]:
419+
if len(o) == 0:
420+
return None
421+
elem = Element(element_name)
422+
expression = next((li for li in o if isinstance(li, LicenseExpression)), None)
423+
if expression:
424+
# mixed license expression and license? this is an invalid constellation according to schema!
425+
# see https://github.com/CycloneDX/specification/pull/205
426+
# but models need to allow it for backwards compatibility with JSON CDX < 1.5
427+
elem.append(expression.as_xml( # type:ignore[attr-defined]
428+
view_=view, as_string=False, element_name='expression', xmlns=xmlns))
429+
else:
430+
elem.extend(
431+
li.as_xml( # type:ignore[attr-defined]
432+
view_=view, as_string=False, element_name='license', xmlns=xmlns)
433+
for li in o
434+
if isinstance(li, DisjunctiveLicense)
435+
)
436+
return elem
437+
438+
@classmethod
439+
def xml_denormalize(cls, o: Element,
440+
default_ns: Optional[str],
441+
**__: Any) -> LicenseRepository:
442+
repo = LicenseRepository()
443+
for li in o:
444+
tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '')
445+
if tag == 'license':
446+
repo.add(DisjunctiveLicense.from_xml( # type:ignore[attr-defined]
447+
li, default_ns))
448+
elif tag == 'expression':
449+
repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined]
450+
li, default_ns))
451+
else:
452+
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
453+
return repo

cyclonedx/model/service.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,14 @@
2929
import serializable
3030
from sortedcontainers import SortedSet
3131

32-
from cyclonedx.serialization import BomRefHelper, LicenseRepositoryHelper
33-
3432
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
3533
from .._internal.compare import ComparableTuple as _ComparableTuple
3634
from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
3735
from . import DataClassification, ExternalReference, Property, XsUri
3836
from .bom_ref import BomRef
3937
from .contact import OrganizationalEntity
4038
from .dependency import Dependable
41-
from .license import License, LicenseRepository
39+
from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper
4240
from .release_note import ReleaseNotes
4341

4442

@@ -87,7 +85,7 @@ def __init__(
8785

8886
@property
8987
@serializable.json_name('bom-ref')
90-
@serializable.type_mapping(BomRefHelper)
88+
@serializable.type_mapping(BomRef)
9189
@serializable.xml_attribute()
9290
@serializable.xml_name('bom-ref')
9391
def bom_ref(self) -> BomRef:
@@ -263,7 +261,7 @@ def data(self, data: Iterable[DataClassification]) -> None:
263261
self._data = SortedSet(data)
264262

265263
@property
266-
@serializable.type_mapping(LicenseRepositoryHelper)
264+
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
267265
@serializable.xml_sequence(11)
268266
def licenses(self) -> LicenseRepository:
269267
"""

cyclonedx/model/vulnerability.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
from .._internal.compare import ComparableTuple as _ComparableTuple
4343
from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException
4444
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
45-
from ..serialization import BomRefHelper
4645
from . import Property, XsUri
4746
from .bom_ref import BomRef
4847
from .contact import OrganizationalContact, OrganizationalEntity
@@ -982,7 +981,7 @@ def __init__(
982981

983982
@property
984983
@serializable.json_name('bom-ref')
985-
@serializable.type_mapping(BomRefHelper)
984+
@serializable.type_mapping(BomRef)
986985
@serializable.xml_attribute()
987986
@serializable.xml_name('bom-ref')
988987
def bom_ref(self) -> BomRef:

0 commit comments

Comments
 (0)