From 5a7eaecc5bae444bb1b6cbbde0c4546c13a154e7 Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Tue, 17 May 2022 14:02:50 +0200 Subject: [PATCH] clearly-defined: Port from Jackson to kotlinx.serialization Note that Retrofit calls `toString()` to convert enums used as part of @GET paths to strings. In particular, Retrofit does not use the underlying serializer's string representation. Also see [1]. [1]: https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter/issues/39 Signed-off-by: Sebastian Schuberth --- clients/clearly-defined/build.gradle.kts | 26 +++- .../kotlin/ClearlyDefinedServiceFunTest.kt | 14 +- .../src/main/kotlin/ClearlyDefinedService.kt | 32 ++-- .../src/main/kotlin/Curation.kt | 53 ++++--- .../src/main/kotlin/Definition.kt | 31 ++-- .../clearly-defined/src/main/kotlin/Enums.kt | 141 ++++++++++-------- .../src/main/kotlin/Serializers.kt | 63 ++++++++ .../test/kotlin/ClearlyDefinedServiceTest.kt | 57 +++++++ .../storages/ClearlyDefinedStorageTest.kt | 18 ++- 9 files changed, 303 insertions(+), 132 deletions(-) create mode 100644 clients/clearly-defined/src/main/kotlin/Serializers.kt create mode 100644 clients/clearly-defined/src/test/kotlin/ClearlyDefinedServiceTest.kt diff --git a/clients/clearly-defined/build.gradle.kts b/clients/clearly-defined/build.gradle.kts index 5caba7f3b4da4..42bcb08ceecb7 100644 --- a/clients/clearly-defined/build.gradle.kts +++ b/clients/clearly-defined/build.gradle.kts @@ -1,6 +1,7 @@ /* * Copyright (C) 2017-2019 HERE Europe B.V. * Copyright (C) 2019 Bosch Software Innovations GmbH + * 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. @@ -18,18 +19,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..7d42ab8ab889e 100644 --- a/clients/clearly-defined/src/funTest/kotlin/ClearlyDefinedServiceFunTest.kt +++ b/clients/clearly-defined/src/funTest/kotlin/ClearlyDefinedServiceFunTest.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2019 Bosch Software Innovations GmbH + * 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. @@ -19,8 +20,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 +32,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 +43,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 +112,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..8799803799bda 100644 --- a/clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt +++ b/clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2019 Bosch Software Innovations GmbH + * 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. @@ -19,17 +20,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 +46,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 +62,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 +99,7 @@ interface ClearlyDefinedService { /** * The return type for https://api.clearlydefined.io/api-docs/#/definitions/post_definitions. */ - @JsonInclude(JsonInclude.Include.NON_NULL) + @Serializable data class Defined( val coordinates: Coordinates, val described: Described, @@ -105,17 +107,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 +128,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 +137,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 +166,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 +175,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..15476213ae7b0 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 @@ -126,21 +129,15 @@ data class Coordinates( */ 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 + constructor(value: String) : this(value.split('/', limit = 5)) + + private constructor(parts: List) : this( + type = ComponentType.fromString(parts[0]), + provider = Provider.fromString(parts[1]), + namespace = parts[2].takeUnless { it == "-" }, + name = parts[3], + revision = parts.getOrNull(4) + ) + 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..5e6cc4cf52b98 100644 --- a/clients/clearly-defined/src/main/kotlin/Enums.kt +++ b/clients/clearly-defined/src/main/kotlin/Enums.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Bosch.IO GmbH + * Copyright (C) 2020-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. @@ -19,102 +19,121 @@ package org.ossreviewtoolkit.clients.clearlydefined -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonValue +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.encodeToJsonElement +import kotlinx.serialization.json.jsonPrimitive /** * See https://github.com/clearlydefined/service/blob/48f2c97/schemas/definition-1.0.json#L32-L48. */ -enum class ComponentType(val value: String) { - NPM("npm"), - CRATE("crate"), - GIT("git"), - MAVEN("maven"), - COMPOSER("composer"), - NUGET("nuget"), - GEM("gem"), - GO("go"), - POD("pod"), - PYPI("pypi"), - SOURCE_ARCHIVE("sourcearchive"), - DEBIAN("deb"), - DEBIAN_SOURCES("debsrc"); +@Serializable +enum class ComponentType { + @SerialName("npm") + NPM, + @SerialName("crate") + CRATE, + @SerialName("git") + GIT, + @SerialName("maven") + MAVEN, + @SerialName("composer") + COMPOSER, + @SerialName("nuget") + NUGET, + @SerialName("gem") + GEM, + @SerialName("go") + GO, + @SerialName("pod") + POD, + @SerialName("pypi") + PYPI, + @SerialName("sourcearchive") + SOURCE_ARCHIVE, + @SerialName("deb") + DEBIAN, + @SerialName("debsrc") + DEBIAN_SOURCES; companion object { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JvmStatic - fun fromString(value: String) = - enumValues().single { value.equals(it.value, ignoreCase = true) } + fun fromString(value: String) = enumValues().single { it.toString() == value } } - @JsonValue - override fun toString() = value + // Align the string representation with the serial name to make Retrofit's GET request work. Also see: + // https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter/issues/39 + override fun toString() = ClearlyDefinedService.JSON.encodeToJsonElement(this).jsonPrimitive.content } /** * See https://github.com/clearlydefined/service/blob/48f2c97/schemas/definition-1.0.json#L49-L65. */ -enum class Provider(val value: String) { - NPM_JS("npmjs"), - COCOAPODS("cocoapods"), - CRATES_IO("cratesio"), - GITHUB("github"), - GITLAB("gitlab"), - PACKAGIST("packagist"), - GOLANG("golang"), - MAVEN_CENTRAL("mavencentral"), - MAVEN_GOOGLE("mavengoogle"), - NUGET("nuget"), - RUBYGEMS("rubygems"), - PYPI("pypi"), - DEBIAN("debian"); +@Serializable +enum class Provider { + @SerialName("npmjs") + NPM_JS, + @SerialName("cocoapods") + COCOAPODS, + @SerialName("cratesio") + CRATES_IO, + @SerialName("github") + GITHUB, + @SerialName("gitlab") + GITLAB, + @SerialName("packagist") + PACKAGIST, + @SerialName("golang") + GOLANG, + @SerialName("mavencentral") + MAVEN_CENTRAL, + @SerialName("mavengoogle") + MAVEN_GOOGLE, + @SerialName("nuget") + NUGET, + @SerialName("rubygems") + RUBYGEMS, + @SerialName("pypi") + PYPI, + @SerialName("debian") + DEBIAN; companion object { - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) @JvmStatic - fun fromString(value: String) = enumValues().single { value.equals(it.value, ignoreCase = true) } + fun fromString(value: String) = enumValues().single { it.toString() == value } } - @JsonValue - override fun toString() = value + // Align the string representation with the serial name to make Retrofit's GET request work. Also see: + // https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter/issues/39 + override fun toString() = ClearlyDefinedService.JSON.encodeToJsonElement(this).jsonPrimitive.content } /** * See https://github.com/clearlydefined/service/blob/4917725/schemas/definition-1.0.json#L128. */ +@Serializable enum class Nature { + @SerialName("license") 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() + @SerialName("notice") + NOTICE } /** * See https://github.com/clearlydefined/website/blob/43ec5e3/src/components/ContributePrompt.js#L78-L82. */ +@Serializable enum class ContributionType { + @SerialName("Missing") MISSING, + @SerialName("Incorrect") INCORRECT, + @SerialName("Incomplete") INCOMPLETE, + @SerialName("Ambiguous") AMBIGUOUS, - 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() + @SerialName("Other") + OTHER } /** 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..3cf3560096edc --- /dev/null +++ b/clients/clearly-defined/src/main/kotlin/Serializers.kt @@ -0,0 +1,63 @@ +/* + * 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.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive + +object CoordinatesSerializer : KSerializer by toStringSerializer(::Coordinates) { + override fun deserialize(decoder: Decoder): Coordinates { + require(decoder is JsonDecoder) + return when (val element = decoder.decodeJsonElement()) { + is JsonPrimitive -> Coordinates(element.content) + is JsonObject -> Coordinates( + ComponentType.fromString(element.getValue("type").jsonPrimitive.content), + Provider.fromString(element.getValue("provider").jsonPrimitive.content), + element["namespace"]?.jsonPrimitive?.content, + element.getValue("name").jsonPrimitive.content, + element["revision"]?.jsonPrimitive?.content + ) + else -> throw IllegalArgumentException("Unsupported JSON element $element.") + } + } +} + +object FileSerializer : KSerializer by toStringSerializer(::File) + +object URISerializer : KSerializer by toStringSerializer(::URI) + +inline fun toStringSerializer(noinline create: (String) -> T): ToStringSerializer = + ToStringSerializer(T::class.java.name, create) + +open class ToStringSerializer(serialName: String, private val create: (String) -> T) : KSerializer { + override val descriptor = PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: T) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder) = create(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..40045fbb9cfd3 --- /dev/null +++ b/clients/clearly-defined/src/test/kotlin/ClearlyDefinedServiceTest.kt @@ -0,0 +1,57 @@ +/* + * 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.core.spec.style.WordSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe + +import java.io.File +import java.net.URI + +import kotlinx.serialization.encodeToString + +class ClearlyDefinedServiceTest : WordSpec({ + "Serialization to a string representation" should { + "work for File" { + ClearlyDefinedService.JSON.encodeToString( + FileEntry(path = File("dummy")) + ) shouldBe """{"path":"dummy"}""" + } + + "work for URI" { + ClearlyDefinedService.JSON.encodeToString( + URLs(registry = URI("https://example.com")) + ) shouldBe """{"registry":"https://example.com"}""" + } + + "work for ComponentType" { + enumValues().forAll { + ClearlyDefinedService.JSON.encodeToString(it) shouldBe "\"$it\"" + } + } + + "work for Provider" { + enumValues().forAll { + ClearlyDefinedService.JSON.encodeToString(it) shouldBe "\"$it\"" + } + } + } +}) diff --git a/scanner/src/test/kotlin/storages/ClearlyDefinedStorageTest.kt b/scanner/src/test/kotlin/storages/ClearlyDefinedStorageTest.kt index 59ca56b42baa9..f44b506dfec92 100644 --- a/scanner/src/test/kotlin/storages/ClearlyDefinedStorageTest.kt +++ b/scanner/src/test/kotlin/storages/ClearlyDefinedStorageTest.kt @@ -46,6 +46,9 @@ import io.kotest.matchers.string.shouldContain import java.io.File import java.net.ServerSocket +import kotlinx.serialization.encodeToString + +import org.ossreviewtoolkit.clients.clearlydefined.ClearlyDefinedService import org.ossreviewtoolkit.clients.clearlydefined.ComponentType import org.ossreviewtoolkit.clients.clearlydefined.Coordinates import org.ossreviewtoolkit.clients.clearlydefined.Provider @@ -57,7 +60,6 @@ import org.ossreviewtoolkit.model.ScanResult import org.ossreviewtoolkit.model.VcsInfo import org.ossreviewtoolkit.model.VcsType import org.ossreviewtoolkit.model.config.ClearlyDefinedStorageConfiguration -import org.ossreviewtoolkit.model.jsonMapper import org.ossreviewtoolkit.scanner.ScannerCriteria private const val PACKAGE_TYPE = "Maven" @@ -164,7 +166,7 @@ private fun stubHarvestToolResponse(server: WireMockServer, coordinates: Coordin */ private fun stubDefinitions(server: WireMockServer, coordinates: Coordinates = COORDINATES) { val coordinatesList = listOf(coordinates) - val expectedBody = jsonMapper.writeValueAsString(coordinatesList) + val expectedBody = ClearlyDefinedService.JSON.encodeToString(coordinatesList) server.stubFor( post(urlPathEqualTo("/definitions")) .withRequestBody(equalToJson(expectedBody)) @@ -372,7 +374,7 @@ class ClearlyDefinedStorageTest : WordSpec({ } } - "return a failure if a harvest tool request returns an unexpected result" { + "return an empty result if a harvest tool request returns an unexpected result" { server.stubFor( get(anyUrl()) .willReturn( @@ -380,13 +382,12 @@ class ClearlyDefinedStorageTest : WordSpec({ .withBody("This is not a JSON response") ) ) - val storage = ClearlyDefinedStorage(storageConfiguration(server)) val result = storage.read(TEST_IDENTIFIER) - result.shouldBeFailure { - it.message shouldContain "JsonParseException" + result.shouldBeSuccess { + it should beEmpty() } } @@ -400,10 +401,11 @@ class ClearlyDefinedStorageTest : WordSpec({ .withBody("{ \"unexpected\": true }") ) ) - val storage = ClearlyDefinedStorage(storageConfiguration(server)) - storage.read(TEST_IDENTIFIER).shouldBeSuccess { + val result = storage.read(TEST_IDENTIFIER) + + result.shouldBeSuccess { it should beEmpty() } }