Skip to content

Commit

Permalink
feat: Update Kotlin SDK to use Gson instead of Jackson (#836)
Browse files Browse the repository at this point in the history
* Updated Kotlin SDK to use Gson for serialization/deserialization.
* Added type adapter for AuthToken to handle constructor calls.
* Added smoke test to check deserialization ignores unknown properties.
  • Loading branch information
mistryrakesh authored Sep 26, 2021
1 parent 283cbd5 commit 4d1f789
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 152 deletions.
2 changes: 1 addition & 1 deletion kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dependencies {
implementation "io.ktor:ktor-client:$ktorVersion"
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
implementation "io.ktor:ktor-client-json:$ktorVersion"
implementation "io.ktor:ktor-client-jackson:$ktorVersion"
implementation "io.ktor:ktor-client-gson:$ktorVersion"

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC'
implementation 'com.google.code.gson:gson:2.8.5'
Expand Down
72 changes: 67 additions & 5 deletions kotlin/src/main/com/looker/rtl/AuthToken.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,28 @@

package com.looker.rtl

import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.TypeAdapter
import com.google.gson.annotations.SerializedName
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import com.looker.sdk.AccessToken
import io.ktor.client.features.json.GsonSerializer
import io.ktor.client.features.json.defaultSerializer
import java.lang.reflect.Type
import java.time.LocalDateTime

data class AuthToken(
@JsonProperty("access_token")
@SerializedName("access_token")
var accessToken: String = "", // TODO: Consider making this/these vals and using new objects instead of mutating
@JsonProperty("token_type")
@SerializedName("token_type")
var tokenType: String = "",
@JsonProperty("expires_in")
@SerializedName("expires_in")
var expiresIn: Long = 0L,
@JsonProperty("refresh_token")
@SerializedName("refresh_token")
var refreshToken: String? = null
) {

Expand Down Expand Up @@ -80,3 +90,55 @@ data class AuthToken(
expiresIn = 0
}
}

/**
* Adapter for serialization/deserialization of [AuthToken].
* This is required since Gson used no-args constructor to create objects. Gson's default
* deserializer first creates an [AuthToken] object with default values which results with incorrect
* value being assigned to [AuthToken.expiresAt] in its init block.
*
* This adapter mitigates this by calling the constructor with deserialized values.
*/
class AuthTokenAdapter: TypeAdapter<AuthToken>() {
override fun read(jsonReader: JsonReader?): AuthToken {
val authToken = AuthToken()
jsonReader?.beginObject()

while (jsonReader?.hasNext() == true) {
if (jsonReader.peek().equals(JsonToken.NAME)) {
when (jsonReader.nextName()) {
"access_token" -> authToken.accessToken = jsonReader.nextString()
"token_type" -> authToken.tokenType = jsonReader.nextString()
"expires_in" -> authToken.expiresIn = jsonReader.nextLong()
"refresh_token" -> {
if (jsonReader.peek().equals(JsonToken.NULL)) {
authToken.refreshToken = null
jsonReader.nextNull()
} else {
authToken.refreshToken = jsonReader.nextString()
}
}
else -> break
}
}
}

jsonReader?.endObject()

// return new AuthToken calling its constructor with deserialized values
return AuthToken(authToken.accessToken, authToken.tokenType, authToken.expiresIn, authToken.refreshToken)
}

override fun write(jsonWriter: JsonWriter?, authToken: AuthToken?) {
jsonWriter?.beginObject()
jsonWriter?.name("access_token")
jsonWriter?.value(authToken?.accessToken)
jsonWriter?.name("token_type")
jsonWriter?.value(authToken?.tokenType)
jsonWriter?.name("expires_in")
jsonWriter?.value(authToken?.expiresIn)
jsonWriter?.name("refresh_token")
jsonWriter?.value(authToken?.refreshToken)
jsonWriter?.endObject()
}
}
17 changes: 10 additions & 7 deletions kotlin/src/main/com/looker/rtl/Transport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@

package com.looker.rtl

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import io.ktor.client.HttpClient
import io.ktor.client.call.receive
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.json.JacksonSerializer
import io.ktor.client.features.json.GsonSerializer
import io.ktor.client.features.json.defaultSerializer
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.FormDataContent
Expand Down Expand Up @@ -179,7 +181,9 @@ fun customClient(options: TransportOptions): HttpClient {
// This construction loosely adapted from https://ktor.io/clients/http-client/engines.html#artifact-7
return HttpClient(OkHttp) {
install(JsonFeature) {
serializer = JacksonSerializer()
serializer = GsonSerializer {
registerTypeAdapter(AuthToken::class.java, AuthTokenAdapter())
}
}
engine {
config {
Expand Down Expand Up @@ -336,12 +340,11 @@ class Transport(val options: TransportOptions) {
}
else -> {
// Request body
// val gson = GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssX").create()
val gson = Gson()
val jsonBody = gson.toJson(body)
val json = defaultSerializer()
val jsonBody = json.write(body)

builder.body = jsonBody
headers["Content-Length"] = jsonBody.length.toString()
headers["Content-Length"] = jsonBody.contentLength.toString()
}
}
}
Expand Down
Loading

0 comments on commit 4d1f789

Please sign in to comment.