diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dae600..6fc49b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.1.4 +* `Versions` + * `sdi`: `0.4.0-rc2` -> `0.4.1` + * `tgbotapi`: `0.32.7` -> `0.32.8` + * `microutils`: `0.4.25` -> `0.4.26` + ## 0.1.3 * `Versions` diff --git a/bot/build.gradle b/bot/build.gradle index 88536b1..e460e81 100644 --- a/bot/build.gradle +++ b/bot/build.gradle @@ -25,6 +25,9 @@ dependencies { kapt "com.github.matfax.klassindex:processor:$klassindex_version" api "org.xerial:sqlite-jdbc:$sqlite_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-annotations-jvm:$kotlin_version" + api project(":plagubot.plugin") } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt index ae68245..78ee034 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt @@ -2,11 +2,10 @@ package dev.inmo.plagubot import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.plagubot.config.* -import dev.inmo.plagubot.config.configSerialFormat +import dev.inmo.plagubot.config.configJsonFormat import dev.inmo.tgbotapi.bot.Ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviour -import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.longPolling import dev.inmo.tgbotapi.types.botCommandsLimit import kotlinx.coroutines.* import kotlinx.serialization.InternalSerializationApi @@ -42,7 +41,7 @@ suspend inline fun initPlaguBot( suspend fun main(args: Array) { val (configPath) = args val file = File(configPath) - val config = configSerialFormat.decodeFromString(Config.serializer(), file.readText()) + val config = configJsonFormat.decodeFromString(ConfigSerializer, file.readText()) val scope = CoroutineScope(Dispatchers.Default) initPlaguBot(config, scope).join() diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt index c3f8fd5..063a53c 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt @@ -2,9 +2,13 @@ package dev.inmo.plagubot.config import com.github.matfax.klassindex.KlassIndex import dev.inmo.plagubot.Plugin -import dev.inmo.sdi.Module +import dev.inmo.plagubot.PluginSerializer +import dev.inmo.sdi.* import kotlinx.serialization.* -import kotlinx.serialization.json.Json +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* import kotlinx.serialization.modules.* import kotlin.reflect.KClass @@ -12,7 +16,7 @@ import kotlin.reflect.KClass internal inline fun KClass.includeIn(builder: PolymorphicModuleBuilder) = builder.subclass(this, serializer()) @InternalSerializationApi -internal val configSerialFormat: StringFormat +internal val configJsonFormat: Json get() = Json { ignoreUnknownKeys = true serializersModule = SerializersModule { @@ -37,8 +41,126 @@ internal val configSerialFormat: StringFormat @Serializable data class Config( - val plugins: List, + val plugins: List<@Contextual Plugin>, val database: DatabaseConfig = DatabaseConfig(), val botToken: String, + @Contextual val params: Module? = null ) + +@Serializer(Plugin::class) +private class InternalPluginSerializer( + private val params: Module +) : KSerializer { + override val descriptor: SerialDescriptor = PluginSerializer.descriptor + + @InternalSerializationApi + override fun deserialize(decoder: Decoder): Plugin { + val asJson = JsonElement.serializer().deserialize(decoder) + + return if (asJson is JsonPrimitive) { + params[asJson.jsonPrimitive.content] as Plugin + } else { + val jsonFormat = ((decoder as? JsonDecoder)?.json ?: configJsonFormat) + jsonFormat.decodeFromJsonElement(PluginSerializer, asJson) + } + } + + @InternalSerializationApi + override fun serialize(encoder: Encoder, value: Plugin) { + params.keys.firstOrNull { params[it] === value } ?.also { + encoder.encodeString(it) + } ?: PluginSerializer.serialize(encoder, value) + } +} + +private val DefaultModuleSerializer = ModuleSerializer(emptyList()) { + +} + +@Serializer(Module::class) +private class InternalModuleSerializer( + private val original: JsonElement?, + private val params: Module +) : KSerializer { + override val descriptor: SerialDescriptor = PluginSerializer.descriptor + + @InternalSerializationApi + override fun deserialize(decoder: Decoder): Module { + val asJson = JsonElement.serializer().deserialize(decoder) + + return if (asJson == original) { + params + } else { + configJsonFormat.decodeFromJsonElement(DefaultModuleSerializer, asJson) + } + } + + @InternalSerializationApi + override fun serialize(encoder: Encoder, value: Module) = DefaultModuleSerializer.serialize(encoder, value) +} + +private fun internalPluginSerializerSerializersModule( + internalPluginSerializer: InternalPluginSerializer, + internalModuleSerializer: InternalModuleSerializer? +) = SerializersModule { + contextual(internalPluginSerializer) + contextual(internalModuleSerializer ?: return@SerializersModule) +} + +@Serializer(Config::class) +internal object ConfigSerializer : KSerializer { + private val jsonSerializer = JsonObject.serializer() + private val moduleSerializer = ModuleSerializer() + + @InternalSerializationApi + override fun deserialize(decoder: Decoder): Config { + val json = jsonSerializer.deserialize(decoder) + val jsonFormat = (decoder as? JsonDecoder) ?.json ?: configJsonFormat + val paramsRow = json["params"] + + val resultJsonFormat = if (paramsRow != null && paramsRow != JsonNull) { + val params = jsonFormat.decodeFromJsonElement( + moduleSerializer, + paramsRow + ) + + val pluginsSerializer = InternalPluginSerializer(params) + val moduleSerializer = InternalModuleSerializer(paramsRow, params) + Json(jsonFormat) { + serializersModule = decoder.serializersModule.overwriteWith( + internalPluginSerializerSerializersModule(pluginsSerializer, moduleSerializer) + ) + } + } else { + jsonFormat + } + return resultJsonFormat.decodeFromJsonElement( + Config.serializer(), + json + ) + } + + @InternalSerializationApi + override fun serialize(encoder: Encoder, value: Config) { + if (value.params != null) { + val pluginsSerializer = InternalPluginSerializer(value.params) + + val jsonFormat = Json(configJsonFormat) { + serializersModule = encoder.serializersModule.overwriteWith( + internalPluginSerializerSerializersModule(pluginsSerializer, null) + ) + } + + jsonSerializer.serialize( + encoder, + jsonFormat.encodeToJsonElement( + Config.serializer(), + value + ) as JsonObject + ) + } else { + Config.serializer().serialize(encoder, value) + } + } +} diff --git a/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt b/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt new file mode 100644 index 0000000..48b6fa9 --- /dev/null +++ b/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt @@ -0,0 +1,37 @@ +package dev.inmo.plagubot.config + +import dev.inmo.plagubot.HelloPlugin +import kotlinx.serialization.InternalSerializationApi +import org.junit.Test +import kotlin.test.assertEquals + +class ConfigTest { + @InternalSerializationApi + @Test + fun testThatPluginPassedToParamsWillBeCorrectlyUsedInPlugins() { + val rawConfig = """ + { + "database": { + }, + "botToken": "", + "plugins": [ + "helloPlugin" + ], + "params": { + "helloPlugin": {"type": "dev.inmo.plagubot.HelloPlugin", "parameter": "Example"} + } + } + """.trimIndent() + val config = configJsonFormat.decodeFromString(ConfigSerializer, rawConfig) + + assert(config.plugins.size == 1) + assert(config.plugins.first() is HelloPlugin) + assert((config.plugins.first() as HelloPlugin).parameter == "Example") + + val redecoded = configJsonFormat.decodeFromString(ConfigSerializer, configJsonFormat.encodeToString(ConfigSerializer, config)) + assertEquals(config.database, redecoded.database) + assertEquals(config.plugins, redecoded.plugins) + assertEquals(config.botToken, redecoded.botToken) + assertEquals(config.params ?.toMap(), redecoded.params ?.toMap()) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index b3dcac8..7354155 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,9 +9,9 @@ kotlin_coroutines_version=1.4.2 kotlin_serialisation_runtime_version=1.1.0-RC kotlin_exposed_version=0.29.1 -sdi_version=0.4.0-rc2 -tgbotapi_version=0.32.7 -microutils_version=0.4.25 +sdi_version=0.4.1 +tgbotapi_version=0.32.8 +microutils_version=0.4.26 klassindex_version=4.1.0-rc.1 sqlite_version=3.30.1