Skip to content

Commit ac4394c

Browse files
committed
JSONv unit tests passing except those blocked by CycloneDX/specification#146
Signed-off-by: Paul Horton <[email protected]>
1 parent 61fceb6 commit ac4394c

28 files changed

+219
-135
lines changed

cyclonedx/model/__init__.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
NoPropertiesProvidedException,
3434
UnknownHashTypeException,
3535
)
36+
from ..schema.schema import (
37+
SchemaVersion1Dot3,
38+
SchemaVersion1Dot4
39+
)
3640

3741
"""
3842
Uniform set of models to represent objects within a CycloneDX software bill-of-materials.
@@ -542,8 +546,10 @@ def type_(self, type_: ExternalReferenceType) -> None:
542546
self._type_ = type_
543547

544548
@property # type: ignore[misc]
549+
@serializable.view(SchemaVersion1Dot3)
550+
@serializable.view(SchemaVersion1Dot4)
545551
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'hash')
546-
def hashes(self) -> "Optional[SortedSet[HashType]]":
552+
def hashes(self) -> "SortedSet[HashType]":
547553
"""
548554
The hashes of the external reference (if applicable).
549555
@@ -1227,6 +1233,7 @@ def hashes(self, hashes: Iterable[HashType]) -> None:
12271233
self._hashes = SortedSet(hashes)
12281234

12291235
@property # type: ignore[misc]
1236+
@serializable.view(SchemaVersion1Dot4)
12301237
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
12311238
@serializable.xml_sequence(5)
12321239
def external_references(self) -> "SortedSet[ExternalReference]":

cyclonedx/model/bom.py

+12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727

2828
from ..exception.model import UnknownComponentDependencyException
2929
from ..parser import BaseParser
30+
from ..schema.schema import (
31+
SchemaVersion1Dot0,
32+
SchemaVersion1Dot1,
33+
SchemaVersion1Dot2,
34+
SchemaVersion1Dot3,
35+
SchemaVersion1Dot4
36+
)
3037
from . import ExternalReference, LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, ThisTool, Tool
3138
from .bom_ref import BomRef
3239
from .component import Component
@@ -173,6 +180,8 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
173180
self._supplier = supplier
174181

175182
@property # type: ignore[misc]
183+
@serializable.view(SchemaVersion1Dot3)
184+
@serializable.view(SchemaVersion1Dot4)
176185
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses')
177186
@serializable.xml_sequence(7)
178187
def licenses(self) -> "SortedSet[LicenseChoice]":
@@ -189,6 +198,8 @@ def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
189198
self._licenses = SortedSet(licenses)
190199

191200
@property # type: ignore[misc]
201+
@serializable.view(SchemaVersion1Dot3)
202+
@serializable.view(SchemaVersion1Dot4)
192203
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
193204
@serializable.xml_sequence(8)
194205
def properties(self) -> "SortedSet[Property]":
@@ -436,6 +447,7 @@ def has_vulnerabilities(self) -> bool:
436447
return bool(self.vulnerabilities)
437448

438449
@property # type: ignore[misc]
450+
@serializable.view(SchemaVersion1Dot4)
439451
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'vulnerability')
440452
@serializable.xml_sequence(8)
441453
def vulnerabilities(self) -> "SortedSet[Vulnerability]":

cyclonedx/model/component.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747
from .dependency import Dependable
4848
from .issue import IssueType
4949
from .release_note import ReleaseNotes
50+
from ..schema.schema import (
51+
SchemaVersion1Dot0,
52+
SchemaVersion1Dot1,
53+
SchemaVersion1Dot2,
54+
SchemaVersion1Dot3,
55+
SchemaVersion1Dot4
56+
)
5057

5158

5259
@serializable.serializable_class
@@ -156,7 +163,7 @@ def __eq__(self, other: object) -> bool:
156163
def __lt__(self, other: Any) -> bool:
157164
if isinstance(other, Commit):
158165
return ComparableTuple((self.uid, self.url, self.author, self.committer, self.message)) < \
159-
ComparableTuple((other.uid, other.url, other.author, other.committer, other.message))
166+
ComparableTuple((other.uid, other.url, other.author, other.committer, other.message))
160167
return NotImplemented
161168

162169
def __hash__(self) -> int:
@@ -404,7 +411,7 @@ def __eq__(self, other: object) -> bool:
404411
def __lt__(self, other: Any) -> bool:
405412
if isinstance(other, Patch):
406413
return ComparableTuple((self.type_, self.diff, ComparableTuple(self.resolves))) < \
407-
ComparableTuple((other.type_, other.diff, ComparableTuple(other.resolves)))
414+
ComparableTuple((other.type_, other.diff, ComparableTuple(other.resolves)))
408415
return NotImplemented
409416

410417
def __hash__(self) -> int:
@@ -932,6 +939,10 @@ def name(self, name: str) -> None:
932939
self._name = name
933940

