Compare commits

..

15 Commits

16 changed files with 256 additions and 79 deletions

View File

@@ -1,7 +1,20 @@
# Changelog # Changelog
## 0.16.0
* `Versions`:
* `Ktor`: `2.1.3` -> `2.2.1`
* `Android Fragment`: `1.5.3` -> `1.5.5`
## 0.15.1 ## 0.15.1
* `Startup`:
* Inited :)
* `Plugin`:
* Inited :)
* `Launcher`:
* Inited :)
## 0.15.0 ## 0.15.0
* `Repos`: * `Repos`:

View File

@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.15.1 version=0.16.0
android_code_version=167 android_code_version=168

View File

@@ -13,19 +13,19 @@ jb-dokka = "1.7.20"
klock = "3.4.0" klock = "3.4.0"
uuid = "0.6.0" uuid = "0.6.0"
ktor = "2.1.3" ktor = "2.2.1"
gh-release = "2.4.1" gh-release = "2.4.1"
koin = "3.2.2" koin = "3.2.2"
android-gradle = "7.2.2" android-gradle = "7.3.0"
dexcount = "3.1.0" dexcount = "3.1.0"
android-coreKtx = "1.9.0" android-coreKtx = "1.9.0"
android-recyclerView = "1.2.1" android-recyclerView = "1.2.1"
android-appCompat = "1.5.1" android-appCompat = "1.5.1"
android-fragment = "1.5.3" android-fragment = "1.5.5"
android-espresso = "3.4.0" android-espresso = "3.4.0"
android-test = "1.1.3" android-test = "1.1.3"

View File

