Skip to content

Commit

Permalink
Feat: Allow defaults for DI observers
Browse files Browse the repository at this point in the history
Feat: Scoped DI

This is pretty much multi-context support but every context is tied to the root DI so clearing is still easy!

Feat: Integrate logging with DI system, tries to create reasonable defaults when a logger isn't registered, deprecate old logging functions.

Deps: Kotlin 1.9.23, compose 1.6.1
  • Loading branch information
0ffz committed Mar 15, 2024
1 parent 5628e5a commit 915f61c
Show file tree
Hide file tree
Showing 19 changed files with 342 additions and 196 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
group=com.mineinabyss
version=0.22
version=0.23
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[versions]
minecraft = "1.20.4-R0.1-SNAPSHOT"
kotlin = "1.9.20"
kotlin = "1.9.23"
jvm = "17"

anvilgui = "1.9.2-SNAPSHOT"
coroutines = "1.7.3"
compose = "1.5.11"
compose = "1.6.1"
exposed = "0.46.0"
fastutil = "8.2.2"
fawe = "2.8.4"
Expand Down Expand Up @@ -153,6 +153,7 @@ platform = [
"ktor-client-cio",
"ktor-client-logging",
"ktor-client-content-negotiation",
"kermit",
"logback-classic",
"kmongo-coroutine-serialization",
"creative-api",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ open class DIContext {
@PublishedApi
internal val moduleObservers = mutableMapOf<KClass<out Any>, ModuleObserver<out Any>>()

@PublishedApi
internal val keyScopes = mutableMapOf<String, ScopedDIContext>()
internal val kClassScopes = mutableMapOf<KClass<*>, ScopedDIContext>()

inline fun <reified T> scoped(): ScopedDIContext = scoped(T::class)

fun scoped(kClass: KClass<*>): ScopedDIContext {
val simpleName = kClass.simpleName ?: error("Class $kClass has no simple name")
return kClassScopes.getOrPut(kClass) {
ScopedDIContext(simpleName = simpleName, byClass = kClass)
}

}

fun scoped(key: String, simpleName: String = key): ScopedDIContext {
return keyScopes.getOrPut(key) { ScopedDIContext(simpleName = simpleName) }
}

/**
* Gets an observer for a module of type [T].
*
Expand Down Expand Up @@ -62,6 +80,7 @@ open class DIContext {

fun clear() {
moduleObservers.forEach { it.value.module = null }
(keyScopes + kClassScopes).forEach { it.value.clear() }
}

@Suppress("UNCHECKED_CAST") // Logic ensures safety
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mineinabyss.idofront.di

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

class DefaultingModuleObserver<T>(
val observer: ModuleObserver<T>,
val defaultProvider: () -> T,
) : ReadOnlyProperty<Any?, T> {
val default by lazy { defaultProvider() }

fun get(): T = observer.getOrNull() ?: default

override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return get()
}
}

fun <T> ModuleObserver<T>.default(provider: () -> T): DefaultingModuleObserver<T> {
return DefaultingModuleObserver(this, provider)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mineinabyss.idofront.di

import kotlin.reflect.KClass

class ScopedDIContext(
val simpleName: String,
val byClass: KClass<*>? = null,
) : DIContext()
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package com.mineinabyss.idofront.features

import com.mineinabyss.idofront.commands.arguments.optionArg
import com.mineinabyss.idofront.commands.execution.IdofrontCommandExecutor
import com.mineinabyss.idofront.messaging.error
import com.mineinabyss.idofront.messaging.logError
import com.mineinabyss.idofront.messaging.logSuccess
import com.mineinabyss.idofront.messaging.success
import com.mineinabyss.idofront.messaging.*
import com.mineinabyss.idofront.plugin.Plugins
import com.mineinabyss.idofront.plugin.actions
import org.bukkit.command.CommandSender
Expand All @@ -14,6 +11,8 @@ import org.bukkit.command.TabCompleter
abstract class FeatureManager<T : FeatureDSL>(
createContext: () -> T,
) : FeatureWithContext<T>(createContext) {
val logger: ComponentLogger get() = context.plugin.injectedLogger()

val commandExecutor: IdofrontCommandExecutor by lazy {
object : IdofrontCommandExecutor(), TabCompleter {
override val commands = commands(context.plugin) {
Expand Down Expand Up @@ -54,15 +53,15 @@ abstract class FeatureManager<T : FeatureDSL>(
val featuresWithMetDeps = context.features.filter { feature -> feature.dependsOn.all { Plugins.isEnabled(it) } }
(context.features - featuresWithMetDeps.toSet()).forEach { feature ->
val featureName = feature::class.simpleName
logError("Could not enable $featureName, missing dependencies: ${feature.dependsOn.filterNot(Plugins::isEnabled)}")
logger.iFail("Could not enable $featureName, missing dependencies: ${feature.dependsOn.filterNot(Plugins::isEnabled)}")
}
"Registering feature contexts" {
featuresWithMetDeps
.filterIsInstance<FeatureWithContext<*>>()
.forEach {
runCatching {
it.createAndInjectContext()
}.onFailure { e -> logError("Failed to create context for ${it::class.simpleName}: $e") }
}.onFailure { error -> logger.iFail("Failed to create context for ${it::class.simpleName}: $error") }
}
}

Expand Down Expand Up @@ -110,8 +109,8 @@ abstract class FeatureManager<T : FeatureDSL>(
"Disabling features" {
context.features.forEach { feature ->
runCatching { feature.disable(context) }
.onSuccess { logSuccess("Disabled ${feature::class.simpleName}") }
.onFailure { e -> logError("Failed to disable ${feature::class.simpleName}: $e") }
.onSuccess { logger.iSuccess("Disabled ${feature::class.simpleName}") }
.onFailure { e -> logger.iFail("Failed to disable ${feature::class.simpleName}: $e") }
}
}
removeContext()
Expand Down
2 changes: 2 additions & 0 deletions idofront-logging/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ plugins {

dependencies {
implementation(project(":idofront-text-components"))
implementation(project(":idofront-di"))
api(libs.kermit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.mineinabyss.idofront.messaging

import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import co.touchlab.kermit.StaticConfig
import com.mineinabyss.idofront.messaging.IdoLogging.errorComp
import com.mineinabyss.idofront.messaging.IdoLogging.successComp
import com.mineinabyss.idofront.textcomponents.miniMsg
import com.mineinabyss.idofront.textcomponents.toPlainText
import net.kyori.adventure.text.ComponentLike
import org.bukkit.plugin.Plugin

open class ComponentLogger(
staticConfig: StaticConfig,
tag: String,
) : Logger(staticConfig, tag) {

fun i(message: ComponentLike) {
if (config.minSeverity <= Severity.Info)
logComponent(Severity.Info, message)
}

fun iMM(message: String) {
i(message.miniMsg())
}

fun iSuccess(message: String) {
i(successComp.append(message.miniMsg()))
}

fun iSuccess(message: ComponentLike) {
i(successComp.append(message))
}

fun iFail(message: String) {
i(errorComp.append(message.miniMsg()))
}

fun iFail(message: ComponentLike) {
i(errorComp.append(message))
}

fun v(message: ComponentLike) {
if (config.minSeverity <= Severity.Verbose)
logComponent(Severity.Verbose, message)
}

fun d(message: ComponentLike) {
if (config.minSeverity <= Severity.Debug)
logComponent(Severity.Debug, message)
}

fun w(message: ComponentLike) {
if (config.minSeverity <= Severity.Warn)
logComponent(Severity.Warn, message)
}

fun e(message: ComponentLike) {
if (config.minSeverity <= Severity.Error)
logComponent(Severity.Error, message)
}

fun a(message: ComponentLike) {
if (config.minSeverity <= Severity.Assert)
logComponent(Severity.Assert, message)
}

fun logComponent(severity: Severity, message: ComponentLike) {
config.logWriterList.forEach {
if (!it.isLoggable(severity)) return@forEach
if (it is KermitPaperWriter) it.log(severity, message)
else it.log(severity, message.asComponent().toPlainText(), tag, null)
}
}

companion object {
fun forPlugin(plugin: Plugin, minSeverity: Severity = Severity.Info): ComponentLogger {
return ComponentLogger(
StaticConfig(minSeverity = minSeverity, logWriterList = listOf(KermitPaperWriter(plugin))),
plugin.name
)
}

fun fallback(
minSeverity: Severity = Severity.Info,
tag: String = "Idofront"
): ComponentLogger {
return ComponentLogger(StaticConfig(minSeverity = minSeverity), tag)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.mineinabyss.idofront.messaging

import com.mineinabyss.idofront.messaging.IdoLogging.ERROR_PREFIX
import com.mineinabyss.idofront.messaging.IdoLogging.SUCCESS_PREFIX
import com.mineinabyss.idofront.messaging.IdoLogging.WARN_PREFIX
import com.mineinabyss.idofront.messaging.IdoLogging.logWithFallback
import com.mineinabyss.idofront.textcomponents.miniMsg
import com.mineinabyss.idofront.textcomponents.toPlainText
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import org.bukkit.command.ConsoleCommandSender

object IdoLogging {
const val ERROR_PREFIX = "<dark_red><b>\u274C</b><red>"
const val SUCCESS_PREFIX = "<green><b>\u2714</b>"
const val WARN_PREFIX = "<yellow>\u26A0<gray>"

val successComp = SUCCESS_PREFIX.miniMsg()
val errorComp = ERROR_PREFIX.miniMsg()
val warnComp = WARN_PREFIX.miniMsg()

@PublishedApi
internal val BUKKIT_LOADED = runCatching {
Bukkit.getConsoleSender()
}.isSuccess

@PublishedApi
internal val ADVENTURE_LOADED = runCatching {
"<green>Test".miniMsg()
}.isSuccess

inline fun logWithFallback(message: Any?, printBukkit: (Component) -> Unit) {
if (ADVENTURE_LOADED) {
val messageComponent = message as? Component ?: message.toString().miniMsg()
if (BUKKIT_LOADED) printBukkit(messageComponent)
else println(messageComponent.toPlainText())
} else {
println(message)
}
}
}

@Deprecated("Use Plugin.logger().i(...)")
fun logInfo(message: Any?) =
logWithFallback(message) { Bukkit.getConsoleSender().sendMessage(it) }

@Deprecated("Use Plugin.logger().i(...)")
fun logSuccess(message: Any?) =
logWithFallback("<green>$message") { Bukkit.getConsoleSender().sendMessage(it) }

@Deprecated("Use Plugin.logger().e(...)")
fun logError(message: Any?) =
logWithFallback(message) { Bukkit.getLogger().severe(it.toPlainText()) }

@Deprecated("Use Plugin.logger().w(...)")
fun logWarn(message: Any?) =
logWithFallback(message) { Bukkit.getLogger().warning(it.toPlainText()) }

/** Broadcasts a message to the entire server. */
fun broadcast(message: Any?) = logWithFallback(message) { Bukkit.getServer().broadcast(it) }

fun CommandSender.info(message: Any?) = logWithFallback(message, printBukkit = ::sendMessage)

fun CommandSender.error(message: Any?) {
if (this is ConsoleCommandSender)
logWithFallback(message) { Bukkit.getLogger().severe(it.toPlainText()) }
else info("$ERROR_PREFIX $message")
}

fun CommandSender.success(message: Any?) {
if (this is ConsoleCommandSender)
logWithFallback("<green>$message") { Bukkit.getConsoleSender().sendMessage(it) }
else info("$SUCCESS_PREFIX $message")
}

fun CommandSender.warn(message: Any?) {
if (this is ConsoleCommandSender)
logWithFallback(message) { Bukkit.getLogger().warning(it.toPlainText()) }
else info("$WARN_PREFIX $message")
}

/**
* (Kotlin) Logs a value with an optional string in front of it e.x.
*
* ```
* val length: Int = "A String".logVal("Name").length.logVal("Its length")
* ```
* Will print:
* ```
* Name: A String
* Its length: 8
* ```
*
* @param message A string to be placed in front of this value.
* @return Itself.
*/
fun <T> T.logVal(message: String = ""): T = logWithFallback(
"${if (message == "") "" else "$message: "}$this",
printBukkit = Bukkit.getConsoleSender()::sendMessage
).let { this }

/**
* Same as [logVal] but uses [broadcast] instead
*/
fun <T> T.broadcastVal(message: String = ""): T = broadcast("$message$this").let { this }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mineinabyss.idofront.messaging

import com.mineinabyss.idofront.di.DI

val idofrontLogger by DI.scoped("Idofront").observeLogger()
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.mineinabyss.idofront.messaging

import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Severity
import com.mineinabyss.idofront.textcomponents.toPlainText
import net.kyori.adventure.text.ComponentLike
import org.bukkit.Bukkit
import org.bukkit.plugin.Plugin
import java.util.logging.Level

class KermitPaperWriter(private val plugin: Plugin) : LogWriter() {
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
plugin.logger.log(severityToLogLevel(severity), message)
throwable?.printStackTrace()
}

fun log(severity: Severity, message: ComponentLike) {
if (severity >= Severity.Warn)
log(severity, message.asComponent().toPlainText(), "", null)
else
Bukkit.getConsoleSender().sendMessage(message)
}

companion object {
// Spigot passes the java log level into log4j that's harder to configure, we'll just stick to info level
// and filter on our end
fun severityToLogLevel(severity: Severity): Level = when (severity) {
Severity.Verbose -> Level.INFO
Severity.Debug -> Level.INFO
Severity.Info -> Level.INFO
Severity.Warn -> Level.WARNING
Severity.Error -> Level.SEVERE
Severity.Assert -> Level.SEVERE
}
}
}
Loading

0 comments on commit 915f61c

Please sign in to comment.