Skip to content

Commit

Permalink
Merge pull request #1 from CycloneDX/Churro/feat/lifecycles
Browse files Browse the repository at this point in the history
changes on top of PR#698
  • Loading branch information
Churro authored Oct 11, 2024
2 parents a4deeaf + 0e842ad commit df0fdf0
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 29 deletions.
2 changes: 1 addition & 1 deletion cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def lifecycles(self) -> LifecycleRepository:
return self._lifecycles

@lifecycles.setter
def lifecycles(self, lifecycles: Optional[Iterable[Lifecycle]]) -> None:
def lifecycles(self, lifecycles: Iterable[Lifecycle]) -> None:
self._lifecycles = LifecycleRepository(lifecycles)

@property
Expand Down
59 changes: 32 additions & 27 deletions cyclonedx/model/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@

@serializable.serializable_enum
class LifecyclePhase(str, Enum):
"""
Enum object that defines the permissible 'phase' for a Lifecycle according to the CycloneDX schema.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.3/#type_classification
"""
DESIGN = 'design'
PREBUILD = 'pre-build'
PRE_BUILD = 'pre-build'
BUILD = 'build'
POSTBUILD = 'post-build'
POST_BUILD = 'post-build'
OPERATIONS = 'operations'
DISCOVERY = 'discovery'
DECOMISSION = 'decommission'
DECOMMISSION = 'decommission'


@serializable.serializable_class
Expand Down Expand Up @@ -88,11 +94,18 @@ def __lt__(self, other: Any) -> bool:
return NotImplemented

def __repr__(self) -> str:
return f'<PredefinedLifecycle name={self._phase}>'
return f'<PredefinedLifecycle phase={self._phase}>'


@serializable.serializable_class
class NamedLifecycle:
"""
Object that defines custom state in the product lifecycle.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.5/#metadata_lifecycles
"""

def __init__(self, name: str, *, description: Optional[str] = None) -> None:
self._name = name
self._description = description
Expand Down Expand Up @@ -165,9 +178,7 @@ class LifecycleRepository(SortedSet[Lifecycle]):
This is a `set`, not a `list`. Order MUST NOT matter here.
"""

else:

class LifecycleRepository(SortedSet):
"""Collection of :class:`Lifecycle`.
Expand All @@ -182,7 +193,6 @@ def json_normalize(cls, o: LifecycleRepository, *,
**__: Any) -> Any:
if len(o) == 0:
return None

return [json_loads(li.as_json( # type:ignore[union-attr]
view_=view)) for li in o]

Expand All @@ -192,12 +202,13 @@ def json_denormalize(cls, o: List[Dict[str, Any]],
repo = LifecycleRepository()
for li in o:
if 'phase' in li:
repo.add(PredefinedLifecycle.from_json(li)) # type:ignore[attr-defined]
repo.add(PredefinedLifecycle.from_json( # type:ignore[attr-defined]
li))
elif 'name' in li:
repo.add(NamedLifecycle.from_json(li)) # type:ignore[attr-defined]
repo.add(NamedLifecycle.from_json( # type:ignore[attr-defined]
li))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')

return repo

@classmethod
Expand All @@ -208,33 +219,27 @@ def xml_normalize(cls, o: LifecycleRepository, *,
**__: Any) -> Optional[Element]:
if len(o) == 0:
return None

elem = Element(element_name)
for li in o:
elem.append(li.as_xml( # type:ignore[union-attr]
view_=view, as_string=False, element_name='lifecycle', xmlns=xmlns))

return elem

@classmethod
def xml_denormalize(cls, o: Element,
default_ns: Optional[str],
**__: Any) -> LifecycleRepository:
repo = LifecycleRepository()

for li in o:
tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '')

if tag == 'lifecycle':
stages = list(li)

predefined_lifecycle = next((el for el in stages if 'phase' in el.tag), None)
named_lifecycle = next((el for el in stages if 'name' in el.tag), None)
if predefined_lifecycle is not None:
repo.add(PredefinedLifecycle.from_xml(li, default_ns)) # type:ignore[attr-defined]
elif named_lifecycle is not None:
repo.add(NamedLifecycle.from_xml(li, default_ns)) # type:ignore[attr-defined]
ns_map = {'bom': default_ns or ''}
# Do not iterate over `o` and do not check for expected `.tag` of items.
# This check could have been done by schema validators before even deserializing.
for li in o.iterfind('bom:lifecycle', ns_map):
if li.find('bom:phase', ns_map) is not None:
repo.add(PredefinedLifecycle.from_xml( # type:ignore[attr-defined]
li, default_ns))
elif li.find('bom:name', ns_map) is not None:
repo.add(NamedLifecycle.from_xml( # type:ignore[attr-defined]
li, default_ns))
else:
raise CycloneDxDeserializationException(f'unexpected: {li!r}')

raise CycloneDxDeserializationException(f'unexpected content: {li!r}')
return repo
2 changes: 1 addition & 1 deletion tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ def get_bom_with_lifecycles() -> Bom:
metadata=BomMetaData(
lifecycles=[
PredefinedLifecycle(LifecyclePhase.BUILD),
PredefinedLifecycle(LifecyclePhase.POSTBUILD),
PredefinedLifecycle(LifecyclePhase.POST_BUILD),
NamedLifecycle(name='platform-integration-testing',
description='Integration testing specific to the runtime platform'),
],
Expand Down

0 comments on commit df0fdf0

Please sign in to comment.