Skip to content

Commit 2950815

Browse files
authored
Add support for component data (de)serialization (#231)
1 parent 93d9225 commit 2950815

File tree

43 files changed

+1341
-481
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1341
-481
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,11 @@
464464
<artifactId>jackson-databind</artifactId>
465465
<version>2.17.2</version>
466466
</dependency>
467+
<dependency>
468+
<groupId>com.fasterxml.jackson.module</groupId>
469+
<artifactId>jackson-module-kotlin</artifactId>
470+
<version>2.17.2</version>
471+
</dependency>
467472
<dependency>
468473
<groupId>club.minnced</groupId>
469474
<artifactId>jda-ktx</artifactId>
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
package io.github.freya022.botcommands.api.components.annotations
22

33
import io.github.freya022.botcommands.api.components.builder.IPersistentActionableComponent
4+
import io.github.freya022.botcommands.api.components.serialization.annotations.SerializableComponentData
5+
import io.github.freya022.botcommands.api.parameters.ParameterResolver
6+
import io.github.freya022.botcommands.api.parameters.resolvers.ComponentParameterResolver
47

58
/**
69
* Sets this parameter as data coming from [IPersistentActionableComponent.bindTo].
710
*
811
* The order and types of the passed data must match with the handler parameters.
912
*
13+
* ### Requirements
14+
* A compatible [ComponentParameterResolver] must exist for the annotated parameter,
15+
* the default supported types can be seen in [ParameterResolver].
16+
*
17+
* If your parameter is a serializable object,
18+
* you can instead use [@SerializableComponentData][SerializableComponentData].
19+
*
1020
* @see JDAButtonListener @JDAButtonListener
1121
* @see JDASelectMenuListener @JDASelectMenuListener
1222
*/
13-
@Target(AnnotationTarget.VALUE_PARAMETER)
23+
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.ANNOTATION_CLASS)
1424
@Retention(AnnotationRetention.RUNTIME)
1525
annotation class ComponentData
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
package io.github.freya022.botcommands.api.components.annotations
22

33
import io.github.freya022.botcommands.api.components.builder.IPersistentTimeoutableComponent
4+
import io.github.freya022.botcommands.api.components.serialization.annotations.SerializableTimeoutData
5+
import io.github.freya022.botcommands.api.parameters.ParameterResolver
6+
import io.github.freya022.botcommands.api.parameters.resolvers.TimeoutParameterResolver
47

58
/**
69
* Sets this parameter as data coming from [IPersistentTimeoutableComponent.timeout].
710
*
811
* The order and types of the passed data must match with the handler parameters.
912
*
13+
* ### Requirements
14+
* A compatible [TimeoutParameterResolver] must exist for the annotated parameter,
15+
* the default supported types can be seen in [ParameterResolver].
16+
*
17+
* If your parameter is a serializable object,
18+
* you can instead use [@SerializableTimeoutData][SerializableTimeoutData].
19+
*
1020
* @see ComponentTimeoutHandler @ComponentTimeoutHandler
1121
* @see GroupTimeoutHandler @GroupTimeoutHandler
1222
*/
13-
@Target(AnnotationTarget.VALUE_PARAMETER)
23+
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.ANNOTATION_CLASS)
1424
@Retention(AnnotationRetention.RUNTIME)
1525
annotation class TimeoutData

src/main/kotlin/io/github/freya022/botcommands/api/components/builder/IActionableComponent.kt

Lines changed: 327 additions & 182 deletions
Large diffs are not rendered by default.

src/main/kotlin/io/github/freya022/botcommands/api/components/builder/ITimeoutableComponent.kt

