Skip to content

Commit

Permalink
clearly-defined: Port from Jackson to kotlinx.serialization
Browse files Browse the repository at this point in the history
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]: JakeWharton/retrofit2-kotlinx-serialization-converter#39

Signed-off-by: Sebastian Schuberth <sebastian.schuberth@bosch.io>
sschuberth authored and porsche-rbieniek committed Jun 27, 2022
1 parent 3cf5a3c commit 2ea13c8
Showing 9 changed files with 303 additions and 132 deletions.
26 changes: 23 additions & 3 deletions clients/clearly-defined/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<KotlinCompile> {
val customCompilerArgs = listOf(
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
)

kotlinOptions {
freeCompilerArgs = freeCompilerArgs + customCompilerArgs
}
}
Original file line number Diff line number Diff line change
@@ -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<Curation>(curationWithEmptyFacetArrays)
val curation = File("src/funTest/assets/gson.json").inputStream().use {
ClearlyDefinedService.JSON.decodeFromStream<Curation>(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")
}
32 changes: 18 additions & 14 deletions clients/clearly-defined/src/main/kotlin/ClearlyDefinedService.kt
Original file line number Diff line number Diff line change
@@ -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,25 +99,24 @@ 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,
val licensed: Licensed,
val files: List<FileEntry>? = 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<Patch>
@@ -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,
53 changes: 25 additions & 28 deletions clients/clearly-defined/src/main/kotlin/Curation.kt
Original file line number Diff line number Diff line change
@@ -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<Coordinates, Curation>,
val contributions: List<JsonNode>
val contributions: List<JsonElement>
)

/**
* 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<String>? = null,
val dev: List<String>? = null,
@@ -69,15 +71,15 @@ 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
)

/**
* 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<String, Curation>
@@ -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<String>) : 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("/")
}
31 changes: 19 additions & 12 deletions clients/clearly-defined/src/main/kotlin/Definition.kt
Original file line number Diff line number Diff line change
@@ -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<String>? = 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<String>? = null,
val unknown: Int? = null
141 changes: 80 additions & 61 deletions clients/clearly-defined/src/main/kotlin/Enums.kt
Original file line number Diff line number Diff line change
@@ -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<ComponentType>().single { value.equals(it.value, ignoreCase = true) }
fun fromString(value: String) = enumValues<ComponentType>().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<Provider>().single { value.equals(it.value, ignoreCase = true) }
fun fromString(value: String) = enumValues<Provider>().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<Nature>().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<ContributionType>().single { value.equals(it.name, ignoreCase = true) }
}

@JsonValue
override fun toString() = name.titlecase()
@SerialName("Other")
OTHER
}

/**
63 changes: 63 additions & 0 deletions clients/clearly-defined/src/main/kotlin/Serializers.kt
Original file line number Diff line number Diff line change
@@ -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<Coordinates> 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<File> by toStringSerializer(::File)

object URISerializer : KSerializer<URI> by toStringSerializer(::URI)

inline fun <reified T : Any> toStringSerializer(noinline create: (String) -> T): ToStringSerializer<T> =
ToStringSerializer(T::class.java.name, create)

open class ToStringSerializer<T : Any>(serialName: String, private val create: (String) -> T) : KSerializer<T> {
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())
}
Original file line number Diff line number Diff line change
@@ -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<ComponentType>().forAll {
ClearlyDefinedService.JSON.encodeToString(it) shouldBe "\"$it\""
}
}

"work for Provider" {
enumValues<Provider>().forAll {
ClearlyDefinedService.JSON.encodeToString(it) shouldBe "\"$it\""
}
}
}
})
18 changes: 10 additions & 8 deletions scanner/src/test/kotlin/storages/ClearlyDefinedStorageTest.kt
Original file line number Diff line number Diff line change
@@ -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,21 +374,20 @@ 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(
aResponse().withStatus(200)
.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()
}
}

0 comments on commit 2ea13c8

Please sign in to comment.