934941
@property # type: ignore[misc]
942+
@serializable.include_none(SchemaVersion1Dot0)
943+
@serializable.include_none(SchemaVersion1Dot1)
944+
@serializable.include_none(SchemaVersion1Dot2)
945+
@serializable.include_none(SchemaVersion1Dot3)
935946
@serializable.xml_sequence(6)
936947
def version(self) -> Optional[str]:
937948
"""
@@ -1113,6 +1124,8 @@ def external_references(self, external_references: Iterable[ExternalReference])
11131124
self._external_references = SortedSet(external_references)
11141125

11151126
@property # type: ignore[misc]
1127+
@serializable.view(SchemaVersion1Dot3)
1128+
@serializable.view(SchemaVersion1Dot4)
11161129
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
11171130
@serializable.xml_sequence(18)
11181131
def properties(self) -> "SortedSet[Property]":
@@ -1148,6 +1161,8 @@ def components(self, components: Iterable['Component']) -> None:
11481161
self._components = SortedSet(components)
11491162

11501163
@property # type: ignore[misc]
1164+
@serializable.view(SchemaVersion1Dot3)
1165+
@serializable.view(SchemaVersion1Dot4)
11511166
@serializable.xml_sequence(20)
11521167
def evidence(self) -> Optional[ComponentEvidence]:
11531168
"""
@@ -1163,6 +1178,7 @@ def evidence(self, evidence: Optional[ComponentEvidence]) -> None:
11631178
self._evidence = evidence
11641179

11651180
@property # type: ignore[misc]
1181+
@serializable.view(SchemaVersion1Dot4)
11661182
@serializable.xml_sequence(21)
11671183
def release_notes(self) -> Optional[ReleaseNotes]:
11681184
"""
@@ -1201,7 +1217,7 @@ def __eq__(self, other: object) -> bool:
12011217
def __lt__(self, other: Any) -> bool:
12021218
if isinstance(other, Component):
12031219
return ComparableTuple((self.type_, self.group, self.name, self.version)) < \
1204-
ComparableTuple((other.type_, other.group, other.name, other.version))
1220+
ComparableTuple((other.type_, other.group, other.name, other.version))
12051221
return NotImplemented
12061222

12071223
def __hash__(self) -> int:

cyclonedx/model/service.py

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@
3434
from .bom_ref import BomRef
3535
from .dependency import Dependable
3636
from .release_note import ReleaseNotes
37+
from ..schema.schema import (
38+
SchemaVersion1Dot0,
39+
SchemaVersion1Dot1,
40+
SchemaVersion1Dot2,
41+
SchemaVersion1Dot3,
42+
SchemaVersion1Dot4
43+
)
3744

3845
"""
3946
This set of classes represents the data that is possible about known Services.
@@ -294,6 +301,7 @@ def services(self, services: Iterable['Service']) -> None:
294301
self._services = SortedSet(services)
295302