Lines changed: 340 additions & 198 deletions
Large diffs are not rendered by default.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.github.freya022.botcommands.api.components.serialization
2+
3+
import io.github.freya022.botcommands.api.components.serialization.annotations.SerializableComponentData
4+
import io.github.freya022.botcommands.api.components.serialization.annotations.SerializableTimeoutData
5+
import io.github.freya022.botcommands.api.core.reflect.ParameterWrapper
6+
import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService
7+
8+
/**
9+
* Serializes and deserializes data from parameters annotated with [@SerializableComponentData][SerializableComponentData]
10+
* and [@SerializableTimeoutData][SerializableTimeoutData].
11+
*
12+
* ### Default implementation
13+
* By default, a Jackson-based serializer with the Kotlin module is used.
14+
*
15+
* ### Overriding the default instance
16+
* You can override the default instance by creating a service implementing this interface,
17+
* in which you can use any serialization library you want.
18+
*
19+
* **Tip:** You will generally need to get the type of the to-be-deserialized parameter,
20+
* which you can get from the [ParameterWrapper].
21+
*
22+
* Here's an example with [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization):
23+
*
24+
* ```kt
25+
* @BService
26+
* class KotlinxComponentDataSerializer : GlobalComponentDataSerializer {
27+
*
28+
* // Default instance, you can customize it later
29+
* private val json = Json
30+
*
31+
* override fun deserialize(parameter: ParameterWrapper, data: SerializedComponentData): Any {
32+
* return json.decodeFromString(serializer(parameter.type), data.asString())!!
33+
* }
34+
*
35+
* override fun serialize(parameter: ParameterWrapper, obj: Any): SerializedComponentData {
36+
* val json = json.encodeToString(serializer(parameter.type), obj)
37+
* return SerializedComponentData.fromString(json)
38+
* }
39+
* }
40+
* ```
41+
*/
42+
@InterfacedService(acceptMultiple = false)
43+
interface GlobalComponentDataSerializer {
44+
45+
/**
46+
* Serializes the given object into a [SerializedComponentData].
47+
*
48+
* @param parameter The parameter which this value is serialized for
49+
* @param obj The data to be serialized
50+
*/
51+
fun serialize(parameter: ParameterWrapper, obj: Any): SerializedComponentData
52+
53+
/**
54+
* Deserializes the [data] into an object compatible with the [parameter].
55+
*
56+
* @param parameter The parameter which this value is deserialized for
57+
* @param data The data to be deserialized into a compatible object
58+
*/
59+
fun deserialize(parameter: ParameterWrapper, data: SerializedComponentData): Any
60+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package io.github.freya022.botcommands.api.components.serialization
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.fasterxml.jackson.module.kotlin.readValue
5+
import io.github.freya022.botcommands.api.components.serialization.SerializedComponentData.Companion.fromBytes
6+
import io.github.freya022.botcommands.api.components.serialization.SerializedComponentData.Companion.fromString
7+
import io.github.freya022.botcommands.api.core.reflect.KotlinTypeToken
8+
import io.github.freya022.botcommands.api.parameters.resolvers.ComponentParameterResolver
9+
10+
/**
11+
* Contains the serialized data of a component argument.
12+
*
13+
* @see fromString
14+
* @see fromBytes
15+
*
16+
* @see ComponentParameterResolver.serialize
17+
* @see GlobalComponentDataSerializer.serialize
18+
* @see GlobalComponentDataSerializer.deserialize
19+
*/
20+
class SerializedComponentData private constructor(
21+
private val bytes: ByteArray,
22+
) {
23+
24+
/**
25+
* Returns the underlying byte array.
26+
*/
27+
fun asBytes(): ByteArray = bytes.clone()
28+
29+
/**
30+
* Decodes the data as a UTF-8 string.
31+
*/
32+
fun asString() = bytes.decodeToString()
33+
34+
override fun equals(other: Any?): Boolean {
35+
if (this === other) return true
36+
if (javaClass != other?.javaClass) return false
37+
38+
other as SerializedComponentData
39+
40+
return bytes.contentEquals(other.bytes)
41+
}
42+
43+
override fun hashCode(): Int {
44+
return bytes.contentHashCode()
45+
}
46+
47+
override fun toString(): String {
48+
return "SerializedComponentData(bytes=${bytes.contentToString()})"
49+
}
50+
51+
companion object {
52+
53+
/**
54+
* Creates a [SerializedComponentData] from the given bytes.
55+
*/
56+
@JvmStatic
57+
fun fromBytes(bytes: ByteArray): SerializedComponentData {
58+
return SerializedComponentData(bytes.clone())
59+
}
60+
61+
/**
62+
* Creates a [SerializedComponentData] from the given string.
63+
*/
64+
@JvmStatic
65+
fun fromString(string: String): SerializedComponentData {
66+
return SerializedComponentData(string.encodeToByteArray())
67+
}
68+
}
69+
}
70+
71+
/**
72+
* Serializes the given [value] as a [SerializedComponentData] encoded as a UTF-8 string.
73+
*/
74+
fun ObjectMapper.writeValueAsComponentData(value: Any): SerializedComponentData =
75+
fromBytes(writeValueAsBytes(value))
76+
77+
/**
78+
* Deserializes the given UTF-8 encoded JSON object [data] as a [T] instance.
79+
*/
80+
inline fun <reified T : Any> ObjectMapper.readValue(data: SerializedComponentData): T =
81+
readValue(data.asBytes())
82+
83+
/**
84+
* Deserializes the given UTF-8 encoded JSON object [data] as a [T] instance.
85+
*/
86+
fun <T : Any> ObjectMapper.readValue(data: SerializedComponentData, typeToken: KotlinTypeToken<T>): T =
87+
readValue(data.asBytes(), constructType(typeToken.javaType))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.freya022.botcommands.api.components.serialization.annotations
2+
3+
import io.github.freya022.botcommands.api.components.annotations.ComponentData
4+
import io.github.freya022.botcommands.api.components.serialization.GlobalComponentDataSerializer
5+
6+
/**
7+
* Same as [@ComponentData][ComponentData],
8+
* but also generates a resolver which (de)serializes the value using the [GlobalComponentDataSerializer].
9+
*/
10+
@ComponentData
11+
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.ANNOTATION_CLASS)
12+
annotation class SerializableComponentData
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.freya022.botcommands.api.components.serialization.annotations
2+
3+
import io.github.freya022.botcommands.api.components.annotations.TimeoutData
4+
import io.github.freya022.botcommands.api.components.serialization.GlobalComponentDataSerializer
5+
6+
/**
7+
* Same as [@TimeoutData][TimeoutData],
8+
* but also generates a resolver which (de)serializes the value using the [GlobalComponentDataSerializer].
9+
*/
10+
@TimeoutData
11+
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.ANNOTATION_CLASS)
12+
annotation class SerializableTimeoutData
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.freya022.botcommands.api.components.serialization.exceptions
2+
3+
import io.github.freya022.botcommands.api.parameters.resolvers.ComponentParameterResolver
4+
import io.github.freya022.botcommands.api.parameters.resolvers.TimeoutParameterResolver
5+
6+
/**
7+
* An exception thrown when [ComponentParameterResolver.serialize] or [TimeoutParameterResolver.serialize] fails.
8+
*/
9+
class ComponentSerializationException : RuntimeException {
10+
11+
constructor(message: String) : super(message)
12+
constructor(message: String, cause: Throwable) : super(message, cause)
13+
}

0 commit comments

Comments
 (0)