Skip to content

Commit 50c4931

Browse files
committed
refactor(cargo): Migrate parsing of JSON nodes to using data classes
This simplifies (and suggests inlining) the code in preparation for moving to kotlinx-serialization. Signed-off-by: Sebastian Schuberth <[email protected]>
1 parent 92bfc97 commit 50c4931

File tree

3 files changed

+112
-78
lines changed

3 files changed

+112
-78
lines changed

plugins/package-managers/cargo/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
implementation(project(":utils:spdx-utils"))
3939

4040
implementation(libs.jacksonDatabind)
41+
implementation(libs.jacksonModuleKotlin)
4142
implementation(libs.toml4j)
4243
constraints {
4344
implementation("com.google.code.gson:gson:2.10.1") {

plugins/package-managers/cargo/src/main/kotlin/Cargo.kt

Lines changed: 47 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
package org.ossreviewtoolkit.plugins.packagemanagers.cargo
2323

24-
import com.fasterxml.jackson.databind.JsonNode
24+
import com.fasterxml.jackson.module.kotlin.readValue
2525

2626
import com.moandjiezana.toml.Toml
2727

@@ -49,7 +49,6 @@ import org.ossreviewtoolkit.model.jsonMapper
4949
import org.ossreviewtoolkit.model.orEmpty
5050
import org.ossreviewtoolkit.utils.common.CommandLineTool
5151
import org.ossreviewtoolkit.utils.common.splitOnWhitespace
52-
import org.ossreviewtoolkit.utils.common.textValueOrEmpty
5352
import org.ossreviewtoolkit.utils.common.unquote
5453
import org.ossreviewtoolkit.utils.ort.DeclaredLicenseProcessor
5554
import org.ossreviewtoolkit.utils.ort.ProcessedDeclaredLicense
@@ -86,9 +85,8 @@ class Cargo(
8685
* Cargo.lock is located next to Cargo.toml or in one of the parent directories. The latter is the case when the
8786
* project is part of a workspace. Cargo.lock is then located next to the Cargo.toml file defining the workspace.
8887
*/
89-
private fun resolveLockfile(metadata: JsonNode): File {
90-
val workspaceRoot = metadata["workspace_root"].textValueOrEmpty()
91-
val workingDir = File(workspaceRoot)
88+
private fun resolveLockfile(metadata: CargoMetadata): File {
89+
val workingDir = File(metadata.workspaceRoot)
9290
val lockfile = workingDir.resolve("Cargo.lock")
9391

9492
requireLockfile(workingDir) { lockfile.isFile }
@@ -136,29 +134,23 @@ class Cargo(
136134
name: String,
137135
version: String,
138136
packages: Map<String, Package>,
139-
metadata: JsonNode
137+
metadata: CargoMetadata
140138
): PackageReference {
141-
val node = metadata["packages"].single {
142-
it["name"].textValue() == name && it["version"].textValue() == version
143-
}
139+
val node = metadata.packages.single { it.name == name && it.version == version }
144140

145-
val dependencies = node["dependencies"].filter {
141+
val dependencies = node.dependencies.filter {
146142
// Filter dev and build dependencies, because they are not transitive.
147-
val kind = it["kind"].textValueOrEmpty()
148-
kind != "dev" && kind != "build"
143+
it.kind != "dev" && it.kind != "build"
149144
}.mapNotNullTo(mutableSetOf()) {
150145
// TODO: Handle renamed dependencies here, see:
151146
// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml
152-
val dependencyName = it["name"].textValue()
153-
154-
getResolvedVersion(name, version, dependencyName, metadata)?.let { dependencyVersion ->
155-
buildDependencyTree(dependencyName, dependencyVersion, packages, metadata)
147+
getResolvedVersion(name, version, it.name, metadata)?.let { dependencyVersion ->
148+
buildDependencyTree(it.name, dependencyVersion, packages, metadata)
156149
}
157150
}
158151

159-
val id = parseCargoId(node)
160-
val pkg = packages.getValue(id)
161-
val linkage = if (isProjectDependency(id)) PackageLinkage.PROJECT_STATIC else PackageLinkage.STATIC
152+
val pkg = packages.getValue(node.id)
153+
val linkage = if (isProjectDependency(node.id)) PackageLinkage.PROJECT_STATIC else PackageLinkage.STATIC
162154

163155
return pkg.toReference(linkage, dependencies)
164156
}
@@ -172,29 +164,26 @@ class Cargo(
172164

173165
val workingDir = definitionFile.parentFile
174166
val metadataProcess = run(workingDir, "metadata", "--format-version=1")
175-
val metadata = jsonMapper.readTree(metadataProcess.stdout)
167+
val metadata = jsonMapper.readValue<CargoMetadata>(metadataProcess.stdout)
176168
val hashes = readHashes(resolveLockfile(metadata))
177169

178-
val packages = metadata["packages"].associateBy(
179-
{ parseCargoId(it) },
170+
val packages = metadata.packages.associateBy(
171+
{ it.id },
180172
{ parsePackage(it, hashes) }
181173
)
182174

183-
val projectId = metadata["workspace_members"]
184-
.map { it.textValueOrEmpty() }
185-
.single { it.startsWith("$projectName $projectVersion") }
175+
val projectId = metadata.workspaceMembers.single { it.startsWith("$projectName $projectVersion") }
186176

187-
val projectNode = metadata["packages"].single { it["id"].textValueOrEmpty() == projectId }
188-
val groupedDependencies = projectNode["dependencies"].groupBy { it["kind"].textValueOrEmpty() }
177+
val projectNode = metadata.packages.single { it.id == projectId }
178+
val groupedDependencies = projectNode.dependencies.groupBy { it.kind.orEmpty() }
189179

190-
fun getTransitiveDependencies(directDependencies: List<JsonNode>?, scope: String): Scope? {
180+
fun getTransitiveDependencies(directDependencies: List<CargoMetadata.Dependency>?, scope: String): Scope? {
191181
if (directDependencies == null) return null
192182

193183
val transitiveDependencies = directDependencies
194184
.mapNotNull { dependency ->
195-
val dependencyName = dependency["name"].textValue()
196-
val version = getResolvedVersion(projectName, projectVersion, dependencyName, metadata)
197-
version?.let { Pair(dependencyName, it) }
185+
val version = getResolvedVersion(projectName, projectVersion, dependency.name, metadata)
186+
version?.let { Pair(dependency.name, it) }
198187
}
199188
.mapTo(mutableSetOf()) {
200189
buildDependencyTree(name = it.first, version = it.second, packages = packages, metadata = metadata)
@@ -239,18 +228,16 @@ class Cargo(
239228

240229
private val PATH_DEPENDENCY_REGEX = Regex("""^.*\(path\+file://(.*)\)$""")
241230

242-
private fun parseCargoId(node: JsonNode) = node["id"].textValueOrEmpty()
243-
244-
private fun parseDeclaredLicenses(node: JsonNode): Set<String> {
245-
val declaredLicenses = node["license"].textValueOrEmpty().split('/')
231+
private fun parseDeclaredLicenses(pkg: CargoMetadata.Package): Set<String> {
232+
val declaredLicenses = pkg.license.orEmpty().split('/')
246233
.map { it.trim() }
247234
.filterTo(mutableSetOf()) { it.isNotEmpty() }
248235

249236
// Cargo allows declaring non-SPDX licenses only by referencing a license file. If a license file is specified, add
250237
// an unknown declared license to indicate that there is a declared license, but we cannot know which it is at this
251238
// point.
252239
// See: https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields
253-
if (node["license_file"].textValueOrEmpty().isNotBlank()) {
240+
if (pkg.licenseFile.orEmpty().isNotBlank()) {
254241
declaredLicenses += SpdxConstants.NOASSERTION
255242
}
256243

@@ -264,51 +251,43 @@ private fun processDeclaredLicenses(licenses: Set<String>): ProcessedDeclaredLic
264251
// https://github.com/rust-lang/cargo/pull/4920
265252
DeclaredLicenseProcessor.process(licenses, operator = SpdxOperator.OR)
266253

267-
private fun parsePackage(node: JsonNode, hashes: Map<String, String>): Package {
268-
val declaredLicenses = parseDeclaredLicenses(node)
254+
private fun parsePackage(pkg: CargoMetadata.Package, hashes: Map<String, String>): Package {
255+
val declaredLicenses = parseDeclaredLicenses(pkg)
269256
val declaredLicensesProcessed = processDeclaredLicenses(declaredLicenses)
270257

271258
return Package(
272-
id = parsePackageId(node),
273-
authors = parseAuthors(node["authors"]),
259+
id = Identifier(
260+
type = "Crate",
261+
// Note that Rust / Cargo do not support package namespaces, see:
262+
// https://samsieber.tech/posts/2020/09/registry-structure-influence/
263+
namespace = "",
264+
name = pkg.name,
265+
version = pkg.version
266+
),
267+
authors = pkg.authors.mapNotNullTo(mutableSetOf()) { parseAuthorString(it) },
274268
declaredLicenses = declaredLicenses,
275269
declaredLicensesProcessed = declaredLicensesProcessed,
276-
description = node["description"].textValueOrEmpty(),
270+
description = pkg.description.orEmpty(),
277271
binaryArtifact = RemoteArtifact.EMPTY,
278-
sourceArtifact = parseSourceArtifact(node, hashes).orEmpty(),
272+
sourceArtifact = parseSourceArtifact(pkg, hashes).orEmpty(),
279273
homepageUrl = "",
280-
vcs = parseVcsInfo(node)
274+
vcs = VcsHost.parseUrl(pkg.repository.orEmpty())
281275
)
282276
}
283277

284-
private fun parsePackageId(node: JsonNode) =
285-
Identifier(
286-
type = "Crate",
287-
// Note that Rust / Cargo do not support package namespaces, see:
288-
// https://samsieber.tech/posts/2020/09/registry-structure-influence/
289-
namespace = "",
290-
name = node["name"].textValueOrEmpty(),
291-
version = node["version"].textValueOrEmpty()
292-
)
293-
294-
private fun parseRepositoryUrl(node: JsonNode) = node["repository"].textValueOrEmpty()
295-
296278
// Match source dependencies that directly reference git repositories. The specified tag or branch
297279
// name is ignored (i.e. not captured) in favor of the actual commit hash that they currently refer
298280
// to.
299281
// See https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories
300282
// for the specification for this kind of dependency.
301283
private val GIT_DEPENDENCY_REGEX = Regex("git\\+(https://.*)\\?(?:rev|tag|branch)=.+#([0-9a-zA-Z]+)")
302284

303-
private fun parseSourceArtifact(node: JsonNode, hashes: Map<String, String>): RemoteArtifact? {
304-
val source = node["source"]?.textValue() ?: return null
285+
private fun parseSourceArtifact(pkg: CargoMetadata.Package, hashes: Map<String, String>): RemoteArtifact? {
286+
val source = pkg.source ?: return null
305287

306288
if (source == "registry+https://github.com/rust-lang/crates.io-index") {
307-
val name = node["name"]?.textValue() ?: return null
308-
val version = node["version"]?.textValue() ?: return null
309-
val url = "https://crates.io/api/v1/crates/$name/$version/download"
310-
val id = parseCargoId(node)
311-
val hash = Hash.create(hashes[id].orEmpty())
289+
val url = "https://crates.io/api/v1/crates/${pkg.name}/${pkg.version}/download"
290+
val hash = Hash.create(hashes[pkg.id].orEmpty())
312291
return RemoteArtifact(url, hash)
313292
}
314293

@@ -317,34 +296,24 @@ private fun parseSourceArtifact(node: JsonNode, hashes: Map<String, String>): Re
317296
return RemoteArtifact(url, Hash.create(hash))
318297
}
319298

320-
private fun parseVcsInfo(node: JsonNode) = VcsHost.parseUrl(parseRepositoryUrl(node))
321-
322299
private fun getResolvedVersion(
323300
parentName: String,
324301
parentVersion: String,
325302
dependencyName: String,
326-
metadata: JsonNode
303+
metadata: CargoMetadata
327304
): String? {
328-
val node = metadata["resolve"]["nodes"].single {
329-
it["id"].textValue().startsWith("$parentName $parentVersion")
330-
}
305+
val node = metadata.resolve.nodes.single { it.id.startsWith("$parentName $parentVersion") }
331306

332-
// This is null if the dependency is optional and the feature was not enabled. In this case the version was not
307+
// This is empty if the dependency is optional and the feature was not enabled. In that case the version was not
333308
// resolved and the dependency should not appear in the dependency tree. An example for a dependency string is
334309
// "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", for more details see
335310
// https://doc.rust-lang.org/cargo/commands/cargo-metadata.html.
336-
node["dependencies"].forEach {
337-
val substrings = it.textValue().splitOnWhitespace()
338-
require(substrings.size > 1) { "Unexpected format while parsing dependency JSON node." }
311+
node.dependencies.forEach {
312+
val substrings = it.splitOnWhitespace()
313+
require(substrings.size > 1) { "Unexpected format while parsing dependency '$it'." }
339314

340315
if (substrings[0] == dependencyName) return substrings[1]
341316
}
342317

343318
return null
344319
}
345-
346-
/**
347-
* Parse information about authors from the given [node] with package metadata.
348-
*/
349-
private fun parseAuthors(node: JsonNode?): Set<String> =
350-
node?.mapNotNullTo(mutableSetOf()) { parseAuthorString(it.textValue()) } ?: emptySet()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (C) 2023 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.packagemanagers.cargo
21+
22+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
23+
24+
/**
25+
* See https://doc.rust-lang.org/cargo/commands/cargo-metadata.html.
26+
*/
27+
@JsonIgnoreProperties(ignoreUnknown = true)
28+
internal data class CargoMetadata(
29+
val packages: List<Package>,
30+
val workspaceMembers: List<String>,
31+
val resolve: Resolve,
32+
val workspaceRoot: String
33+
) {
34+
@JsonIgnoreProperties(ignoreUnknown = true)
35+
data class Package(
36+
val name: String,
37+
val version: String,
38+
val id: String,
39+
val license: String?,
40+
val licenseFile: String?,
41+
val description: String?,
42+
val source: String?,
43+
val dependencies: List<Dependency>,
44+
val authors: List<String>,
45+
val repository: String?
46+
)
47+
48+
@JsonIgnoreProperties(ignoreUnknown = true)
49+
data class Dependency(
50+
val name: String,
51+
val kind: String?
52+
)
53+
54+
data class Resolve(
55+
val nodes: List<Node>,
56+
val root: String?
57+
)
58+
59+
@JsonIgnoreProperties(ignoreUnknown = true)
60+
data class Node(
61+
val id: String,
62+
val dependencies: List<String>
63+
)
64+
}

0 commit comments

Comments
 (0)