Skip to content

Commit

Permalink
Add @bencode and @DefaultValue annotations support for Enums
Browse files Browse the repository at this point in the history
  • Loading branch information
a.kolomeytsev authored and tonykolomeytsev committed Dec 19, 2021
1 parent c426801 commit 8ecbcf0
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 6 deletions.
12 changes: 11 additions & 1 deletion kben-core/src/main/kotlin/kekmech/kben/annotations/Bencode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -22,6 +22,16 @@ package kekmech.kben.annotations
* val objectCopy =
* kben.fromBencode<User>(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)
Expand Down
24 changes: 24 additions & 0 deletions kben-core/src/main/kotlin/kekmech/kben/annotations/DefaultValue.kt
Original file line number Diff line number Diff line change
@@ -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<SomeOptions>("OPTION_3") // returns SomeOptions.UNKNOWN
* ```
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class DefaultValue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class DeserializationContext(
typeHolder.type.isSubclassOf(Map::class) ->
MapTypeAdapter<T>().fromBencode(bencodeElement, this, typeHolder)
typeHolder.type.isSubclassOf(Enum::class) ->
EnumTypeAdapter<T>().fromBencode(bencodeElement, this, typeHolder)
EnumTypeAdapter().fromBencode(bencodeElement, this, typeHolder)
else ->
AnyTypeAdapter<T>().fromBencode(bencodeElement, this, typeHolder)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SerializationContext(
obj is Map<*, *> ->
MapTypeAdapter<T>().toBencode(obj as Map<String, T>, this)
obj is Enum<*> ->
EnumTypeAdapter<T>().toBencode(obj, this)
EnumTypeAdapter().toBencode(obj, this)
else ->
AnyTypeAdapter<T>().toBencode(obj, this)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
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<T : Any> : TypeAdapter<Enum<*>>() {
internal class EnumTypeAdapter : TypeAdapter<Enum<*>>() {

@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<Enum<*>>
return options.firstOrNull { it.name == serializedOptionName }
return options.firstOrNull { it.annotatedName == serializedOptionName }
?: options.firstOrNull { it.isDefaultOption }
?: throw optionError(
enumClassName = typeHolder.type.qualifiedName!!,
optionName = serializedOptionName,
)
}

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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}
}

0 comments on commit 8ecbcf0

Please sign in to comment.