diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb04dda479..e79aa0c3e38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.16.2 + +* `Versions`: + * `Compose`: `1.2.1` -> `1.2.2` +* `Startup`: + * Module become available on `JS` target + ## 0.16.1 * `Coroutines`: diff --git a/extensions.gradle b/extensions.gradle index 751d061e0d8..9b17566dc84 100644 --- a/extensions.gradle +++ b/extensions.gradle @@ -23,6 +23,7 @@ allprojects { mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle" mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" + mppJsAndJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsAndJavaProject.gradle" mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle" diff --git a/gradle.properties b/gradle.properties index c382506e62f..7c110af57c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.16.1 -android_code_version=169 +version=0.16.2 +android_code_version=170 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2a605e31cc..a5e33be3753 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kt-coroutines = "1.6.4" kslog = "0.5.4" -jb-compose = "1.2.1" +jb-compose = "1.2.2" jb-exposed = "0.41.1" jb-dokka = "1.7.20" diff --git a/mppJsAndJavaProject.gradle b/mppJsAndJavaProject.gradle new file mode 100644 index 00000000000..4b24838a9f3 --- /dev/null +++ b/mppJsAndJavaProject.gradle @@ -0,0 +1,49 @@ +project.version = "$version" +project.group = "$group" + +apply from: "$publishGradlePath" + +kotlin { + jvm { + compilations.main { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + js (IR) { + browser() + nodejs() + } + + sourceSets { + commonMain { + dependencies { + implementation kotlin('stdlib') + } + } + commonTest { + dependencies { + implementation kotlin('test-common') + implementation kotlin('test-annotations-common') + } + } + + jvmTest { + dependencies { + implementation kotlin('test-junit') + } + } + jsTest { + dependencies { + implementation kotlin('test-js') + implementation kotlin('test-junit') + } + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/startup/launcher/README.md b/startup/launcher/README.md new file mode 100644 index 00000000000..82206d17dcd --- /dev/null +++ b/startup/launcher/README.md @@ -0,0 +1,92 @@ +# Startup Plugin Launcher + +This module contains tools to start your plugin system. + +## Config + +Base config is pretty simple: + +```json +{ + "plugins": [ + "dev.inmo.micro_utils.startup.launcher.HelloWorldPlugin" + ] +} +``` + +So, `"dev.inmo.micro_utils.startup.launcher.HelloWorldPlugin"` is the fully qualified name of plugin you wish to be +included in the server. + +> JS note: In JS there are no opportunity to determine object type by its full name. Because of it, in JS developers +> should prefer to use `Config` in their kotlin code directly instead of json config passing. More info see in [JS](#js) +> section + +## JVM + +For JVM target you may use main class by path: `dev.inmo.micro_utils.startup.launcher.MainKt` + +It is expected, that you will pass the main ONE argument with path to the config json. Sample of launching: + +```bash +./gradlew run --args="sample.config.json" +``` + +Content of `sample.config.json` described in [Config](#config) section. + +You may build runnable app using: + +```bash +./gradlew assembleDist +``` + +In that case in `build/distributions` folder you will be able to find zip and tar files with all required +tools for application running (via their `bin/app_name` binary). In that case yoy will not need to pass +`--args=...` and launch will look like `./bin/app_name sample.config.json` + +## JS + +In JS for starting of your plugins app, you should use `PluginsStarter` in your code: + +```kotlin +PluginsStarter.startPlugins( + Config(HelloWorldPlugin) +) +``` + +`Config` here is deserialized variant from [Config](#config) section. As was said in [Config](#config) section, in JS +there is no way to find classes/objects by their full qualifiers. Because of it you should use some way to register your +plugins in `StartPluginSerializer` or use the code like in the snippet above: there plugins will be registered +automatically. + +In case you wish to register your plugins manually and run server from config, you should use one of the ways to register +plugin on start. + +Sample with `EagerInitialization`: [Kotlin JS doc about lazy initialization](https://kotlinlang.org/docs/js-ir-compiler.html#incremental-compilation-for-development-binaries), + [@EagerInitialization docs](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.js/-eager-initialization/): + +```kotlin +@ExperimentalStdlibApi +@EagerInitialization +val plugin = createStartupPluginAndRegister("PluginNameToUseInConfig") { + // Your plugin creation. For example: + HelloWorldPlugin +} +``` + +So, in that case you will be able to load plugins list as `JsonObject` from anywhere and start plugins app with it: + +```kotlin +PluginsStarter.startPlugins( + jsonObject +) +``` + +It will load `HelloWorldPlugin` if `jsonObject` have next content: + +```json +{ + "plugins": [ + "PluginNameToUseInConfig" + ] +} +``` diff --git a/startup/launcher/build.gradle b/startup/launcher/build.gradle index f04028c0d4d..e80e77abe88 100644 --- a/startup/launcher/build.gradle +++ b/startup/launcher/build.gradle @@ -4,7 +4,7 @@ plugins { id "application" } -apply from: "$mppJavaProjectPresetPath" +apply from: "$mppJsAndJavaProjectPresetPath" kotlin { sourceSets { @@ -13,7 +13,7 @@ kotlin { api internalProject("micro_utils.startup.plugin") } } - jvmTest { + commonTest { dependencies { implementation libs.kt.coroutines.test } diff --git a/startup/launcher/src/commonMain/kotlin/Start.kt b/startup/launcher/src/commonMain/kotlin/Start.kt index 7526adb8de9..fa22995d9dd 100644 --- a/startup/launcher/src/commonMain/kotlin/Start.kt +++ b/startup/launcher/src/commonMain/kotlin/Start.kt @@ -3,7 +3,7 @@ package dev.inmo.micro_utils.startup.launcher import dev.inmo.kslog.common.i import kotlinx.serialization.json.JsonObject import org.koin.core.KoinApplication -import org.koin.core.context.GlobalContext +import org.koin.core.context.startKoin import org.koin.dsl.module /** @@ -23,7 +23,7 @@ suspend fun start(rawConfig: JsonObject) { } ) logger.i("Modules loaded") - GlobalContext.startKoin(koinApp) + startKoin(koinApp) logger.i("Koin started") startPlugin(koinApp.koin) logger.i("App has been setup") diff --git a/startup/launcher/src/jvmTest/kotlin/StartupLaunchingTests.kt b/startup/launcher/src/commonTest/kotlin/StartupLaunchingTests.kt similarity index 84% rename from startup/launcher/src/jvmTest/kotlin/StartupLaunchingTests.kt rename to startup/launcher/src/commonTest/kotlin/StartupLaunchingTests.kt index f911e0d5234..1da5c79224c 100644 --- a/startup/launcher/src/jvmTest/kotlin/StartupLaunchingTests.kt +++ b/startup/launcher/src/commonTest/kotlin/StartupLaunchingTests.kt @@ -1,4 +1,3 @@ -import dev.inmo.micro_utils.coroutines.launchSynchronously import dev.inmo.micro_utils.startup.launcher.Config import dev.inmo.micro_utils.startup.launcher.HelloWorldPlugin import dev.inmo.micro_utils.startup.launcher.defaultJson @@ -6,16 +5,16 @@ import dev.inmo.micro_utils.startup.launcher.start import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.jsonObject -import org.koin.core.context.GlobalContext +import org.koin.core.context.stopKoin import kotlin.test.BeforeTest import kotlin.test.Test class StartupLaunchingTests { @BeforeTest fun resetGlobalKoinContext() { - kotlin.runCatching { GlobalContext.stopKoin() } + runCatching { stopKoin() } } - @Test(timeout = 60000L) + @Test fun CheckThatEmptyPluginsListLeadsToEndOfMain() { val emptyJson = defaultJson.encodeToJsonElement( Config.serializer(), @@ -29,7 +28,7 @@ class StartupLaunchingTests { job.join() } } - @Test(timeout = 60000L) + @Test fun CheckThatHelloWorldPluginsListLeadsToEndOfMain() { val emptyJson = defaultJson.encodeToJsonElement( Config.serializer(), diff --git a/startup/launcher/src/jsMain/kotlin/PluginsStarter.kt b/startup/launcher/src/jsMain/kotlin/PluginsStarter.kt new file mode 100644 index 00000000000..37bb2e6f1a8 --- /dev/null +++ b/startup/launcher/src/jsMain/kotlin/PluginsStarter.kt @@ -0,0 +1,33 @@ +package dev.inmo.micro_utils.startup.launcher + +import dev.inmo.kslog.common.KSLog +import dev.inmo.kslog.common.i +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject + +object PluginsStarter { + init { + KSLog.default = KSLog("Launcher") + } + + /** + * It is expected that you have registered all the [dev.inmo.micro_utils.startup.plugin.StartPlugin]s of your JS + * app inside of [dev.inmo.micro_utils.startup.plugin.StartPluginSerializer] using its + * [dev.inmo.micro_utils.startup.plugin.StartPluginSerializer.registerPlugin] method + */ + suspend fun startPlugins(json: JsonObject) { + start(json) + } + /** + * Will convert [config] to [JsonObject] with auto registration of [dev.inmo.micro_utils.startup.plugin.StartPlugin]s + * in [dev.inmo.micro_utils.startup.plugin.StartPluginSerializer] + */ + suspend fun startPlugins(config: Config) { + + KSLog.i("Start convert config to JSON") + val json = defaultJson.encodeToJsonElement(Config.serializer(), config).jsonObject + KSLog.i("Config has been read") + + start(json) + } +} diff --git a/startup/launcher/src/jvmMain/kotlin/Main.kt b/startup/launcher/src/jvmMain/kotlin/Main.kt index 23bb111cefa..3d1f475f366 100644 --- a/startup/launcher/src/jvmMain/kotlin/Main.kt +++ b/startup/launcher/src/jvmMain/kotlin/Main.kt @@ -26,7 +26,7 @@ import java.io.File */ suspend fun main(args: Array) { - KSLog.default = KSLog("ServerLauncher") + KSLog.default = KSLog("Launcher") val (configPath) = args val file = File(configPath) KSLog.i("Start read config from ${file.absolutePath}") diff --git a/startup/plugin/build.gradle b/startup/plugin/build.gradle index 9177a9e7e61..64b4acd8103 100644 --- a/startup/plugin/build.gradle +++ b/startup/plugin/build.gradle @@ -3,7 +3,7 @@ plugins { id "org.jetbrains.kotlin.plugin.serialization" } -apply from: "$mppJavaProjectPresetPath" +apply from: "$mppJsAndJavaProjectPresetPath" kotlin { sourceSets { @@ -16,5 +16,10 @@ kotlin { api project(":micro_utils.coroutines") } } + jsMain { + dependencies { + api libs.uuid + } + } } } diff --git a/startup/plugin/src/jsMain/kotlin/CreateStartupPluginAndRegister.kt b/startup/plugin/src/jsMain/kotlin/CreateStartupPluginAndRegister.kt new file mode 100644 index 00000000000..b4feec36747 --- /dev/null +++ b/startup/plugin/src/jsMain/kotlin/CreateStartupPluginAndRegister.kt @@ -0,0 +1,38 @@ +package dev.inmo.micro_utils.startup.plugin + +import com.benasher44.uuid.uuid4 +import kotlin.reflect.KClass + +/** + * Creates [T] using [block], register it in [StartPluginSerializer] using its [StartPluginSerializer.registerPlugin] + * and returns created by [block] plugin + * + * @param name Will be used as a key for registration in [StartPluginSerializer] and will be passed to the [block] as + * parameter + */ +inline fun createStartupPluginAndRegister( + name: String = uuid4().toString(), + block: (String) -> T +): T { + val plugin = block(name) + StartPluginSerializer.registerPlugin(name, plugin) + return plugin +} + +/** + * Creates [T] using [block], register it in [StartPluginSerializer] using its [StartPluginSerializer.registerPlugin] + * and returns created by [block] plugin + */ +inline fun createStartupPluginAndRegister( + kClass: KClass, + name: String = uuid4().toString(), + block: (String) -> T +): T = createStartupPluginAndRegister("${kClass.simpleName}_$name", block) + +/** + * Creates [T] using [block], register it in [StartPluginSerializer] using its [StartPluginSerializer.registerPlugin] + * and returns created by [block] plugin + */ +inline fun createStartupPluginAndRegister( + block: (String) -> T +): T = createStartupPluginAndRegister(T::class, uuid4().toString(), block) diff --git a/startup/plugin/src/jsMain/kotlin/StartPluginSerializer.kt b/startup/plugin/src/jsMain/kotlin/StartPluginSerializer.kt new file mode 100644 index 00000000000..b8eda302903 --- /dev/null +++ b/startup/plugin/src/jsMain/kotlin/StartPluginSerializer.kt @@ -0,0 +1,35 @@ +package dev.inmo.micro_utils.startup.plugin + +import com.benasher44.uuid.uuid4 +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +actual object StartPluginSerializer : KSerializer { + private val registeredPlugins = mutableMapOf() + private val registeredPluginsByPlugin = mutableMapOf() + override val descriptor: SerialDescriptor = String.serializer().descriptor + + override fun deserialize(decoder: Decoder): StartPlugin { + val name = decoder.decodeString() + return registeredPlugins[name] ?: error("Unable to find startup plugin for $name") + } + + override fun serialize(encoder: Encoder, value: StartPlugin) { + val name = registeredPluginsByPlugin[value] ?: uuid4().toString().also { + registeredPlugins[it] = value + registeredPluginsByPlugin[value] = it + } + encoder.encodeString(name) + } + + /** + * Register plugin inside of this [KSerializer]. Since plugin has been registered, you may use its [name] in any + * serialized [dev.inmo.micro_utils.startup.launcher.Config] to retrieve [plugin] you passed here + */ + fun registerPlugin(name: String, plugin: StartPlugin) { + registeredPlugins[name] = plugin + } +}