Skip to content

Commit dd73756

Browse files
committed
Switch from Gson to Moshi
The issue I am having with Gson is that it relies on some unsafe thing that is not available in all cases. Since Moshi is by the same folks that wrote the HTTP and REST clients we are using, it seemed like the best replacement. Thankfully, for the most part this was a simple search and replace, but calling toJson and fromJson is a bit more burdensome as you have to call `adapter` and include the type, and it has no built-in converter for UUID. On the flip side, the converters require slightly less code.
1 parent be860cc commit dd73756

27 files changed

+332
-438
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ dependencies {
2525
implementation("com.squareup.retrofit2:retrofit:2.9.0")
2626
// define a BOM and its version
2727
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0"))
28-
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
28+
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
2929
implementation("com.squareup.okhttp3:okhttp")
3030
implementation("com.squareup.okhttp3:logging-interceptor")
3131

src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import com.coder.gateway.util.getHeaders
1616
import com.coder.gateway.util.getOS
1717
import com.coder.gateway.util.safeHost
1818
import com.coder.gateway.util.sha1
19-
import com.google.gson.Gson
20-
import com.google.gson.JsonSyntaxException
2119
import com.intellij.openapi.diagnostic.Logger
2220
import com.intellij.openapi.progress.ProgressIndicator
21+
import com.squareup.moshi.Json
22+
import com.squareup.moshi.Moshi
2323
import org.zeroturnaround.exec.ProcessExecutor
24+
import java.io.EOFException
2425
import java.io.FileInputStream
2526
import java.io.FileNotFoundException
2627
import java.net.ConnectException
@@ -328,7 +329,7 @@ class CoderCLIManager(
328329
* Version output from the CLI's version command.
329330
*/
330331
private data class Version(
331-
val version: String,
332+
@Json(name = "version") val version: String,
332333
)
333334

334335
/**
@@ -338,11 +339,15 @@ class CoderCLIManager(
338339
*/
339340
fun version(): SemVer {
340341
val raw = exec("version", "--output", "json")
341-
val json = Gson().fromJson(raw, Version::class.java)
342-
if (json?.version == null) {
342+
try {
343+
val json = Moshi.Builder().build().adapter(Version::class.java).fromJson(raw)
344+
if (json?.version == null) {
345+
throw MissingVersionException("No version found in output")
346+
}
347+
return SemVer.parse(json.version)
348+
} catch (exception: EOFException) {
343349
throw MissingVersionException("No version found in output")
344350
}
345-
return SemVer.parse(json.version)
346351
}
347352

348353
/**
@@ -353,7 +358,6 @@ class CoderCLIManager(
353358
version()
354359
} catch (e: Exception) {
355360
when (e) {
356-
is JsonSyntaxException,
357361
is InvalidVersionException -> {
358362
logger.info("Got invalid version from $localBinaryPath: ${e.message}")
359363
}

src/main/kotlin/com/coder/gateway/sdk/BaseCoderRestClient.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.coder.gateway.icons.toRetinaAwareIcon
55
import com.coder.gateway.sdk.convertors.ArchConverter
66
import com.coder.gateway.sdk.convertors.InstantConverter
77
import com.coder.gateway.sdk.convertors.OSConverter
8+
import com.coder.gateway.sdk.convertors.UUIDConverter
89
import com.coder.gateway.sdk.ex.AuthenticationResponseException
910
import com.coder.gateway.sdk.ex.TemplateResponseException
1011
import com.coder.gateway.sdk.ex.WorkspaceResponseException
@@ -19,28 +20,24 @@ import com.coder.gateway.sdk.v2.models.WorkspaceResource
1920
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
2021
import com.coder.gateway.services.CoderSettingsState
2122
import com.coder.gateway.settings.CoderSettings
22-
import com.coder.gateway.util.Arch
2323
import com.coder.gateway.util.CoderHostnameVerifier
24-
import com.coder.gateway.util.OS
2524
import com.coder.gateway.util.coderSocketFactory
2625
import com.coder.gateway.util.coderTrustManagers
2726
import com.coder.gateway.util.getHeaders
2827
import com.coder.gateway.util.toURL
2928
import com.coder.gateway.util.withPath
30-
import com.google.gson.Gson
31-
import com.google.gson.GsonBuilder
3229
import com.intellij.openapi.util.SystemInfo
3330
import com.intellij.util.ImageLoader
3431
import com.intellij.util.ui.ImageUtil
32+
import com.squareup.moshi.Moshi
3533
import okhttp3.Credentials
3634
import okhttp3.OkHttpClient
3735
import okhttp3.logging.HttpLoggingInterceptor
3836
import org.imgscalr.Scalr
3937
import retrofit2.Retrofit
40-
import retrofit2.converter.gson.GsonConverterFactory
38+
import retrofit2.converter.moshi.MoshiConverterFactory
4139
import java.net.HttpURLConnection
4240
import java.net.URL
43-
import java.time.Instant
4441
import java.util.UUID
4542
import javax.net.ssl.X509TrustManager
4643
import javax.swing.Icon
@@ -61,11 +58,12 @@ open class BaseCoderRestClient(
6158
lateinit var buildVersion: String
6259

6360
init {
64-
val gson: Gson = GsonBuilder()
65-
.registerTypeAdapter(Instant::class.java, InstantConverter())
66-
.registerTypeAdapter(Arch::class.java, ArchConverter())
67-
.registerTypeAdapter(OS::class.java, OSConverter())
68-
.setPrettyPrinting().create()
61+
val moshi = Moshi.Builder()
62+
.add(ArchConverter())
63+
.add(InstantConverter())
64+
.add(OSConverter())
65+
.add(UUIDConverter())
66+
.build()
6967

7068
val socketFactory = coderSocketFactory(settings.tls)
7169
val trustManagers = coderTrustManagers(settings.tls.caPath)
@@ -104,7 +102,7 @@ open class BaseCoderRestClient(
104102
.build()
105103

106104
retroRestClient = Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
107-
.addConverterFactory(GsonConverterFactory.create(gson))
105+
.addConverterFactory(MoshiConverterFactory.create(moshi))
108106
.build().create(CoderV2RestFacade::class.java)
109107
}
110108

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
11
package com.coder.gateway.sdk.convertors
22

33
import com.coder.gateway.util.Arch
4-
import com.google.gson.JsonDeserializationContext
5-
import com.google.gson.JsonDeserializer
6-
import com.google.gson.JsonElement
7-
import com.google.gson.JsonParseException
8-
import com.google.gson.JsonPrimitive
9-
import com.google.gson.JsonSerializationContext
10-
import com.google.gson.JsonSerializer
11-
import java.lang.reflect.Type
4+
import com.squareup.moshi.FromJson
5+
import com.squareup.moshi.ToJson
126

137
/**
14-
* GSON serialiser/deserialiser for converting [Arch] objects.
8+
* Serializer/deserializer for converting [Arch] objects.
159
*/
16-
class ArchConverter : JsonSerializer<Arch?>, JsonDeserializer<Arch?> {
17-
override fun serialize(src: Arch?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
18-
return JsonPrimitive(src?.toString() ?: "")
19-
}
20-
21-
@Throws(JsonParseException::class)
22-
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Arch? {
23-
return Arch.from(json.asString)
24-
}
10+
class ArchConverter {
11+
@ToJson fun toJson(src: Arch?): String = src?.toString() ?: ""
12+
@FromJson fun fromJson(src: String): Arch? = Arch.from(src)
2513
}
Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,20 @@
11
package com.coder.gateway.sdk.convertors
22

3-
import com.google.gson.JsonDeserializationContext
4-
import com.google.gson.JsonDeserializer
5-
import com.google.gson.JsonElement
6-
import com.google.gson.JsonParseException
7-
import com.google.gson.JsonPrimitive
8-
import com.google.gson.JsonSerializationContext
9-
import com.google.gson.JsonSerializer
10-
import java.lang.reflect.Type
3+
import com.squareup.moshi.FromJson
4+
import com.squareup.moshi.ToJson
115
import java.time.Instant
126
import java.time.format.DateTimeFormatter
137
import java.time.temporal.TemporalAccessor
148

159
/**
16-
* GSON serialiser/deserialiser for converting [Instant] objects.
10+
* Serializer/deserializer for converting [Instant] objects.
1711
*/
18-
class InstantConverter : JsonSerializer<Instant?>, JsonDeserializer<Instant?> {
19-
/**
20-
* Gson invokes this call-back method during serialization when it encounters a field of the
21-
* specified type.
22-
*
23-
*
24-
*
25-
* In the implementation of this call-back method, you should consider invoking
26-
* [JsonSerializationContext.serialize] method to create JsonElements for any
27-
* non-trivial field of the `src` object. However, you should never invoke it on the
28-
* `src` object itself since that will cause an infinite loop (Gson will call your
29-
* call-back method again).
30-
*
31-
* @param src the object that needs to be converted to Json.
32-
* @param typeOfSrc the actual type (fully genericized version) of the source object.
33-
* @return a JsonElement corresponding to the specified object.
34-
*/
35-
override fun serialize(src: Instant?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
36-
return JsonPrimitive(FORMATTER.format(src))
37-
}
38-
39-
/**
40-
* Gson invokes this call-back method during deserialization when it encounters a field of the
41-
* specified type.
42-
*
43-
*
44-
*
45-
* In the implementation of this call-back method, you should consider invoking
46-
* [JsonDeserializationContext.deserialize] method to create objects
47-
* for any non-trivial field of the returned object. However, you should never invoke it on the
48-
* the same type passing `json` since that will cause an infinite loop (Gson will call your
49-
* call-back method again).
50-
*
51-
* @param json The Json data being deserialized
52-
* @param typeOfT The type of the Object to deserialize to
53-
* @return a deserialized object of the specified type typeOfT which is a subclass of `T`
54-
* @throws JsonParseException if json is not in the expected format of `typeOfT`
55-
*/
56-
@Throws(JsonParseException::class)
57-
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Instant {
58-
return FORMATTER.parse(json.asString) { temporal: TemporalAccessor? -> Instant.from(temporal) }
59-
}
12+
class InstantConverter {
13+
@ToJson fun toJson(src: Instant?): String = FORMATTER.format(src)
14+
@FromJson fun fromJson(src: String): Instant? = FORMATTER.parse(src) {
15+
temporal: TemporalAccessor? -> Instant.from(temporal) }
6016

6117
companion object {
62-
/** Formatter. */
6318
private val FORMATTER = DateTimeFormatter.ISO_INSTANT
6419
}
65-
}
20+
}
Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
11
package com.coder.gateway.sdk.convertors
22

33
import com.coder.gateway.util.OS
4-
import com.google.gson.JsonDeserializationContext
5-
import com.google.gson.JsonDeserializer
6-
import com.google.gson.JsonElement
7-
import com.google.gson.JsonParseException
8-
import com.google.gson.JsonPrimitive
9-
import com.google.gson.JsonSerializationContext
10-
import com.google.gson.JsonSerializer
11-
import java.lang.reflect.Type
4+
import com.squareup.moshi.FromJson
5+
import com.squareup.moshi.ToJson
126

137
/**
14-
* GSON serialiser/deserialiser for converting [OS] objects.
8+
* Serializer/deserializer for converting [OS] objects.
159
*/
16-
class OSConverter : JsonSerializer<OS?>, JsonDeserializer<OS?> {
17-
override fun serialize(src: OS?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
18-
return JsonPrimitive(src?.toString() ?: "")
19-
}
20-
21-
@Throws(JsonParseException::class)
22-
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): OS? {
23-
return OS.from(json.asString)
24-
}
10+
class OSConverter {
11+
@ToJson fun toJson(src: OS?): String = src?.toString() ?: ""
12+
@FromJson fun fromJson(src: String): OS? = OS.from(src)
2513
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.coder.gateway.sdk.convertors
2+
3+
import com.squareup.moshi.FromJson
4+
import com.squareup.moshi.ToJson
5+
import java.util.UUID
6+
7+
/**
8+
* Serializer/deserializer for converting [UUID] objects.
9+
*/
10+
class UUIDConverter {
11+
@ToJson fun toJson(src: UUID): String = src.toString()
12+
@FromJson fun fromJson(src: String): UUID = UUID.fromString(src)
13+
}

src/main/kotlin/com/coder/gateway/sdk/v2/models/BuildInfo.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.coder.gateway.sdk.v2.models
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.squareup.moshi.Json
44

55
/**
66
* Contains build information for a Coder instance.
@@ -12,6 +12,6 @@ import com.google.gson.annotations.SerializedName
1212
* @param version the semantic version of the build.
1313
*/
1414
data class BuildInfo(
15-
@SerializedName("external_url") val externalUrl: String,
16-
@SerializedName("version") val version: String
15+
@Json(name = "external_url") val externalUrl: String,
16+
@Json(name = "version") val version: String
1717
)
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
package com.coder.gateway.sdk.v2.models
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.squareup.moshi.Json
44

55
enum class BuildReason {
66
// "initiator" is used when a workspace build is triggered by a user.
77
// Combined with the initiator id/username, it indicates which user initiated the build.
8-
@SerializedName("initiator")
9-
INITIATOR,
10-
8+
@Json(name = "initiator") INITIATOR,
119
// "autostart" is used when a build to start a workspace is triggered by Autostart.
1210
// The initiator id/username in this case is the workspace owner and can be ignored.
13-
@SerializedName("autostart")
14-
AUTOSTART,
15-
11+
@Json(name = "autostart") AUTOSTART,
1612
// "autostop" is used when a build to stop a workspace is triggered by Autostop.
1713
// The initiator id/username in this case is the workspace owner and can be ignored.
18-
@SerializedName("autostop")
19-
AUTOSTOP
20-
}
14+
@Json(name = "autostop") AUTOSTOP
15+
}
Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,23 @@
11
package com.coder.gateway.sdk.v2.models
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.squareup.moshi.Json
44
import java.util.UUID
55

66
data class CreateParameterRequest(
7-
@SerializedName("copy_from_parameter") val cloneID: UUID?,
8-
@SerializedName("name") val name: String,
9-
@SerializedName("source_value") val sourceValue: String,
10-
@SerializedName("source_scheme") val sourceScheme: ParameterSourceScheme,
11-
@SerializedName("destination_scheme") val destinationScheme: ParameterDestinationScheme
7+
@Json(name = "copy_from_parameter") val cloneID: UUID?,
8+
@Json(name = "name") val name: String,
9+
@Json(name = "source_value") val sourceValue: String,
10+
@Json(name = "source_scheme") val sourceScheme: ParameterSourceScheme,
11+
@Json(name = "destination_scheme") val destinationScheme: ParameterDestinationScheme
1212
)
1313

1414
enum class ParameterSourceScheme {
15-
@SerializedName("none")
16-
NONE,
17-
18-
@SerializedName("data")
19-
DATA
15+
@Json(name = "none") NONE,
16+
@Json(name = "data") DATA
2017
}
2118

2219
enum class ParameterDestinationScheme {
23-
@SerializedName("none")
24-
NONE,
25-
26-
@SerializedName("environment_variable")
27-
ENVIRONMENT_VARIABLE,
28-
29-
@SerializedName("provisioner_variable")
30-
PROVISIONER_VARIABLE
31-
}
20+
@Json(name = "none") NONE,
21+
@Json(name = "environment_variable") ENVIRONMENT_VARIABLE,
22+
@Json(name = "provisioner_variable") PROVISIONER_VARIABLE
23+
}

src/main/kotlin/com/coder/gateway/sdk/v2/models/CreateWorkspaceBuildRequest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package com.coder.gateway.sdk.v2.models
22

3-
import com.google.gson.annotations.SerializedName
3+
import com.squareup.moshi.Json
44
import java.util.UUID
55

66
data class CreateWorkspaceBuildRequest(
7-
@SerializedName("template_version_id") val templateVersionID: UUID?,
8-
@SerializedName("transition") val transition: WorkspaceTransition,
9-
@SerializedName("dry_run") val dryRun: Boolean?,
10-
@SerializedName("state") val provisionerState: Array<Byte>?,
7+
@Json(name = "template_version_id") val templateVersionID: UUID?,
8+
@Json(name = "transition") val transition: WorkspaceTransition,
9+
@Json(name = "dry_run") val dryRun: Boolean?,
10+
@Json(name = "state") val provisionerState: Array<Byte>?,
1111
// Orphan may be set for the Destroy transition.
12-
@SerializedName("orphan") val orphan: Boolean?,
13-
@SerializedName("parameter_values") val parameterValues: Array<CreateParameterRequest>?
12+
@Json(name = "orphan") val orphan: Boolean?,
13+
@Json(name = "parameter_values") val parameterValues: Array<CreateParameterRequest>?
1414
) {
1515
override fun equals(other: Any?): Boolean {
1616
if (this === other) return true

0 commit comments

Comments
 (0)