diff --git a/kben-core/src/main/kotlin/kekmech/kben/annotations/Bencode.kt b/kben-core/src/main/kotlin/kekmech/kben/annotations/Bencode.kt index 6a4e7df..0318a1e 100644 --- a/kben-core/src/main/kotlin/kekmech/kben/annotations/Bencode.kt +++ b/kben-core/src/main/kotlin/kekmech/kben/annotations/Bencode.kt @@ -4,7 +4,7 @@ package kekmech.kben.annotations * An annotation that indicates this member should be serialized to Bencode with * the provided name value as its field name. * - * Example: + * **Example with properties**: * ```kotlin * data class User( * @Bencode("full name") @@ -22,6 +22,16 @@ package kekmech.kben.annotations * val objectCopy = * kben.fromBencode(bencode) * ``` + * + * **Example with enums**: + * ```kotlin + * enum class UserStatus { + * @Bencode("online") ONLINE, + * @Bencode("offline") OFFLINE + * } + * // ... + * kben.toBencode(UserStatus.ONLINE) // '6:online' + * ``` */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.PROPERTY) diff --git a/kben-core/src/main/kotlin/kekmech/kben/annotations/DefaultValue.kt b/kben-core/src/main/kotlin/kekmech/kben/annotations/DefaultValue.kt new file mode 100644 index 0000000..be4cb7f --- /dev/null +++ b/kben-core/src/main/kotlin/kekmech/kben/annotations/DefaultValue.kt @@ -0,0 +1,24 @@ +package kekmech.kben.annotations + +/** + * Annotation pointing to the default enum value. + * + * Add this annotation to one of the enum values so that it is returned in case of an enum deserialization error. + * Annotation affects only the result of deserialization and does not change the result of enum serialization in any way. + * + * Example: + * ```kotlin + * // enum with default value + * enum class SomeOptions { + * OPTION_1, + * OPTION_2, + * + * @DefaultValue UNKNOWN + * } + * // ... + * kben.fromBencode("OPTION_3") // returns SomeOptions.UNKNOWN + * ``` + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class DefaultValue diff --git a/kben-core/src/main/kotlin/kekmech/kben/domain/DeserializationContext.kt b/kben-core/src/main/kotlin/kekmech/kben/domain/DeserializationContext.kt index 85fad47..eda385c 100644 --- a/kben-core/src/main/kotlin/kekmech/kben/domain/DeserializationContext.kt +++ b/kben-core/src/main/kotlin/kekmech/kben/domain/DeserializationContext.kt @@ -32,7 +32,7 @@ class DeserializationContext( typeHolder.type.isSubclassOf(Map::class) -> MapTypeAdapter().fromBencode(bencodeElement, this, typeHolder) typeHolder.type.isSubclassOf(Enum::class) -> - EnumTypeAdapter().fromBencode(bencodeElement, this, typeHolder) + EnumTypeAdapter().fromBencode(bencodeElement, this, typeHolder) else -> AnyTypeAdapter().fromBencode(bencodeElement, this, typeHolder) } diff --git a/kben-core/src/main/kotlin/kekmech/kben/domain/SerializationContext.kt b/kben-core/src/main/kotlin/kekmech/kben/domain/SerializationContext.kt index 24414b4..f111eee 100644 --- a/kben-core/src/main/kotlin/kekmech/kben/domain/SerializationContext.kt +++ b/kben-core/src/main/kotlin/kekmech/kben/domain/SerializationContext.kt @@ -25,7 +25,7 @@ class SerializationContext( obj is Map<*, *> -> MapTypeAdapter().toBencode(obj as Map, this) obj is Enum<*> -> - EnumTypeAdapter().toBencode(obj, this) + EnumTypeAdapter().toBencode(obj, this) else -> AnyTypeAdapter().toBencode(obj, this) } diff --git a/kben-core/src/main/kotlin/kekmech/kben/domain/adapters/EnumTypeAdapter.kt b/kben-core/src/main/kotlin/kekmech/kben/domain/adapters/EnumTypeAdapter.kt index 01e9c29..99f28f0 100644 --- a/kben-core/src/main/kotlin/kekmech/kben/domain/adapters/EnumTypeAdapter.kt +++ b/kben-core/src/main/kotlin/kekmech/kben/domain/adapters/EnumTypeAdapter.kt @@ -1,20 +1,23 @@ package kekmech.kben.domain.adapters import kekmech.kben.TypeHolder +import kekmech.kben.annotations.Bencode +import kekmech.kben.annotations.DefaultValue import kekmech.kben.domain.DeserializationContext import kekmech.kben.domain.SerializationContext import kekmech.kben.domain.TypeAdapter import kekmech.kben.domain.dto.BencodeElement import kekmech.kben.domain.dto.BencodeElement.BencodeByteString -class EnumTypeAdapter : TypeAdapter>() { +internal class EnumTypeAdapter : TypeAdapter>() { @Suppress("UNCHECKED_CAST") override fun fromBencode(value: BencodeElement, context: DeserializationContext, typeHolder: TypeHolder): Enum<*> { typeHolder as TypeHolder.Simple // enum classes cannot have type parameters val serializedOptionName = (value as BencodeByteString).asString val options = typeHolder.type.java.enumConstants as Array> - return options.firstOrNull { it.name == serializedOptionName } + return options.firstOrNull { it.annotatedName == serializedOptionName } + ?: options.firstOrNull { it.isDefaultOption } ?: throw optionError( enumClassName = typeHolder.type.qualifiedName!!, optionName = serializedOptionName, @@ -22,7 +25,13 @@ class EnumTypeAdapter : TypeAdapter>() { } override fun toBencode(value: Enum<*>, context: SerializationContext): BencodeElement = - BencodeByteString(value.name) + BencodeByteString(value.annotatedName) + + private val Enum<*>.annotatedName: String + get() = (javaClass.getField(name).annotations.firstOrNull { it is Bencode } as? Bencode)?.name ?: name + + private val Enum<*>.isDefaultOption: Boolean + get() = javaClass.getField(name).annotations.any { it is DefaultValue } private fun optionError(enumClassName: String, optionName: String): Throwable = IllegalStateException("Option with name '$optionName' not found in enum class $enumClassName") diff --git a/kben-core/src/test/kotlin/kekmech/kben/domain/DeserializationContextTest.kt b/kben-core/src/test/kotlin/kekmech/kben/domain/DeserializationContextTest.kt index aa0cb38..5d81b46 100644 --- a/kben-core/src/test/kotlin/kekmech/kben/domain/DeserializationContextTest.kt +++ b/kben-core/src/test/kotlin/kekmech/kben/domain/DeserializationContextTest.kt @@ -2,6 +2,7 @@ package kekmech.kben.domain import kekmech.kben.TypeHolder import kekmech.kben.annotations.Bencode +import kekmech.kben.annotations.DefaultValue import kekmech.kben.domain.dto.BencodeElement.* import kekmech.kben.mocks.Mocks import org.junit.jupiter.api.Assertions.assertArrayEquals @@ -275,6 +276,38 @@ internal class DeserializationContextTest { ) } + private enum class TestEnum2 { + @Bencode(name = "first option") OPTION_1, + @Bencode(name = "second option") OPTION_2 + } + + @Test + fun `deserialize enum with @Bencode annotated options`() { + assertEquals( + TestEnum2.OPTION_1, + context.fromBencode(BencodeByteString("first option"), TypeHolder.Simple(TestEnum2::class)), + ) + assertEquals( + TestEnum2.OPTION_2, + context.fromBencode(BencodeByteString("second option"), TypeHolder.Simple(TestEnum2::class)), + ) + } + + private enum class TestEnum3 { + OPTION_1, + OPTION_2, + + @DefaultValue UNKNOWN, + } + + @Test + fun `deserialize enum with @DefaultValue annotated option`() { + assertEquals( + TestEnum3.UNKNOWN, + context.fromBencode(BencodeByteString("OPTION_3"), TypeHolder.Simple(TestEnum3::class)), + ) + } + @Test fun `deserialize bencode integer to any`() { val expected: Any = 42L diff --git a/kben-core/src/test/kotlin/kekmech/kben/domain/SerializationContextTest.kt b/kben-core/src/test/kotlin/kekmech/kben/domain/SerializationContextTest.kt index 6b28cc8..e1bf088 100644 --- a/kben-core/src/test/kotlin/kekmech/kben/domain/SerializationContextTest.kt +++ b/kben-core/src/test/kotlin/kekmech/kben/domain/SerializationContextTest.kt @@ -249,4 +249,21 @@ internal class SerializationContextTest { context.toBencode(TestEnum1.OPTION_2) ) } + + private enum class TestEnum2 { + @Bencode(name = "first option") OPTION_1, + @Bencode(name = "second option") OPTION_2 + } + + @Test + fun `serialize enum with @Bencode annotated options`() { + assertEquals( + BencodeByteString("first option"), + context.toBencode(TestEnum2.OPTION_1) + ) + assertEquals( + BencodeByteString("second option"), + context.toBencode(TestEnum2.OPTION_2) + ) + } } \ No newline at end of file