Skip to content

Commit

Permalink
Merge pull request #350 from sanao1006/migrate-from-gson-to-ktxSerial…
Browse files Browse the repository at this point in the history
…ization

refactor: migrate from Gson to KotlinX.Serialization
  • Loading branch information
takahirom authored May 10, 2024
2 parents d6f06f3 + db1ccbf commit 9878a70
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 157 deletions.
2 changes: 0 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ kim = "0.17.7"

dropbox-differ = "0.0.2"
google-android-material = "1.5.0"
gson = "2.10.1"
junit = "4.13.2"
ktor-serialization-kotlinx-xml = "2.3.0"
kotlinx-serialization = "1.6.3"
Expand Down Expand Up @@ -80,7 +79,6 @@ compose-ui-graphics-desktop = { module = "org.jetbrains.compose.ui:ui-graphics-d
compose-ui-test-junit4-desktop = { module = "org.jetbrains.compose.ui:ui-test-junit4-desktop", version.ref = "composeMultiplatform" }
dropbox-differ = { module = "com.dropbox.differ:differ", version.ref = "dropbox-differ" }
google-android-material = { module = "com.google.android.material:material", version.ref = "google-android-material" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson"}
junit = { module = "junit:junit", version.ref = "junit" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
ktor-serialization-kotlinx-xml = { module = "io.ktor:ktor-serialization-kotlinx-xml", version.ref = "ktor-serialization-kotlinx-xml" }
Expand Down
7 changes: 4 additions & 3 deletions include-build/roborazzi-core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "com.android.library"
id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin
}
if (System.getenv("INTEGRATION_TEST") != "true") {
pluginManager.apply("com.vanniktech.maven.publish")
Expand Down Expand Up @@ -55,7 +56,7 @@ kotlin {
commonJvmMain {
dependencies {
api libs.dropbox.differ
compileOnly libs.gson
compileOnly libs.kotlinx.serialization.json
implementation libs.junit
}
}
Expand All @@ -68,7 +69,7 @@ kotlin {
}
jvmMain {
dependencies {
implementation libs.gson
implementation libs.kotlinx.serialization.json
}
}
jvmTest {
Expand All @@ -82,7 +83,7 @@ kotlin {
compileOnly libs.androidx.compose.ui.test.junit4
api libs.androidx.test.espresso.core
implementation libs.androidx.core.ktx
implementation libs.gson
implementation libs.kotlinx.serialization.json
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
package com.github.takahirom.roborazzi

import com.github.takahirom.roborazzi.CaptureResults.Companion.gson
import com.google.gson.annotations.JsonAdapter
import com.google.gson.annotations.SerializedName
import com.github.takahirom.roborazzi.CaptureResults.Companion.json
import kotlinx.serialization.Contextual
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.io.File
import java.io.FileReader

@JsonAdapter(CaptureResult.JsonAdapter::class)
@Serializable(with = CaptureResult.CaptureResultSerializer::class)
sealed interface CaptureResult {
val type: String
val timestampNs: Long
val compareFile: File?
val actualFile: File?
val goldenFile: File?
val contextData: Map<String, Any>
val contextData: Map<String,@Contextual Any>

val reportFile: File
get() = when (val result = this) {
Expand All @@ -23,59 +34,62 @@ sealed interface CaptureResult {
is Unchanged -> result.goldenFile
}

@Serializable
data class Recorded(
@SerializedName("golden_file_path")
override val goldenFile: File,
@SerializedName("timestamp")
@SerialName("golden_file_path")
override val goldenFile:@Contextual File,
@SerialName("timestamp")
override val timestampNs: Long,
@SerializedName("context_data")
override val contextData: Map<String, Any>
@SerialName("context_data")
override val contextData: Map<String,@Contextual Any>
) : CaptureResult {

override val type = "recorded"
override val actualFile: File?
get() = null
override val compareFile: File?
get() = null
}

@Serializable
data class Added(
@SerializedName("compare_file_path")
override val compareFile: File,
@SerializedName("actual_file_path")
override val actualFile: File,
@SerializedName("golden_file_path")
override val goldenFile: File,
@SerializedName("timestamp")
@SerialName("compare_file_path")
override val compareFile:@Contextual File,
@SerialName("actual_file_path")
override val actualFile:@Contextual File,
@SerialName("golden_file_path")
override val goldenFile:@Contextual File,
@SerialName("timestamp")
override val timestampNs: Long,
@SerializedName("context_data")
override val contextData: Map<String, Any>
@SerialName("context_data")
override val contextData: Map<String,@Contextual Any>
) : CaptureResult {
override val type = "added"
}

@Serializable
data class Changed(
@SerializedName("compare_file_path")
override val compareFile: File,
@SerializedName("golden_file_path")
override val goldenFile: File,
@SerializedName("actual_file_path")
override val actualFile: File,
@SerializedName("timestamp")
@SerialName("compare_file_path")
override val compareFile:@Contextual File,
@SerialName("golden_file_path")
override val goldenFile:@Contextual File,
@SerialName("actual_file_path")
override val actualFile:@Contextual File,
@SerialName("timestamp")
override val timestampNs: Long,
@SerializedName("context_data")
override val contextData: Map<String, Any>
@SerialName("context_data")
override val contextData: Map<String,@Contextual Any>
) : CaptureResult {
override val type = "changed"
}

@Serializable
data class Unchanged(
@SerializedName("golden_file_path")
override val goldenFile: File,
@SerializedName("timestamp")
@SerialName("golden_file_path")
override val goldenFile:@Contextual File,
@SerialName("timestamp")
override val timestampNs: Long,
@SerializedName("context_data")
override val contextData: Map<String, Any>
@SerialName("context_data")
override val contextData: Map<String,@Contextual Any>
) : CaptureResult {
override val type = "unchanged"
override val actualFile: File?
Expand All @@ -86,38 +100,32 @@ sealed interface CaptureResult {

companion object {
fun fromJsonFile(filePath: String): CaptureResult {
return gson.fromJson(FileReader(filePath), CaptureResult::class.java)
val jsonElement = json.parseToJsonElement(FileReader(filePath).readText())
return json.decodeFromJsonElement<CaptureResult>(jsonElement)
}
}

object JsonAdapter : com.google.gson.JsonSerializer<CaptureResult>,
com.google.gson.JsonDeserializer<CaptureResult> {
override fun serialize(
src: CaptureResult,
typeOfSrc: java.lang.reflect.Type,
context: com.google.gson.JsonSerializationContext
): com.google.gson.JsonElement {
val jsonElement = when (src) {
is Recorded -> context.serialize(src, Recorded::class.java)
is Changed -> context.serialize(src, Changed::class.java)
is Unchanged -> context.serialize(src, Unchanged::class.java)
is Added -> context.serialize(src, Added::class.java)
object CaptureResultSerializer : KSerializer<CaptureResult> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("CaptureResult", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: CaptureResult) =
when (value) {
is Recorded -> encoder.encodeSerializableValue(Recorded.serializer(), value)
is Changed -> encoder.encodeSerializableValue(Changed.serializer(), value)
is Unchanged -> encoder.encodeSerializableValue(Unchanged.serializer(), value)
is Added -> encoder.encodeSerializableValue(Added.serializer(), value)
}
return jsonElement
}

override fun deserialize(
json: com.google.gson.JsonElement,
typeOfT: java.lang.reflect.Type,
context: com.google.gson.JsonDeserializationContext
): CaptureResult? {
val type = requireNotNull(json.asJsonObject.get("type")?.asString)
override fun deserialize(decoder: Decoder): CaptureResult {
require(decoder is JsonDecoder)
val type = decoder.decodeJsonElement().jsonObject["type"]!!.jsonPrimitive.content
return when (type) {
"recorded" -> context.deserialize(json, Recorded::class.java)
"changed" -> context.deserialize(json, Changed::class.java)
"unchanged" -> context.deserialize(json, Unchanged::class.java)
"added" -> context.deserialize(json, Added::class.java)
else -> throw IllegalArgumentException("Unknown type $type")
"recorded" -> decoder.decodeSerializableValue(Recorded.serializer())
"changed" -> decoder.decodeSerializableValue(Changed.serializer())
"unchanged" -> decoder.decodeSerializableValue(Unchanged.serializer())
"added" -> decoder.decodeSerializableValue(Added.serializer())
else -> throw IllegalArgumentException("Unknown type $type")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
package com.github.takahirom.roborazzi

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.ContextualSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.double
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.modules.SerializersModule
import java.io.File
import java.io.FileReader
import java.lang.reflect.Type

@Serializable
data class CaptureResults(
@SerializedName("summary")
@SerialName("summary")
val resultSummary: ResultSummary,
@SerializedName("results")
@SerialName("results")
val captureResults: List<CaptureResult>
) {

fun toJson(): String {
return gson.toJson(this)
return json.encodeToString(this)
}

class Tab(
Expand Down Expand Up @@ -150,36 +160,38 @@ data class CaptureResults(
}

companion object {
val gson: Gson = GsonBuilder()
.registerTypeAdapter(File::class.java, object : JsonSerializer<File>, JsonDeserializer<File> {
override fun serialize(
src: File?,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
val absolutePath = src?.absolutePath ?: return JsonNull.INSTANCE
return JsonPrimitive(absolutePath)
}
val json = Json {
isLenient = true
encodeDefaults = true
ignoreUnknownKeys = true
classDiscriminator = "#class"
serializersModule = SerializersModule {
contextual(File::class,
object : KSerializer<File> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("FileSerializer", PrimitiveKind.STRING)

override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): File {
val path = json?.asString ?: throw JsonParseException("File path is null")
return File(path)
}
})
.create()
override fun serialize(encoder: Encoder, value: File) {
encoder.encodeString(value.absolutePath)
}

override fun deserialize(decoder: Decoder): File {
val path = decoder.decodeString()
return File(path)
}
}
)
contextual(Any::class, AnySerializer)
}
}

fun fromJsonFile(inputPath: String): CaptureResults {
val jsonObject = JsonParser.parseString(FileReader(inputPath).readText()).asJsonObject
return fromJson(jsonObject)
val jsonElement = json.parseToJsonElement(File(inputPath).readText())
return json.decodeFromJsonElement(jsonElement)
}

fun fromJson(jsonObject: JsonObject): CaptureResults {
// Auto convert using Gson
return gson.fromJson(jsonObject, CaptureResults::class.java)
fun fromJson(jsonString: JsonObject): CaptureResults {
return json.decodeFromJsonElement(jsonString)
}

fun from(results: List<CaptureResult>): CaptureResults {
Expand All @@ -196,3 +208,34 @@ data class CaptureResults(
}
}
}

object AnySerializer : KSerializer<Any> {
@OptIn(ExperimentalSerializationApi::class)
override val descriptor: SerialDescriptor
get() = ContextualSerializer(Any::class, null, emptyArray()).descriptor

private val delegateSerializer = JsonPrimitive.serializer()

override fun serialize(encoder: Encoder, value: Any) {
when (value) {
is String -> encoder.encodeString(value)
is Int -> encoder.encodeInt(value)
is Long -> encoder.encodeLong(value)
is Double -> encoder.encodeDouble(value.toDouble())
is Boolean -> encoder.encodeBoolean(value)
else -> throw IllegalArgumentException("Unknown type: ${value::class.qualifiedName}")
}
}

override fun deserialize(decoder: Decoder): Any {
val input = decoder.decodeSerializableValue(delegateSerializer)
return when {
input.isString -> input.content
input.booleanOrNull != null -> input.boolean
input.intOrNull != null -> input.int
input.longOrNull != null -> input.long
input.doubleOrNull != null -> input.double
else -> throw IllegalArgumentException("Unknown type: ${input::class.qualifiedName}")
}
}
}
Loading

0 comments on commit 9878a70

Please sign in to comment.