Add BaseSurrogateSerializer (#120)

This commit is contained in:
Brian Yencho 2023-10-17 09:18:27 -05:00 committed by Álison Fernandes
parent 69a4eef68f
commit 3fddc3d285
3 changed files with 150 additions and 36 deletions

View file

@ -1,10 +1,7 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.KSerializer
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseSurrogateSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
private const val KDF_TYPE_ARGON2_ID = 1
private const val KDF_TYPE_PBKDF2_SHA256 = 0
@ -40,38 +37,37 @@ data class PreLoginResponseJson(
}
}
private class PreLoginResponseSerializer : KSerializer<PreLoginResponseJson> {
private class PreLoginResponseSerializer :
BaseSurrogateSerializer<PreLoginResponseJson, InternalPreLoginResponseJson>() {
private val surrogateSerializer = InternalPreLoginResponseJson.serializer()
override val surrogateSerializer = InternalPreLoginResponseJson.serializer()
override val descriptor: SerialDescriptor = surrogateSerializer.descriptor
override fun InternalPreLoginResponseJson.toExternalType(): PreLoginResponseJson =
PreLoginResponseJson(
kdfParams = when (this.kdfType) {
KDF_TYPE_PBKDF2_SHA256 -> {
PreLoginResponseJson.KdfParams.Pbkdf2(
iterations = this.kdfIterations,
)
}
override fun deserialize(decoder: Decoder): PreLoginResponseJson {
val surrogate = decoder.decodeSerializableValue(surrogateSerializer)
val kdfParams = when (surrogate.kdfType) {
KDF_TYPE_PBKDF2_SHA256 -> {
PreLoginResponseJson.KdfParams.Pbkdf2(
iterations = surrogate.kdfIterations,
KDF_TYPE_ARGON2_ID -> {
@Suppress("UnsafeCallOnNullableType")
PreLoginResponseJson.KdfParams.Argon2ID(
iterations = this.kdfIterations,
memory = this.kdfMemory!!,
parallelism = this.kdfParallelism!!,
)
}
else -> throw IllegalStateException(
"Unable to parse KDF params for unknown kdfType: ${this.kdfType}",
)
}
},
)
KDF_TYPE_ARGON2_ID -> {
PreLoginResponseJson.KdfParams.Argon2ID(
iterations = surrogate.kdfIterations,
memory = surrogate.kdfMemory!!,
parallelism = surrogate.kdfParallelism!!,
)
}
else -> throw IllegalStateException(
"Unable to parse KDF params for unknown kdfType: ${surrogate.kdfType}",
)
}
return PreLoginResponseJson(kdfParams = kdfParams)
}
override fun serialize(encoder: Encoder, value: PreLoginResponseJson) {
val surrogate = when (val params = value.kdfParams) {
override fun PreLoginResponseJson.toSurrogateType(): InternalPreLoginResponseJson =
when (val params = this.kdfParams) {
is PreLoginResponseJson.KdfParams.Argon2ID -> {
InternalPreLoginResponseJson(
kdfType = KDF_TYPE_ARGON2_ID,
@ -88,9 +84,4 @@ private class PreLoginResponseSerializer : KSerializer<PreLoginResponseJson> {
)
}
}
encoder.encodeSerializableValue(
surrogateSerializer,
surrogate,
)
}
}

View file

@ -0,0 +1,52 @@
package com.x8bit.bitwarden.data.platform.datasource.network.serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* A helper class that simplifies the process of providing a "surrogate" [KSerializer]. These are
* used to provide mappings between an "internal" type [R] (the "surrogate") to an external type
* [T].
*
* See the [official surrogate documentation](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#composite-serializer-via-surrogate)
* for details.
*/
abstract class BaseSurrogateSerializer<T, R> : KSerializer<T> {
/**
* The [KSerializer] naturally associated with the type [R], which is a typical class annotated
* with [Serializable].
*/
abstract val surrogateSerializer: KSerializer<R>
/**
* A conversion from the internal/surrogate type [R] to external type [T].
*/
abstract fun R.toExternalType(): T
/**
* A conversion from the external type [T] to the internal/surrogate type [R].
*/
abstract fun T.toSurrogateType(): R
//region KSerializer overrides
override val descriptor: SerialDescriptor
get() = surrogateSerializer.descriptor
final override fun deserialize(decoder: Decoder): T =
decoder
.decodeSerializableValue(surrogateSerializer)
.toExternalType()
final override fun serialize(encoder: Encoder, value: T) {
encoder.encodeSerializableValue(
surrogateSerializer,
value.toSurrogateType(),
)
}
//endregion KSerializer overrides
}

View file

@ -0,0 +1,71 @@
package com.x8bit.bitwarden.data.platform.datasource.network.serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class BaseSurrogateSerializerTest {
private val json = Json
@Test
fun `properly deserializes raw JSON to the external model`() {
assertEquals(
ExternalData(
dataAsInt = 100,
),
json.decodeFromString<ExternalData>(
"""
{
"dataAsString": "100"
}
""",
),
)
}
@Test
fun `properly serializes external model back to raw JSON`() {
assertEquals(
json.parseToJsonElement(
"""
{
"dataAsString": "100"
}
""",
),
json.encodeToJsonElement(
ExternalData(
dataAsInt = 100,
),
),
)
}
}
@Serializable
private data class InternalData(
val dataAsString: String,
)
@Serializable(TestSurrogateSerializer::class)
private data class ExternalData(
val dataAsInt: Int,
)
private class TestSurrogateSerializer : BaseSurrogateSerializer<ExternalData, InternalData>() {
override val surrogateSerializer: KSerializer<InternalData>
get() = InternalData.serializer()
override fun InternalData.toExternalType(): ExternalData =
ExternalData(
dataAsInt = this.dataAsString.toInt(),
)
override fun ExternalData.toSurrogateType(): InternalData =
InternalData(
dataAsString = this.dataAsInt.toString(),
)
}