diff --git a/clients/bazel-module-registry/build.gradle.kts b/clients/bazel-module-registry/build.gradle.kts new file mode 100644 index 0000000000000..67021f76155b6 --- /dev/null +++ b/clients/bazel-module-registry/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + // Apply precompiled plugins. + id("ort-library-conventions") + + // Apply third-party plugins. + alias(libs.plugins.kotlinSerialization) +} + +dependencies { + api(libs.okhttp) + api(libs.retrofit) + + implementation(libs.bundles.kotlinxSerialization) + implementation(libs.retrofit.converter.kotlinxSerialization) +} + +tasks.withType().configureEach { + val customCompilerArgs = listOf( + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" + ) + + compilerOptions { + freeCompilerArgs.addAll(customCompilerArgs) + } +} diff --git a/clients/bazel-module-registry/src/funTest/kotlin/BazelModuleRegistryClientFunTest.kt b/clients/bazel-module-registry/src/funTest/kotlin/BazelModuleRegistryClientFunTest.kt new file mode 100644 index 0000000000000..8ba41c5a52cc5 --- /dev/null +++ b/clients/bazel-module-registry/src/funTest/kotlin/BazelModuleRegistryClientFunTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.clients.bazelmoduleregistry + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.comparables.shouldBeGreaterThan +import io.kotest.matchers.shouldBe + +class BazelModuleRegistryClientTest : WordSpec({ + val service = BazelModuleRegistryClient.create() + val repoUrl = "https://github.com/google/glog" + + "getModuleMetadata" should { + "include a homepage URL and version 0.5.0 for the 'glog' module" { + val metadata = service.getModuleMetadata("glog") + + metadata.homepage.toString() shouldBe repoUrl + metadata.versions.size shouldBeGreaterThan 1 + metadata.versions shouldContain "0.5.0" + } + } + + "getModuleSource" should { + "include URL and hash data" { + val sourceInfo = service.getModuleSourceInfo("glog", "0.5.0") + + sourceInfo.url.toString() shouldBe "$repoUrl/archive/refs/tags/v0.5.0.tar.gz" + sourceInfo.integrity shouldBe "sha256-7t5x8oNxvzmqabRd4jsynTchQBbiBVJps7Xnz9QLWfU=" + } + } +}) diff --git a/clients/bazel-module-registry/src/main/kotlin/BazelModuleRegistryClient.kt b/clients/bazel-module-registry/src/main/kotlin/BazelModuleRegistryClient.kt new file mode 100644 index 0000000000000..e217846edbe39 --- /dev/null +++ b/clients/bazel-module-registry/src/main/kotlin/BazelModuleRegistryClient.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.clients.bazelmoduleregistry + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNamingStrategy + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient + +import retrofit2.Retrofit +import retrofit2.http.GET +import retrofit2.http.Path + +/** + * The JSON (de-)serialization object used by this service. + */ +private val JSON = Json { + ignoreUnknownKeys = true + namingStrategy = JsonNamingStrategy.SnakeCase +} + +/** + * The service uses the Bazel Central Registry by default. + */ +private const val DEFAULT_URL = "https://bcr.bazel.build" + +/** + * Interface for a Bazel Module Registry, based on the directory structure of https://bcr.bazel.build + * and the git repository it is based on (https://github.com/bazelbuild/bazel-central-registry/). + */ +interface BazelModuleRegistryClient { + companion object { + /** + * Create a Bazel Module Registry service instance for communicating with a server running at the given [url], + * defaulting to the Bazel Central Registry, optionally with a pre-built OkHttp [client]. + */ + fun create( + url: String? = null, + client: OkHttpClient? = null + ): BazelModuleRegistryClient { + val bmrClient = client ?: OkHttpClient() + + val contentType = "application/json".toMediaType() + val retrofit = Retrofit.Builder() + .client(bmrClient) + .baseUrl(url ?: DEFAULT_URL) + .addConverterFactory(JSON.asConverterFactory(contentType)) + .build() + + return retrofit.create(BazelModuleRegistryClient::class.java) + } + } + + /** + * Retrieve the metadata for a module. + * E.g. https://bcr.bazel.build/modules/glog/metadata.json. + */ + @GET("modules/{name}/metadata.json") + suspend fun getModuleMetadata(@Path("name") name: String): ModuleMetadata + + /** + * Retrieve information about the source code for a specific version of a module. + * E.g. https://bcr.bazel.build/modules/glog/0.5.0/source.json. + */ + @GET("modules/{name}/{version}/source.json") + suspend fun getModuleSourceInfo(@Path("name") name: String, @Path("version") version: String): ModuleSourceInfo +} diff --git a/clients/bazel-module-registry/src/main/kotlin/Model.kt b/clients/bazel-module-registry/src/main/kotlin/Model.kt new file mode 100644 index 0000000000000..f004b5d53f5c7 --- /dev/null +++ b/clients/bazel-module-registry/src/main/kotlin/Model.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.clients.bazelmoduleregistry + +import java.net.URI + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializer(URI::class) +object URISerializer : KSerializer { + override fun serialize(encoder: Encoder, value: URI) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder) = URI(decoder.decodeString()) +} + +/** + * E.g. https://bcr.bazel.build/modules/glog/metadata.json. + */ +@Serializable +data class ModuleMetadata( + @Serializable(URISerializer::class) val homepage: URI?, + val maintainers: List? = null, + val repository: List? = null, + val versions: List, + // The key in the map is the version, the value the reason for yanking it. + val yankedVersions: Map, +) { + @Serializable + data class Maintainer( + val email: String? = null, + val name: String? = null + ) +} + +/** + * E.g. https://bcr.bazel.build/modules/glog/0.5.0/source.json. + */ +@Serializable +data class ModuleSourceInfo( + val integrity: String, + val patchStrip: Int? = null, + val patches: Map? = null, + val stripPrefix: String? = null, + @Serializable(URISerializer::class) val url: URI, +) diff --git a/settings.gradle.kts b/settings.gradle.kts index ed8997b62b03d..cef54ad5d32fe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ rootProject.name = "oss-review-toolkit" include(":advisor") include(":analyzer") include(":cli") +include(":clients:bazel-module-registry") include(":clients:clearly-defined") include(":clients:fossid-webapp") include(":clients:github-graphql") @@ -48,6 +49,7 @@ include(":utils:scripting") include(":utils:spdx") include(":utils:test") +project(":clients:bazel-module-registry").name = "bazel-module-registry-client" project(":clients:clearly-defined").name = "clearly-defined-client" project(":clients:fossid-webapp").name = "fossid-webapp-client" project(":clients:github-graphql").name = "github-graphql-client"