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>
  • Loading branch information
sschuberth authored and porsche-rbieniek committed Jun 27, 2022
1 parent c305ec7 commit 01479ef
Show file tree
Hide file tree
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.
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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")
}
Expand Down
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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand All @@ -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,

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
53 changes: 25 additions & 28 deletions clients/clearly-defined/src/main/kotlin/Curation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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>
Expand All @@ -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
Expand Down Expand Up @@ -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("/")
}
Loading

0 comments on commit 01479ef

Please sign in to comment.