@@ -1,6 +1,7 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "application"
} }
apply from: "$mppJavaProjectPresetPath" apply from: "$mppJavaProjectPresetPath"
@@ -12,5 +13,19 @@ kotlin {
api internalProject("micro_utils.startup.plugin") api internalProject("micro_utils.startup.plugin")
} }
} }
jvmTest {
dependencies {
implementation libs.kt.coroutines.test
}
}
} }
} }
application {
mainClassName = "dev.inmo.micro_utils.startup.launcher.MainKt"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -1,9 +1,22 @@
package dev.inmo.micro_utils.startup.launcher package dev.inmo.micro_utils.startup.launcher
import dev.inmo.micro_utils.startup.plugin.ServerPlugin import dev.inmo.micro_utils.startup.plugin.StartPlugin
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/**
* Contains just [List] of [StartPlugin]s. In json this config should look like:
*
* ```json
* {
* "plugins": [
* "dev.inmo.micro_utils.startup.launcher.HelloWorldPlugin"
* ]
* }
* ```
*
* In the sample above [HelloWorldPlugin] will be loaded during startup of application
*/
@Serializable @Serializable
data class Config( data class Config(
val plugins: List<ServerPlugin> val plugins: List<StartPlugin>
) )

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.startup.launcher
import dev.inmo.kslog.common.i
import dev.inmo.kslog.common.logger
import dev.inmo.micro_utils.startup.plugin.StartPlugin
import org.koin.core.Koin
object HelloWorldPlugin : StartPlugin {
override suspend fun startPlugin(koin: Koin) {
super.startPlugin(koin)
logger.i("Hello world")
}
}

View File

@@ -0,0 +1,31 @@
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.dsl.module
/**
* Will create [KoinApplication], init, load modules using [StartLauncherPlugin] and start plugins using the same base
* plugin
*
* @param rawConfig It is expected that this [JsonObject] will contain serialized [Config] ([StartLauncherPlugin] will
* deserialize it in its [StartLauncherPlugin.setupDI]
*/
suspend fun start(rawConfig: JsonObject) {
with(StartLauncherPlugin) {
logger.i("Start initialization")
val koinApp = KoinApplication.init()
koinApp.modules(
module {
setupDI(rawConfig)
}
)
logger.i("Modules loaded")
GlobalContext.startKoin(koinApp)
logger.i("Koin started")
startPlugin(koinApp.koin)
logger.i("App has been setup")
}
}

View File

@@ -4,23 +4,38 @@ import dev.inmo.kslog.common.i
import dev.inmo.kslog.common.taggedLogger import dev.inmo.kslog.common.taggedLogger
import dev.inmo.kslog.common.w import dev.inmo.kslog.common.w
import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.startup.plugin.ServerPlugin import dev.inmo.micro_utils.startup.plugin.StartPlugin
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.SerialFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.binds
import org.koin.dsl.module import org.koin.dsl.module
object StartupLauncher : ServerPlugin { /**
* Default startup plugin. See [setupDI] and [startPlugin] for more info
*/
object StartLauncherPlugin : StartPlugin {
internal val logger = taggedLogger(this) internal val logger = taggedLogger(this)
/**
* Will deserialize [Config] from [config], register it in receiver [Module] (as well as [CoroutineScope] and
* [kotlinx.serialization.json.Json])
*
* Besides, in this method will be called [StartPlugin.setupDI] on each plugin from [Config.plugins]. In case when
* some plugin will not be loaded correctly it will be reported throw the [logger]
*/
override fun Module.setupDI(config: JsonObject) { override fun Module.setupDI(config: JsonObject) {
val pluginsConfig = defaultJson.decodeFromJsonElement(Config.serializer(), config) val pluginsConfig = defaultJson.decodeFromJsonElement(Config.serializer(), config)
single { pluginsConfig } single { pluginsConfig }
single { CoroutineScope(Dispatchers.Default) } single { CoroutineScope(Dispatchers.Default) }
single { defaultJson } binds arrayOf(StringFormat::class, SerialFormat::class)
includes( includes(
pluginsConfig.plugins.mapNotNull { pluginsConfig.plugins.mapNotNull {
@@ -37,6 +52,10 @@ object StartupLauncher : ServerPlugin {
) )
} }
/**
* Takes [CoroutineScope] and [Config] from the [koin], and call starting of each plugin from [Config.plugins]
* ASYNCHRONOUSLY. Just like in [setupDI], in case of fail in some plugin it will be reported using [logger]
*/
override suspend fun startPlugin(koin: Koin) { override suspend fun startPlugin(koin: Koin) {
val scope = koin.get<CoroutineScope>() val scope = koin.get<CoroutineScope>()
koin.get<Config>().plugins.map { plugin -> koin.get<Config>().plugins.map { plugin ->

View File

@@ -0,0 +1,37 @@
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 java.io.File
/**
* It is expected, that [args] will contain 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] KDocs.
*
* 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`
*/
suspend fun main(args: Array<String>) {
KSLog.default = KSLog("ServerLauncher")
val (configPath) = args
val file = File(configPath)
KSLog.i("Start read config from ${file.absolutePath}")
val json = defaultJson.parseToJsonElement(file.readText()).jsonObject
KSLog.i("Config has been read")
start(json)
}

View File

@@ -1,36 +0,0 @@
package dev.inmo.micro_utils.startup.launcher
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.i
import dev.inmo.micro_utils.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.jsonObject
import org.koin.core.KoinApplication
import org.koin.core.context.GlobalContext
import org.koin.dsl.module
import java.io.File
suspend fun main(args: Array<String>) {
KSLog.default = KSLog("ServerLauncher")
val (configPath) = args
val file = File(configPath)
KSLog.i("Start read config from ${file.absolutePath}")
val json = defaultJson.parseToJsonElement(file.readText()).jsonObject
KSLog.i("Config has been read")
with(StartupLauncher) {
logger.i("Start initialization")
val koinApp = KoinApplication.init()
koinApp.modules(
module {
setupDI(json)
}
)
logger.i("Modules loaded")
GlobalContext.startKoin(koinApp)
logger.i("Koin started")
startPlugin(koinApp.koin)
logger.i("Behaviour builder has been setup")
}
}

View File

@@ -0,0 +1,46 @@
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
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 kotlin.test.BeforeTest
import kotlin.test.Test
class StartupLaunchingTests {
@BeforeTest
fun resetGlobalKoinContext() {
kotlin.runCatching { GlobalContext.stopKoin() }
}
@Test(timeout = 60000L)
fun CheckThatEmptyPluginsListLeadsToEndOfMain() {
val emptyJson = defaultJson.encodeToJsonElement(
Config.serializer(),
Config(emptyList())
).jsonObject
runTest {
val job = launch {
start(emptyJson)
}
job.join()
}
}
@Test(timeout = 60000L)
fun CheckThatHelloWorldPluginsListLeadsToEndOfMain() {
val emptyJson = defaultJson.encodeToJsonElement(
Config.serializer(),
Config(listOf(HelloWorldPlugin))
).jsonObject
runTest {
val job = launch {
start(emptyJson)
}
job.join()
}
}
}

View File

@@ -12,6 +12,7 @@ kotlin {
api libs.koin api libs.koin
api libs.kt.serialization api libs.kt.serialization
api libs.kslog api libs.kslog
api libs.kt.reflect
api project(":micro_utils.coroutines") api project(":micro_utils.coroutines")
} }
} }

View File

@@ -1,34 +0,0 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonObject
import org.koin.core.Koin
import org.koin.core.module.Module
@Serializable(ServerPlugin.Companion::class)
interface ServerPlugin {
fun Module.setupDI(config: JsonObject) {}
suspend fun startPlugin(koin: Koin) {}
companion object : KSerializer<ServerPlugin> {
override val descriptor: SerialDescriptor
get() = String.serializer().descriptor
override fun deserialize(decoder: Decoder): ServerPlugin {
val kclass = Class.forName(decoder.decodeString()).kotlin
return (kclass.objectInstance ?: kclass.constructors.first { it.parameters.isEmpty() }.call()) as ServerPlugin
}
override fun serialize(encoder: Encoder, value: ServerPlugin) {
encoder.encodeString(
value::class.java.canonicalName
)
}
}
}

View File

@@ -0,0 +1,31 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import org.koin.core.Koin
import org.koin.core.module.Module
/**
* Default plugin for start of your app
*/
@Serializable(StartPluginSerializer::class)
interface StartPlugin {
/**
* This method will be called first to configure [Koin] [Module] related to this plugin. You may use
* [org.koin.core.scope.Scope.get] in your koin definitions like [Module.single] to retrieve
* [kotlinx.coroutines.CoroutineScope], [kotlinx.serialization.json.Json] or [dev.inmo.micro_utils.startup.launcher.Config]
*/
fun Module.setupDI(config: JsonObject) {}
/**
* This method will be called after all other [StartPlugin] will [setupDI]
*
* It is allowed to lock end of this method in case you require to prevent application to end its run (for example,
* you are starting some web server)
*
* @param koin Will contains everything you will register in [setupDI] (as well as other [StartPlugin]s) and
* [kotlinx.coroutines.CoroutineScope], [kotlinx.serialization.json.Json] and [dev.inmo.micro_utils.startup.launcher.Config]
* by their types
*/
suspend fun startPlugin(koin: Koin) {}
}

View File

@@ -0,0 +1,5 @@
package dev.inmo.micro_utils.startup.plugin
import kotlinx.serialization.KSerializer
expect object StartPluginSerializer : KSerializer<StartPlugin>

View File

@@ -0,0 +1,23 @@
package dev.inmo.micro_utils.startup.plugin
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<StartPlugin> {
override val descriptor: SerialDescriptor
get() = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StartPlugin {
val kclass = Class.forName(decoder.decodeString()).kotlin
return (kclass.objectInstance ?: kclass.constructors.first { it.parameters.isEmpty() }.call()) as StartPlugin
}
override fun serialize(encoder: Encoder, value: StartPlugin) {
encoder.encodeString(
value::class.java.canonicalName
)
}
}