Skip to content

Commit

Permalink
Add support for deserialization Duration in HOCON format
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Mihailov committed Oct 24, 2022
1 parent a7cee0b commit 430084b
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 12 deletions.
11 changes: 10 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,16 @@ subprojects {
afterEvaluate { // Can be applied only when the project is evaluated
animalsniffer {
sourceSets = [sourceSets.main]
annotation = (name == "kotlinx-serialization-core")? "kotlinx.serialization.internal.SuppressAnimalSniffer" : "kotlinx.serialization.json.internal.SuppressAnimalSniffer"
def annotationValue = "kotlinx.serialization.json.internal.SuppressAnimalSniffer"
switch (name) {
case "kotlinx-serialization-core":
annotationValue = "kotlinx.serialization.internal.SuppressAnimalSniffer"
break
case "kotlinx-serialization-hocon":
annotationValue = "kotlinx.serialization.hocon.internal.SuppressAnimalSniffer"
break
}
annotation = annotationValue
}
dependencies {
signature 'net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature'
Expand Down
37 changes: 26 additions & 11 deletions formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
package kotlinx.serialization.hocon

import com.typesafe.config.*
import kotlin.time.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
import kotlinx.serialization.hocon.internal.SuppressAnimalSniffer
import kotlinx.serialization.internal.*
import kotlinx.serialization.modules.*

Expand Down Expand Up @@ -128,6 +131,13 @@ public sealed class Hocon(
private fun composeName(parentName: String, childName: String) =
if (parentName.isEmpty()) childName else "$parentName.$childName"

@SuppressAnimalSniffer
private fun Config.decodeDurationInHoconFormat(path: String) : Duration = try {
getDuration(path).toKotlinDuration()
} catch (e: ConfigException) {
throw SerializationException(e)
}

override fun SerialDescriptor.getTag(index: Int): String =
composeName(currentTagOrNull.orEmpty(), getConventionElementName(index, useConfigNamingConvention))

Expand All @@ -138,19 +148,24 @@ public sealed class Hocon(
}

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
if (deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism) {
return deserializer.deserialize(this)
}

val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf
return when {
deserializer == Duration.serializer() -> {
@Suppress("UNCHECKED_CAST")
conf.decodeDurationInHoconFormat(currentTag) as T
}
deserializer !is AbstractPolymorphicSerializer<*> || useArrayPolymorphism -> deserializer.deserialize(this)
else -> {
val config = if (currentTagOrNull != null) conf.getConfig(currentTag) else conf

val reader = ConfigReader(config)
val type = reader.decodeTaggedString(classDiscriminator)
val actualSerializer = deserializer.findPolymorphicSerializerOrNull(reader, type)
?: throw SerializerNotFoundException(type)
val reader = ConfigReader(config)
val type = reader.decodeTaggedString(classDiscriminator)
val actualSerializer = deserializer.findPolymorphicSerializerOrNull(reader, type)
?: throw SerializerNotFoundException(type)

@Suppress("UNCHECKED_CAST")
return (actualSerializer as DeserializationStrategy<T>).deserialize(reader)
@Suppress("UNCHECKED_CAST")
(actualSerializer as DeserializationStrategy<T>).deserialize(reader)
}
}
}

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kotlinx.serialization.hocon.internal

/**
* Suppresses Animal Sniffer plugin errors for certain methods.
* Such methods include references to Java 8 methods that are not
* available in Android API, but can be desugared by R8.
*/
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
internal annotation class SuppressAnimalSniffer
1 change: 1 addition & 0 deletions formats/hocon/src/mainModule/kotlin/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module kotlinx.serialization.hocon {
requires transitive kotlin.stdlib;
requires transitive kotlinx.serialization.core;
requires transitive kotlin.stdlib.jdk8;
requires transitive typesafe.config;

exports kotlinx.serialization.hocon;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package kotlinx.serialization.hocon

import kotlin.time.*
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlinx.serialization.*
import org.junit.Assert.*
import org.junit.Test

class HoconDurationDeserializerTest {

@Serializable
data class Simple(val d: Duration)

@Serializable
data class Nullable(val d: Duration?)

@Serializable
data class Complex(val i: Int, val s: Simple, val n: Nullable, val l: List<Simple>, val ln: List<Nullable>, val f: Boolean)

@Test
fun testDeserializeDurationInHoconFormat() {
var obj = deserializeConfig("d = 10s", Simple.serializer())
assertEquals(10.seconds, obj.d)
obj = deserializeConfig("d = 10 hours", Simple.serializer())
assertEquals(10.hours, obj.d)
obj = deserializeConfig("d = 5 ms", Simple.serializer())
assertEquals(5.milliseconds, obj.d)
}

@Test
fun testDeserializeNullableDurationInHoconFormat() {
var obj = deserializeConfig("d = null", Nullable.serializer())
assertNull(obj.d)

obj = deserializeConfig("d = 5 days", Nullable.serializer())
assertEquals(5.days, obj.d!!)
}

@Test
fun testDeserializeComplexDurationInHoconFormat() {
val obj = deserializeConfig("""
i = 6
s: { d = 5m }
n: { d = null }
l: [ { d = 1m }, { d = 2s } ]
ln: [ { d = null }, { d = 6h } ]
f = true
""".trimIndent(), Complex.serializer())
assertEquals(5.minutes, obj.s.d)
assertNull(obj.n.d)
assertEquals(2, obj.l.size)
assertEquals(1.minutes, obj.l[0].d)
assertEquals(2.seconds, obj.l[1].d)
assertEquals(2, obj.ln.size)
assertNull(obj.ln[0].d)
assertEquals(6.hours, obj.ln[1].d!!)
assertEquals(6, obj.i)
assertTrue(obj.f)
}

@Test
fun testThrowsWhenNotTimeUnitHocon() {
assertThrows(SerializationException::class.java) {
deserializeConfig("d = 10 unknown", Simple.serializer())
}
}
}

0 comments on commit 430084b

Please sign in to comment.