From 98134aa22f701501326f0cf1d3e8447224df51e8 Mon Sep 17 00:00:00 2001 From: Anton Ekblad Date: Thu, 23 Jun 2022 19:15:49 +0200 Subject: [PATCH 1/2] Support automatic decoding of enum values. --- .../cc/ekblad/toml/transcoding/TomlDecoder.kt | 22 +++++++- .../toml/transcoder/BuiltinDecoderTests.kt | 55 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/cc/ekblad/toml/transcoding/TomlDecoder.kt b/src/main/kotlin/cc/ekblad/toml/transcoding/TomlDecoder.kt index 8e2619b..de7ebec 100644 --- a/src/main/kotlin/cc/ekblad/toml/transcoding/TomlDecoder.kt +++ b/src/main/kotlin/cc/ekblad/toml/transcoding/TomlDecoder.kt @@ -14,6 +14,7 @@ import kotlin.reflect.KProperty1 import kotlin.reflect.KType import kotlin.reflect.KVisibility import kotlin.reflect.full.createType +import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.memberProperties import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.isAccessible @@ -75,10 +76,29 @@ fun TomlDecoder.decode(value: TomlValue, target: KType): T { return when (value) { is TomlValue.List -> toList(value, target) is TomlValue.Map -> toObject(value, target) - else -> throw TomlException.DecodingError("no decoder registered for value/target pair", value, target) + is TomlValue.String -> toEnum(value, target) + else -> throw noDecoder(value, target) } } +private fun noDecoder(value: TomlValue, target: KType) = + TomlException.DecodingError("no decoder registered for value/target pair", value, target) + +fun toEnum(value: TomlValue.String, target: KType): T { + val kClass = requireKClass(target.classifier) + if (!kClass.isSubclassOf(Enum::class)) { + throw noDecoder(value, target) + } + val enumValues = kClass.java.enumConstants as Array> + val enumValue = enumValues.singleOrNull { it.name == value.value } + ?: throw TomlException.DecodingError( + "${value.value} is not a constructor of enum class ${kClass.simpleName}", + value, + target + ) + return enumValue as T +} + private val anyKType: KType = Any::class.createType() private val stringKType: KType = String::class.createType() diff --git a/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinDecoderTests.kt b/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinDecoderTests.kt index d419b90..c0e323e 100644 --- a/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinDecoderTests.kt +++ b/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinDecoderTests.kt @@ -36,6 +36,10 @@ class BuiltinDecoderTests : StringTest { val dad: Person?, ) + enum class PublicEnum { Foo, Bar } + private enum class PrivateEnum { Foo, Bar } + private enum class EnumWithArgs(someValue: String) { Foo("hello"), Bar("goodbye") } + @Test fun `throws on decode to invalid type`() { listOf( @@ -65,6 +69,57 @@ class BuiltinDecoderTests : StringTest { assertContains(error.message, error.reason!!) } + @Test + fun `can decode strings to enums`() { + assertEquals(PublicEnum.Foo, mapper.decode(TomlValue.String("Foo"))) + assertEquals(PublicEnum.Bar, mapper.decode(TomlValue.String("Bar"))) + assertEquals(PrivateEnum.Foo, mapper.decode(TomlValue.String("Foo"))) + assertEquals(PrivateEnum.Bar, mapper.decode(TomlValue.String("Bar"))) + assertEquals(EnumWithArgs.Foo, mapper.decode(TomlValue.String("Foo"))) + assertEquals(EnumWithArgs.Bar, mapper.decode(TomlValue.String("Bar"))) + } + + @Test + fun `decoding string to nonexistent enum constructor throws`() { + val error = assertFailsWith { + mapper.decode(TomlValue.String("Not Foo")) + } + assertNotNull(error.reason) + assertNull(error.cause) + assertContains(error.reason!!, "not a constructor of enum class") + assertEquals(TomlValue.String("Not Foo"), error.sourceValue) + assertEquals(typeOf(), error.targetType) + } + + @Test + fun `enum decoding is case sensitive`() { + val error = assertFailsWith { + mapper.decode(TomlValue.String("foo")) + } + } + + @Test + fun `enum decoding compares the entire strings`() { + assertFailsWith { + mapper.decode(TomlValue.String("Fooo")) + } + assertFailsWith { + mapper.decode(TomlValue.String("oFoo")) + } + assertFailsWith { + mapper.decode(TomlValue.String("oFooo")) + } + assertFailsWith { + mapper.decode(TomlValue.String("oo")) + } + assertFailsWith { + mapper.decode(TomlValue.String("Fo")) + } + assertFailsWith { + mapper.decode(TomlValue.String("o")) + } + } + @Test fun `can decode objects to data classes`() { val expected = Person( From 0b0b65b446f757b668ebc6a74de14a586b617436 Mon Sep 17 00:00:00 2001 From: Anton Ekblad Date: Thu, 23 Jun 2022 19:21:26 +0200 Subject: [PATCH 2/2] Support automatic encoding of enum values. --- .../cc/ekblad/toml/transcoding/TomlEncoder.kt | 2 ++ .../ekblad/toml/transcoder/BuiltinEncoderTests.kt | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/kotlin/cc/ekblad/toml/transcoding/TomlEncoder.kt b/src/main/kotlin/cc/ekblad/toml/transcoding/TomlEncoder.kt index defe139..c9b7a68 100644 --- a/src/main/kotlin/cc/ekblad/toml/transcoding/TomlEncoder.kt +++ b/src/main/kotlin/cc/ekblad/toml/transcoding/TomlEncoder.kt @@ -7,6 +7,7 @@ import cc.ekblad.toml.util.TomlName import kotlin.reflect.KClass import kotlin.reflect.KProperty1 import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.isAccessible @@ -59,6 +60,7 @@ fun TomlEncoder.encode(value: Any): TomlValue { value is Map<*, *> -> fromMap(value) value is Iterable<*> -> TomlValue.List(value.mapNotNull { it?.let(::encode) }) value::class.isData -> fromDataClass(value) + value::class.isSubclassOf(Enum::class) -> TomlValue.String((value as Enum<*>).name) else -> throw TomlException.EncodingError(value, null) } } diff --git a/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinEncoderTests.kt b/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinEncoderTests.kt index e99cc21..85dd70c 100644 --- a/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinEncoderTests.kt +++ b/src/test/kotlin/cc/ekblad/toml/transcoder/BuiltinEncoderTests.kt @@ -21,6 +21,19 @@ import kotlin.test.assertNull class BuiltinEncoderTests : UnitTest { private val mapper = tomlMapper { } + enum class PublicEnum { Foo, Bar } + private enum class PrivateEnum { Foo, Bar } + private enum class EnumWithArgs(val number: Int) { Foo(12), Bar(32) } + + @Test + fun `can encode enum values`() { + assertEncodesTo(PublicEnum.Foo, TomlValue.String("Foo")) + assertEncodesTo(PublicEnum.Bar, TomlValue.String("Bar")) + assertEncodesTo(PrivateEnum.Foo, TomlValue.String("Foo")) + assertEncodesTo(PrivateEnum.Bar, TomlValue.String("Bar")) + assertEncodesTo(EnumWithArgs.Foo, TomlValue.String("Foo")) + assertEncodesTo(EnumWithArgs.Bar, TomlValue.String("Bar")) + } @Test fun `can encode integers`() {