diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b833ee..9f662b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 0.2.2 +* `Bot` + * Add plugin `PluginsHolder` + * Rewrite mechanism of `Config` working + * `PlaguBot` now is correctly serializable/deserializable + ## 0.2.1 * `Versions` diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt index cfa0bbe..9c5b12f 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt @@ -24,7 +24,7 @@ suspend inline fun initPlaguBot( suspend fun main(args: Array) { val (configPath) = args val file = File(configPath) - val config = configJsonFormat.decodeFromString(ConfigSerializer, file.readText()) + val config = configAndPluginsConfigJsonFormat.decodeFromString(PluginsConfigurationSerializer, file.readText()) as Config PlaguBot(config).start().join() } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt index ca7e3b5..98dae72 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt @@ -1,8 +1,7 @@ package dev.inmo.plagubot import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions -import dev.inmo.plagubot.config.Config -import dev.inmo.plagubot.config.database +import dev.inmo.plagubot.config.* import dev.inmo.tgbotapi.bot.Ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext @@ -20,6 +19,7 @@ val Map.plagubot @Serializable data class PlaguBot( + @Serializable(PluginsConfigurationSerializer::class) private val config: Config ) : Plugin { @Transient diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PluginsHolder.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PluginsHolder.kt new file mode 100644 index 0000000..a30a362 --- /dev/null +++ b/bot/src/main/kotlin/dev/inmo/plagubot/PluginsHolder.kt @@ -0,0 +1,25 @@ +package dev.inmo.plagubot + +import dev.inmo.plagubot.config.PluginsConfigurationSerializer +import dev.inmo.plagubot.config.SimplePluginsConfiguration +import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.types.BotCommand +import kotlinx.serialization.Serializable +import org.jetbrains.exposed.sql.Database + +@Serializable +data class PluginsHolder( + @Serializable(PluginsConfigurationSerializer::class) + private val pluginsConfiguration: SimplePluginsConfiguration +) : Plugin { + override suspend fun getCommands(): List = pluginsConfiguration.plugins.flatMap { + it.getCommands() + } + + override suspend fun BehaviourContext.invoke(database: Database, params: Map) { + val finalParams = pluginsConfiguration.params ?.plus(params) ?: params + pluginsConfiguration.plugins.forEach { + it.apply { invoke(database, finalParams) } + } + } +} \ No newline at end of file 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 063a53c..d4b0238 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt @@ -1,166 +1,15 @@ package dev.inmo.plagubot.config -import com.github.matfax.klassindex.KlassIndex import dev.inmo.plagubot.Plugin -import dev.inmo.plagubot.PluginSerializer -import dev.inmo.sdi.* -import kotlinx.serialization.* -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 - -@InternalSerializationApi -internal inline fun KClass.includeIn(builder: PolymorphicModuleBuilder) = builder.subclass(this, serializer()) - -@InternalSerializationApi -internal val configJsonFormat: Json - get() = Json { - ignoreUnknownKeys = true - serializersModule = SerializersModule { - polymorphic(Plugin::class) { - KlassIndex.getSubclasses(Plugin::class).flatMap { kclass -> - kclass.includeIn(this) - kclass.annotations.mapNotNull { it as? SerialName }.map { - it.value to kclass.serializer() - } + listOfNotNull( - kclass.simpleName ?.let { - it to kclass.serializer() - } - ) - }.toMap().let { - default { requiredType -> - it[requiredType] - } - } - } - } - } +import dev.inmo.sdi.Module +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable @Serializable data class Config( - val plugins: List<@Contextual Plugin>, + override 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) - } - } -} + override val params: Module? = null +) : PluginsConfiguration diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/PluginsConfiguration.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/PluginsConfiguration.kt new file mode 100644 index 0000000..c94ada7 --- /dev/null +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/PluginsConfiguration.kt @@ -0,0 +1,190 @@ +package dev.inmo.plagubot.config + +import com.github.matfax.klassindex.KlassIndex +import dev.inmo.plagubot.Plugin +import dev.inmo.plagubot.PluginSerializer +import dev.inmo.sdi.Module +import dev.inmo.sdi.ModuleSerializer +import kotlinx.serialization.* +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 + +@InternalSerializationApi +internal inline fun KClass.includeIn(builder: PolymorphicModuleBuilder) = builder.subclass(this, serializer()) + +@InternalSerializationApi +internal val configAndPluginsConfigJsonFormat: Json + get() = Json { + ignoreUnknownKeys = true + serializersModule = SerializersModule { + polymorphic(Plugin::class) { + KlassIndex.getSubclasses(Plugin::class).flatMap { kclass -> + kclass.includeIn(this) + kclass.annotations.mapNotNull { it as? SerialName }.map { + it.value to kclass.serializer() + } + listOfNotNull( + kclass.simpleName ?.let { + it to kclass.serializer() + } + ) + }.toMap().let { + default { requiredType -> + it[requiredType] + } + } + } + } + } + +internal interface PluginsConfiguration { + val plugins: List + val params: Module? +} + +@Serializable +data class SimplePluginsConfiguration( + override val plugins: List<@Contextual Plugin>, + @Contextual + override val params: Module? = null +) : PluginsConfiguration + + +internal val DefaultModuleSerializer = ModuleSerializer(emptyList()) { + +} + +@Serializer(Plugin::class) +internal 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 ?: configAndPluginsConfigJsonFormat) + 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) + } +} + +@Serializer(Module::class) +internal 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 { + configAndPluginsConfigJsonFormat.decodeFromJsonElement(DefaultModuleSerializer, asJson) + } + } + + @InternalSerializationApi + override fun serialize(encoder: Encoder, value: Module) = DefaultModuleSerializer.serialize(encoder, value) +} + +internal fun internalPluginSerializerSerializersModule( + internalPluginSerializer: InternalPluginSerializer, + internalModuleSerializer: InternalModuleSerializer? +) = SerializersModule { + contextual(internalPluginSerializer) + contextual(internalModuleSerializer ?: return@SerializersModule) +} + +@Serializer(PluginsConfiguration::class) +internal object PluginsConfigurationSerializer : KSerializer { + private val jsonSerializer = JsonObject.serializer() + private val moduleSerializer = ModuleSerializer() + override val descriptor: SerialDescriptor = jsonSerializer.descriptor + + @InternalSerializationApi + override fun deserialize(decoder: Decoder): PluginsConfiguration { + val json = jsonSerializer.deserialize(decoder) + val jsonFormat = (decoder as? JsonDecoder) ?.json ?: configAndPluginsConfigJsonFormat + 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 try { + resultJsonFormat.decodeFromJsonElement( + Config.serializer(), + json + ) + } catch (e: SerializationException) { + resultJsonFormat.decodeFromJsonElement( + SimplePluginsConfiguration.serializer(), + json + ) + } + } + + @InternalSerializationApi + override fun serialize(encoder: Encoder, value: PluginsConfiguration) { + val params = value.params + val serializer = when (value) { + is Config -> Config.serializer() + is SimplePluginsConfiguration -> SimplePluginsConfiguration.serializer() + else -> return + } + if (params != null) { + val pluginsSerializer = InternalPluginSerializer(params) + + val jsonFormat = Json(configAndPluginsConfigJsonFormat) { + serializersModule = encoder.serializersModule.overwriteWith( + internalPluginSerializerSerializersModule(pluginsSerializer, null) + ) + } + + jsonSerializer.serialize( + encoder, + when (value) { + is Config -> jsonFormat.encodeToJsonElement(Config.serializer(), value) + is SimplePluginsConfiguration -> jsonFormat.encodeToJsonElement(SimplePluginsConfiguration.serializer(), value) + else -> return + } as JsonObject + ) + } else { + when (value) { + is Config -> Config.serializer().serialize(encoder, value) + is SimplePluginsConfiguration -> SimplePluginsConfiguration.serializer().serialize(encoder, value) + else -> return + } + } + } +} diff --git a/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt b/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt index 48b6fa9..3cd4fe9 100644 --- a/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt +++ b/bot/src/test/kotlin/dev/inmo/plagubot/config/ConfigTest.kt @@ -22,13 +22,16 @@ class ConfigTest { } } """.trimIndent() - val config = configJsonFormat.decodeFromString(ConfigSerializer, rawConfig) + val config = configAndPluginsConfigJsonFormat.decodeFromString(PluginsConfigurationSerializer, rawConfig) as Config 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)) + val redecoded = configAndPluginsConfigJsonFormat.decodeFromString( + PluginsConfigurationSerializer, + configAndPluginsConfigJsonFormat.encodeToString(PluginsConfigurationSerializer, config) + ) as Config assertEquals(config.database, redecoded.database) assertEquals(config.plugins, redecoded.plugins) assertEquals(config.botToken, redecoded.botToken)