296303
@property # type: ignore[misc]
304+
@serializable.view(SchemaVersion1Dot4)
297305
@serializable.xml_sequence(14)
298306
def release_notes(self) -> Optional[ReleaseNotes]:
299307
"""
@@ -309,6 +317,8 @@ def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None:
309317
self._release_notes = release_notes
310318

311319
@property # type: ignore[misc]
320+
@serializable.view(SchemaVersion1Dot3)
321+
@serializable.view(SchemaVersion1Dot4)
312322
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
313323
@serializable.xml_sequence(12)
314324
def properties(self) -> "SortedSet[Property]":

cyclonedx/output/__init__.py

+2-24
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,11 @@
2222
import importlib
2323
import os
2424
from abc import ABC, abstractmethod
25-
from enum import Enum
25+
2626
from typing import cast
2727

2828
from ..model.bom import Bom
29-
30-
31-
class OutputFormat(str, Enum):
32-
JSON: str = 'Json'
33-
XML: str = 'Xml'
34-
35-
36-
class SchemaVersion(str, Enum):
37-
V1_0: str = 'V1Dot0'
38-
V1_1: str = 'V1Dot1'
39-
V1_2: str = 'V1Dot2'
40-
V1_3: str = 'V1Dot3'
41-
V1_4: str = 'V1Dot4'
42-
43-
def to_version(self) -> str:
44-
"""
45-
Return as a version string - e.g. `1.4`
46-
47-
Returns:
48-
`str` version
49-
"""
50-
return f'{self.value[1]}.{self.value[5]}'
51-
29+
from ..schema import OutputFormat, SchemaVersion
5230

5331
LATEST_SUPPORTED_SCHEMA_VERSION = SchemaVersion.V1_4
5432

cyclonedx/output/json.py

+53-54
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
from ..model.bom import Bom
2626
from ..model.component import Component
2727
from . import BaseOutput, SchemaVersion
28-
from .schema import (
28+
from ..schema.schema import (
29+
SCHEMA_VERSIONS,
2930
BaseSchemaVersion,
3031
SchemaVersion1Dot0,
3132
SchemaVersion1Dot1,
@@ -54,64 +55,62 @@ def schema_version(self) -> SchemaVersion:
5455

5556
def generate(self, force_regeneration: bool = False) -> None:
5657
# New Way
57-
if self.schema_version == SchemaVersion.V1_4:
58-
if self.generated and force_regeneration:
59-
self.get_bom().validate()
60-
bom_json = json.loads(self.get_bom().as_json())
61-
bom_json.update({
62-
'$schema': self._get_schema_uri(),
63-
'bomFormat': 'CycloneDX',
64-
'specVersion': '1.4'
65-
})
66-
self._json_output = json.dumps(bom_json)
67-
self.generated = True
68-
return
69-
elif self.generated:
70-
return
71-
else:
72-
self.get_bom().validate()
73-
bom_json = json.loads(self.get_bom().as_json())
74-
bom_json.update({
75-
'$schema': self._get_schema_uri(),
76-
'bomFormat': 'CycloneDX',
77-
'specVersion': '1.4'
78-
})
79-
self._json_output = json.dumps(bom_json)
80-
self.generated = True
81-
return
82-
83-
# Old Way
84-
if self.generated and not force_regeneration:
85-
return
86-
87-
bom = self.get_bom()
88-
bom.validate()
89-
9058
schema_uri: Optional[str] = self._get_schema_uri()
9159
if not schema_uri:
9260
raise FormatNotSupportedException(
9361
f'JSON is not supported by CycloneDX in schema version {self.schema_version.to_version()}')
9462

95-
extras = {}
96-
if self.bom_supports_dependencies():
97-
dep_components: Iterable[Component] = bom.components
98-
if bom.metadata.component:
99-
dep_components = [bom.metadata.component, *dep_components]
100-
dependencies = []
101-
for component in dep_components:
102-
dependencies.append({
103-
'ref': str(component.bom_ref),
104-
'dependsOn': [*map(str, component.dependencies)]
105-
})
106-
if dependencies:
107-
extras["dependencies"] = dependencies
108-
del dep_components
109-
110-
bom_json = json.loads(json.dumps(bom, cls=CycloneDxJSONEncoder))
111-
bom_json = json.loads(self._specialise_output_for_schema_version(bom_json=bom_json))
112-
self._json_output = json.dumps({**bom_json, **self._create_bom_element(), **extras})
113-
114-
self.generated = True
63+
# if self.schema_version == SchemaVersion.V1_4:
64+
_json_core = {
65+
'$schema': schema_uri,
66+
'bomFormat': 'CycloneDX',
67+
'specVersion': self.schema_version.to_version()
68+
}
69+
_view = SCHEMA_VERSIONS.get(self.get_schema_version())
70+
if self.generated and force_regeneration:
71+
self.get_bom().validate()
72+
bom_json = json.loads(self.get_bom().as_json(view_=_view))
73+
bom_json.update(_json_core)
74+
self._json_output = json.dumps(bom_json)
75+
self.generated = True
76+
return
77+
elif self.generated:
78+
return
79+
else:
80+
self.get_bom().validate()
81+
bom_json = json.loads(self.get_bom().as_json(view_=_view))
82+
bom_json.update(_json_core)
83+
self._json_output = json.dumps(bom_json)
84+
self.generated = True
85+
return
86+
87+
# Old Way
88+
# if self.generated and not force_regeneration:
89+
# return
90+
#
91+
# bom = self.get_bom()
92+
# bom.validate()
93+
#
94+
# extras = {}
95+
# if self.bom_supports_dependencies():
96+
# dep_components: Iterable[Component] = bom.components
97+
# if bom.metadata.component:
98+
# dep_components = [bom.metadata.component, *dep_components]
99+
# dependencies = []
100+
# for component in dep_components:
101+
# dependencies.append({
102+
# 'ref': str(component.bom_ref),
103+
# 'dependsOn': [*map(str, component.dependencies)]
104+
# })
105+
# if dependencies:
106+
# extras["dependencies"] = dependencies
107+
# del dep_components
108+
#
109+
# bom_json = json.loads(json.dumps(bom, cls=CycloneDxJSONEncoder))
110+
# bom_json = json.loads(self._specialise_output_for_schema_version(bom_json=bom_json))
111+
# self._json_output = json.dumps({**bom_json, **self._create_bom_element(), **extras})
112+
#
113+
# self.generated = True
115114

116115
def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str:
117116
if not self.bom_supports_metadata():

cyclonedx/schema/__init__.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# encoding: utf-8
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
from enum import Enum
18+
19+
20+
class OutputFormat(str, Enum):
21+
JSON: str = 'Json'
22+
XML: str = 'Xml'
23+
24+
25+
class SchemaVersion(str, Enum):
26+
V1_0: str = 'V1Dot0'
27+
V1_1: str = 'V1Dot1'
28+
V1_2: str = 'V1Dot2'
29+
V1_3: str = 'V1Dot3'
30+
V1_4: str = 'V1Dot4'
31+
32+
def to_version(self) -> str:
33+
"""
34+
Return as a version string - e.g. `1.4`
35+
36+
Returns:
37+
`str` version
38+
"""
39+
return f'{self.value[1]}.{self.value[5]}'

0 commit comments

Comments
 (0)