diff --git a/clients/clearly-defined/build.gradle.kts b/clients/clearly-defined/build.gradle.kts index 5caba7f3b4da4..5b9690bfd0f79 100644 --- a/clients/clearly-defined/build.gradle.kts +++ b/clients/clearly-defined/build.gradle.kts @@ -18,18 +18,37 @@ * License-Filename: LICENSE */ -val jacksonVersion: String by project +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +val kotlinxSerializationVersion: String by project val retrofitVersion: String by project +val retrofitKotlinxSerializationConverterVersion: String by project plugins { // Apply core plugins. `java-library` + + // Apply third-party plugins. + kotlin("plugin.serialization") } dependencies { api("com.squareup.retrofit2:retrofit:$retrofitVersion") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") - implementation("com.squareup.retrofit2:converter-jackson:$retrofitVersion") implementation("com.squareup.retrofit2:converter-scalars:$retrofitVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + implementation( + "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:" + + retrofitKotlinxSerializationConverterVersion + ) +} + +tasks.withType { + val customCompilerArgs = listOf( + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" + ) + + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + customCompilerArgs + } } diff --git a/clients/clearly-defined/src/funTest/kotlin/ClearlyDefinedServiceFunTest.kt b/clients/clearly-defined/src/funTest/kotlin/ClearlyDefinedServiceFunTest.kt index 28828efd0b3e8..e6bbdeada8f35 100644 --- a/clients/clearly-defined/src/funTest/kotlin/ClearlyDefinedServiceFunTest.kt +++ b/clients/clearly-defined/src/funTest/kotlin/ClearlyDefinedServiceFunTest.kt @@ -19,8 +19,6 @@ package org.ossreviewtoolkit.clients.clearlydefined -import com.fasterxml.jackson.module.kotlin.readValue - import io.kotest.core.spec.style.WordSpec import io.kotest.matchers.collections.beEmpty import io.kotest.matchers.comparables.shouldBeGreaterThan @@ -33,6 +31,9 @@ import io.kotest.matchers.string.shouldStartWith import java.io.File +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.decodeFromStream + import org.ossreviewtoolkit.clients.clearlydefined.ClearlyDefinedService.ContributionInfo import org.ossreviewtoolkit.clients.clearlydefined.ClearlyDefinedService.ContributionPatch import org.ossreviewtoolkit.clients.clearlydefined.ClearlyDefinedService.Server @@ -41,9 +42,9 @@ class ClearlyDefinedServiceFunTest : WordSpec({ "A contribution patch" should { "be correctly deserialized when using empty facet arrays" { // See https://github.com/clearlydefined/curated-data/blob/0b2db78/curations/maven/mavencentral/com.google.code.gson/gson.yaml#L10-L11. - val curationWithEmptyFacetArrays = File("src/funTest/assets/gson.json") - - val curation = ClearlyDefinedService.JSON_MAPPER.readValue(curationWithEmptyFacetArrays) + val curation = File("src/funTest/assets/gson.json").inputStream().use { + ClearlyDefinedService.JSON.decodeFromStream(it) + } curation.described?.facets?.dev.shouldNotBeNull() should beEmpty() curation.described?.facets?.tests.shouldNotBeNull() should beEmpty() @@ -110,7 +111,7 @@ class ClearlyDefinedServiceFunTest : WordSpec({ "only serialize non-null values" { val contributionPatch = ContributionPatch(info, listOf(patch)) - val patchJson = ClearlyDefinedService.JSON_MAPPER.writeValueAsString(contributionPatch) + val patchJson = ClearlyDefinedService.JSON.encodeToString(contributionPatch) patchJson shouldNot include("null") } diff --git a/clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt b/clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt index 09bebd8a59a03..5bdcc8689020e 100644 --- a/clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt +++ b/clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt @@ -19,17 +19,17 @@ package org.ossreviewtoolkit.clients.clearlydefined -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.json.JsonMapper -import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.ResponseBody import retrofit2.Retrofit -import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.scalars.ScalarsConverterFactory import retrofit2.http.Body import retrofit2.http.GET @@ -45,9 +45,9 @@ import retrofit2.http.Query interface ClearlyDefinedService { companion object { /** - * The mapper for JSON (de-)serialization used by this service. + * The JSON (de-)serialization object used by this service. */ - val JSON_MAPPER = JsonMapper().registerKotlinModule() + val JSON = Json { encodeDefaults = false } /** * Create a ClearlyDefined service instance for communicating with the given [server], optionally using a @@ -61,11 +61,12 @@ interface ClearlyDefinedService { * optionally using a pre-built OkHttp [client]. */ fun create(url: String, client: OkHttpClient? = null): ClearlyDefinedService { + val contentType = "application/json".toMediaType() val retrofit = Retrofit.Builder() .apply { if (client != null) client(client) } .baseUrl(url) .addConverterFactory(ScalarsConverterFactory.create()) - .addConverterFactory(JacksonConverterFactory.create(JSON_MAPPER)) + .addConverterFactory(JSON.asConverterFactory(contentType)) .build() return retrofit.create(ClearlyDefinedService::class.java) @@ -97,7 +98,6 @@ interface ClearlyDefinedService { /** * The return type for https://api.clearlydefined.io/api-docs/#/definitions/post_definitions. */ - @JsonInclude(JsonInclude.Include.NON_NULL) data class Defined( val coordinates: Coordinates, val described: Described, @@ -105,17 +105,16 @@ interface ClearlyDefinedService { val files: List? = null, val scores: FinalScore, - @JsonProperty("_id") + @SerialName("_id") val id: String? = null, - @JsonProperty("_meta") + @SerialName("_meta") val meta: Meta ) { /** * Return the harvest status of a described component, also see * https://github.com/clearlydefined/website/blob/de42d2c/src/components/Navigation/Ui/HarvestIndicator.js#L8. */ - @JsonIgnore fun getHarvestStatus() = when { described.tools == null -> HarvestStatus.NOT_HARVESTED @@ -127,6 +126,7 @@ interface ClearlyDefinedService { /** * See https://github.com/clearlydefined/service/blob/4e210d7/schemas/swagger.yaml#L84-L101. */ + @Serializable data class ContributionPatch( val contributionInfo: ContributionInfo, val patches: List @@ -135,6 +135,7 @@ interface ClearlyDefinedService { /** * See https://github.com/clearlydefined/service/blob/4e210d7/schemas/swagger.yaml#L87-L97. */ + @Serializable data class ContributionInfo( val type: ContributionType, @@ -163,6 +164,7 @@ interface ClearlyDefinedService { /** * See https://github.com/clearlydefined/service/blob/53acc01/routes/curations.js#L86-L89. */ + @Serializable data class ContributionSummary( val prNumber: Int, val url: String @@ -171,7 +173,7 @@ interface ClearlyDefinedService { /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/harvest-1.0.json#L12-L22. */ - @JsonInclude(JsonInclude.Include.NON_NULL) + @Serializable data class HarvestRequest( val tool: String? = null, val coordinates: String, diff --git a/clients/clearly-defined/src/main/kotlin/Curation.kt b/clients/clearly-defined/src/main/kotlin/Curation.kt index 4a318abdf8412..d453a217aff89 100644 --- a/clients/clearly-defined/src/main/kotlin/Curation.kt +++ b/clients/clearly-defined/src/main/kotlin/Curation.kt @@ -17,25 +17,27 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.clients.clearlydefined +@file:UseSerializers(FileSerializer::class, URISerializer::class) -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonValue -import com.fasterxml.jackson.databind.JsonNode +package org.ossreviewtoolkit.clients.clearlydefined import java.io.File import java.net.URI +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.json.JsonElement + +@Serializable data class ContributedCurations( val curations: Map, - val contributions: List + val contributions: List ) /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/curation-1.0.json#L7-L17. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Curation( val described: CurationDescribed? = null, val licensed: CurationLicensed? = null, @@ -45,7 +47,7 @@ data class Curation( /** * See https://github.com/clearlydefined/service/blob/0d00f25/schemas/curation-1.0.json#L70-L119. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class CurationDescribed( val facets: CurationFacets? = null, val sourceLocation: SourceLocation? = null, @@ -57,7 +59,7 @@ data class CurationDescribed( /** * See https://github.com/clearlydefined/service/blob/0d00f25/schemas/curation-1.0.json#L74-L90. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class CurationFacets( val data: List? = null, val dev: List? = null, @@ -69,7 +71,7 @@ data class CurationFacets( /** * See https://github.com/clearlydefined/service/blob/0d00f25/schemas/curation-1.0.json#L243-L247. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class CurationLicensed( val declared: String? = null ) @@ -77,7 +79,7 @@ data class CurationLicensed( /** * See https://github.com/clearlydefined/service/blob/0d00f25/schemas/curation-1.0.json#L201-L229. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class CurationFileEntry( val path: File, val license: String? = null, @@ -87,6 +89,7 @@ data class CurationFileEntry( /** * See https://github.com/clearlydefined/service/blob/b339cb7/schemas/curations-1.0.json#L8-L15. */ +@Serializable data class Patch( val coordinates: Coordinates, val revisions: Map @@ -96,7 +99,7 @@ data class Patch( * See https://github.com/clearlydefined/service/blob/b339cb7/schemas/curations-1.0.json#L64-L83 and * https://docs.clearlydefined.io/using-data#a-note-on-definition-coordinates. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable(CoordinatesSerializer::class) data class Coordinates( /** * The type of the component. For example, npm, git, nuget, maven, etc. This talks about the shape of the @@ -125,22 +128,4 @@ data class Coordinates( * omitted, the latest revision is used (if that makes sense for the provider). */ val revision: String? = null -) { - companion object { - @JsonCreator - @JvmStatic - fun fromString(value: String): Coordinates { - val parts = value.split('/', limit = 5) - return Coordinates( - type = ComponentType.fromString(parts[0]), - provider = Provider.fromString(parts[1]), - namespace = parts[2].takeUnless { it == "-" }, - name = parts[3], - revision = parts.getOrNull(4) - ) - } - } - - @JsonValue - override fun toString() = listOfNotNull(type, provider, namespace ?: "-", name, revision).joinToString("/") -} +) diff --git a/clients/clearly-defined/src/main/kotlin/Definition.kt b/clients/clearly-defined/src/main/kotlin/Definition.kt index 21c4cd8d3f05b..82a284ca4042e 100644 --- a/clients/clearly-defined/src/main/kotlin/Definition.kt +++ b/clients/clearly-defined/src/main/kotlin/Definition.kt @@ -17,16 +17,20 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.clients.clearlydefined +@file:UseSerializers(FileSerializer::class, URISerializer::class) -import com.fasterxml.jackson.annotation.JsonInclude +package org.ossreviewtoolkit.clients.clearlydefined import java.io.File import java.net.URI +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers + /** * See https://github.com/clearlydefined/service/blob/b339cb7/schemas/definition-1.0.json#L48-L61. */ +@Serializable data class Meta( val schemaVersion: String, val updated: String @@ -35,6 +39,7 @@ data class Meta( /** * See https://github.com/clearlydefined/service/blob/b339cb7/schemas/definition-1.0.json#L80-L89. */ +@Serializable data class FinalScore( val effective: Int, val tool: Int @@ -43,7 +48,7 @@ data class FinalScore( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L90-L134. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class FileEntry( val path: File, val license: String? = null, @@ -57,7 +62,7 @@ data class FileEntry( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L135-L144. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Hashes( val md5: String? = null, val sha1: String? = null, @@ -68,7 +73,7 @@ data class Hashes( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L145-L179. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Described( val score: DescribedScore? = null, val toolScore: DescribedScore? = null, @@ -86,6 +91,7 @@ data class Described( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L180-L190. */ +@Serializable data class DescribedScore( val total: Int, val date: Int, @@ -95,6 +101,7 @@ data class DescribedScore( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L191-L204. */ +@Serializable data class LicensedScore( val total: Int, val declared: Int, @@ -107,7 +114,7 @@ data class LicensedScore( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L211-L235. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class SourceLocation( // The following properties match those of Coordinates, except that the revision is mandatory here. val type: ComponentType, @@ -123,7 +130,7 @@ data class SourceLocation( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L236-L253. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class URLs( val registry: URI? = null, val version: URI? = null, @@ -133,7 +140,7 @@ data class URLs( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L254-L263. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Licensed( val score: LicensedScore? = null, val toolScore: LicensedScore? = null, @@ -144,7 +151,7 @@ data class Licensed( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L264-L275. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Facets( val core: Facet? = null, val data: Facet? = null, @@ -157,7 +164,7 @@ data class Facets( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L276-L286. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Facet( val files: Int? = null, val attribution: Attribution? = null, @@ -167,7 +174,7 @@ data class Facet( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L287-L301. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Attribution( val parties: List? = null, val unknown: Int? = null @@ -176,7 +183,7 @@ data class Attribution( /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L305-L319. */ -@JsonInclude(JsonInclude.Include.NON_NULL) +@Serializable data class Discovered( val expressions: List? = null, val unknown: Int? = null diff --git a/clients/clearly-defined/src/main/kotlin/Enums.kt b/clients/clearly-defined/src/main/kotlin/Enums.kt index 0ca570d3761e9..add7825777191 100644 --- a/clients/clearly-defined/src/main/kotlin/Enums.kt +++ b/clients/clearly-defined/src/main/kotlin/Enums.kt @@ -19,12 +19,12 @@ package org.ossreviewtoolkit.clients.clearlydefined -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonValue +import kotlinx.serialization.Serializable /** * See https://github.com/clearlydefined/service/blob/48f2c97/schemas/definition-1.0.json#L32-L48. */ +@Serializable enum class ComponentType(val value: String) { NPM("npm"), CRATE("crate"), @@ -41,19 +41,18 @@ enum class ComponentType(val value: String) { DEBIAN_SOURCES("debsrc"); companion object { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JvmStatic fun fromString(value: String) = enumValues().single { value.equals(it.value, ignoreCase = true) } } - @JsonValue override fun toString() = value } /** * See https://github.com/clearlydefined/service/blob/48f2c97/schemas/definition-1.0.json#L49-L65. */ +@Serializable enum class Provider(val value: String) { NPM_JS("npmjs"), COCOAPODS("cocoapods"), @@ -70,29 +69,26 @@ enum class Provider(val value: String) { DEBIAN("debian"); companion object { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JvmStatic fun fromString(value: String) = enumValues().single { value.equals(it.value, ignoreCase = true) } } - @JsonValue override fun toString() = value } /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L128. */ +@Serializable enum class Nature { LICENSE, NOTICE; companion object { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JvmStatic fun fromString(value: String) = enumValues().single { value.equals(it.name, ignoreCase = true) } } - @JsonValue override fun toString() = name.lowercase() } @@ -107,13 +103,11 @@ enum class ContributionType { OTHER; companion object { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JvmStatic fun fromString(value: String) = enumValues().single { value.equals(it.name, ignoreCase = true) } } - @JsonValue override fun toString() = name.titlecase() } diff --git a/clients/clearly-defined/src/main/kotlin/Serializers.kt b/clients/clearly-defined/src/main/kotlin/Serializers.kt new file mode 100644 index 0000000000000..a0066aa30393e --- /dev/null +++ b/clients/clearly-defined/src/main/kotlin/Serializers.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 Bosch.IO GmbH + * + * 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.clearlydefined + +import java.io.File +import java.net.URI + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializer(Coordinates::class) +object CoordinatesSerializer : KSerializer { + override fun serialize(encoder: Encoder, value: Coordinates) { + val string = with(value) { + listOfNotNull(type, provider, namespace ?: "-", name, revision) + }.joinToString("/") + + encoder.encodeString(string) + } + + override fun deserialize(decoder: Decoder): Coordinates { + val string = decoder.decodeString() + val parts = string.split('/', limit = 5) + + return Coordinates( + type = ComponentType.valueOf(parts[0]), + provider = Provider.valueOf(parts[1]), + namespace = parts[2].takeUnless { it == "-" }, + name = parts[3], + revision = parts.getOrNull(4) + ) + } +} + +@Serializer(File::class) +object FileSerializer : KSerializer { + override fun serialize(encoder: Encoder, value: File) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder) = File(decoder.decodeString()) +} + +@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()) +} diff --git a/clients/clearly-defined/src/test/kotlin/ClearlyDefinedServiceTest.kt b/clients/clearly-defined/src/test/kotlin/ClearlyDefinedServiceTest.kt new file mode 100644 index 0000000000000..d72b0c9d55277 --- /dev/null +++ b/clients/clearly-defined/src/test/kotlin/ClearlyDefinedServiceTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Bosch.IO GmbH + * + * 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.clearlydefined + +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.StringSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe + +import kotlinx.serialization.encodeToString + +class ClearlyDefinedServiceTest : StringSpec({ + val json = ClearlyDefinedService.JSON + + "Enums should be serialized correctly" { + assertSoftly { + enumValues().forAll { + json.encodeToString(it) shouldBe it.toString() + } + + enumValues().forAll { + json.encodeToString(it) shouldBe it.toString() + } + + enumValues().forAll { + json.encodeToString(it) shouldBe it.toString() + } + + enumValues().forAll { + json.encodeToString(it) shouldBe it.toString() + } + } + } +})