diff --git a/.agents/rules/coding-guide.md b/.agents/rules/coding-guide.md new file mode 100644 index 00000000..557ad7a9 --- /dev/null +++ b/.agents/rules/coding-guide.md @@ -0,0 +1,5 @@ +--- +trigger: always_on +--- + +Make sure `./gradlew build` compiles before you say you're done. \ No newline at end of file diff --git a/planet/src/main/kotlin/ext/Spring.kt b/planet/src/main/kotlin/ext/Spring.kt index 3048ba62..cd53d667 100644 --- a/planet/src/main/kotlin/ext/Spring.kt +++ b/planet/src/main/kotlin/ext/Spring.kt @@ -32,10 +32,7 @@ fun HttpServletResponse.details() = mapOf( // HTTP operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase) -operator fun Int.minus(message: String): Nothing { - ApiException.log.info("> Error $this: $message") - throw ApiException(this, message) -} + fun parsing(block: () -> R) = try { block() } catch (e: ApiException) { throw e } catch (e: Exception) { 400 - e.message.toString() } diff --git a/planet/src/main/kotlin/icu/samnyan/aqua/net/utils/ErrorResponse.kt b/planet/src/main/kotlin/icu/samnyan/aqua/net/utils/ErrorResponse.kt index 7c87b979..b421b5d2 100644 --- a/planet/src/main/kotlin/icu/samnyan/aqua/net/utils/ErrorResponse.kt +++ b/planet/src/main/kotlin/icu/samnyan/aqua/net/utils/ErrorResponse.kt @@ -1,29 +1,21 @@ -package icu.samnyan.aqua.net.utils - -import ext.Str -import org.slf4j.LoggerFactory -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.ControllerAdvice -import org.springframework.web.bind.annotation.ExceptionHandler - - -val SUCCESS = ResponseEntity.ok().body(mapOf("status" to "ok")) - -class ApiException(val code: Int, message: Str) : RuntimeException(message) { - companion object { - val log = LoggerFactory.getLogger(ApiException::class.java) - } - - fun resp() = ResponseEntity.status(code).body(message.toString()) -} - -fun Exception.simpleDescribe(): String = if (this is ApiException) "E${code}" else javaClass.simpleName - -@ControllerAdvice(basePackages = ["icu.samnyan"]) -class GlobalExceptionHandler { - @ExceptionHandler(ApiException::class) - fun handleCustomApiException(e: ApiException): ResponseEntity { - // On error, return the error code and message - return e.resp() - } +package icu.samnyan.aqua.net.utils + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler + + +val SUCCESS: ResponseEntity> = ResponseEntity.ok().body(mapOf("status" to "ok")) + +fun ApiException.resp(): ResponseEntity = ResponseEntity.status(code).body(message.toString()) + +fun Exception.simpleDescribe(): String = if (this is ApiException) "E${code}" else javaClass.simpleName + +@ControllerAdvice(basePackages = ["icu.samnyan"]) +class GlobalExceptionHandler { + @ExceptionHandler(ApiException::class) + fun handleCustomApiException(e: ApiException): ResponseEntity { + // On error, return the error code and message + return e.resp() + } } \ No newline at end of file diff --git a/planet/src/test/kotlin/ext/TestExt.kt b/planet/src/test/kotlin/ext/TestExt.kt index aba1fc92..57847332 100644 --- a/planet/src/test/kotlin/ext/TestExt.kt +++ b/planet/src/test/kotlin/ext/TestExt.kt @@ -4,6 +4,8 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import kotlin.random.Random import kotlin.random.nextInt +import ext.ensureEndingSlash +import ext.jsonMap const val BOARD_ID = "ACAE-01A99999999" const val FULL_CLIENT_ID = "A123-45678909999" diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 97323024..d3f80072 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -9,4 +9,11 @@ dependencies { api("jakarta.servlet:jakarta.servlet-api:6.0.0") api("com.fasterxml.jackson.core:jackson-annotations:2.17.0") api("com.fasterxml.jackson.core:jackson-databind:2.17.0") + api("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0") + + // Core libraries + api("org.slf4j:slf4j-api:2.0.12") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + api("org.jetbrains.kotlin:kotlin-reflect:2.1.10") } diff --git a/planet/src/main/kotlin/ext/Ext.kt b/shared/src/main/kotlin/ext/Ext.kt similarity index 94% rename from planet/src/main/kotlin/ext/Ext.kt rename to shared/src/main/kotlin/ext/Ext.kt index fe233fa2..a7542455 100644 --- a/planet/src/main/kotlin/ext/Ext.kt +++ b/shared/src/main/kotlin/ext/Ext.kt @@ -2,6 +2,7 @@ package ext +import icu.samnyan.aqua.net.utils.ApiException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory @@ -37,12 +38,12 @@ val emailRegex = "^(?=.{1,64}@)[\\p{L}0-9_-]+(\\.[\\p{L}0-9_-]+)*@[^-][\\p{L}0-9 fun Str.isValidEmail(): Bool = emailRegex.matches(this) // Class resource -object Ext { val log = logger() } +object Ext fun res(name: Str) = Ext::class.java.getResourceAsStream(name) fun resStr(name: Str) = res(name)?.reader()?.readText() inline fun resJson(name: Str, warn: Boolean = true) = resStr(name)?.let { JSON.decodeFromString(it) -} ?: run { if (warn) Ext.log.warn("Resource $name is not found"); null } +} ?: run { if (warn) ApiException.log.warn("Resource $name is not found"); null } // Encodings fun Long.toHex(len: Int = 16): Str = "0x${this.toString(len).padStart(len, '0').uppercase()}" @@ -56,6 +57,10 @@ fun Any.long() = when (this) { is String -> toLong() else -> 400 - "Invalid number: $this" } +operator fun Int.minus(message: String): Nothing { + ApiException.log.info("> Error $this: $message") + throw ApiException(this, message) +} fun Any.uint32() = long() and 0xFFFFFFFF fun Any.int() = long().toInt() val Any.long get() = long() @@ -120,17 +125,19 @@ val Str.some get() = ifBlank { null } val ByteArray.hexStr get() = toHexString() operator fun StringBuilder.plusAssign(other: String) { this.append(other) } -// Coroutine -suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } +// Coroutine-lite fun thread(block: () -> T) = Thread { block() }.apply { start() } fun Lock.maybeLock(block: () -> T) = if (tryLock()) try { block() } finally { unlock() } else null +// Coroutine +suspend fun async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() } + // Paths fun path(part1: Str, vararg parts: Str) = Path.of(part1, *parts) fun Str.path() = Path.of(this) operator fun Path.div(part: Str) = resolve(part) operator fun File.div(fileName: Str) = File(this, fileName) -fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/" +fun String.ensureEndingSlash() = if (endsWith('/')) this else "$this/" fun Str.ensureNoEndingSlash() = if (endsWith('/')) dropLast(1) else this fun T.logger() = LoggerFactory.getLogger(this::class.java) diff --git a/planet/src/main/kotlin/ext/Http.kt b/shared/src/main/kotlin/ext/Http.kt similarity index 100% rename from planet/src/main/kotlin/ext/Http.kt rename to shared/src/main/kotlin/ext/Http.kt diff --git a/planet/src/main/kotlin/ext/Jackson.kt b/shared/src/main/kotlin/ext/Jackson.kt similarity index 88% rename from planet/src/main/kotlin/ext/Jackson.kt rename to shared/src/main/kotlin/ext/Jackson.kt index 8ef2c452..99f69c4a 100644 --- a/planet/src/main/kotlin/ext/Jackson.kt +++ b/shared/src/main/kotlin/ext/Jackson.kt @@ -56,7 +56,7 @@ catch (e: Exception) { throw e } -fun String.jsonMap(): Map = json() ?: emptyMap() -fun String.jsonArray(): List> = json() ?: emptyList() -fun String.jsonMaybeMap(): Map? = json() -fun String.jsonMaybeArray(): List>? = json() +fun String.jsonMap(): Map = json>() ?: emptyMap() +fun String.jsonArray(): List> = json>>() ?: emptyList() +fun String.jsonMaybeMap(): Map? = json>() +fun String.jsonMaybeArray(): List>? = json>>() diff --git a/planet/src/main/kotlin/ext/Json.kt b/shared/src/main/kotlin/ext/Json.kt similarity index 100% rename from planet/src/main/kotlin/ext/Json.kt rename to shared/src/main/kotlin/ext/Json.kt diff --git a/planet/src/main/kotlin/ext/Reflect.kt b/shared/src/main/kotlin/ext/Reflect.kt similarity index 100% rename from planet/src/main/kotlin/ext/Reflect.kt rename to shared/src/main/kotlin/ext/Reflect.kt diff --git a/planet/src/main/kotlin/ext/Time.kt b/shared/src/main/kotlin/ext/Time.kt similarity index 100% rename from planet/src/main/kotlin/ext/Time.kt rename to shared/src/main/kotlin/ext/Time.kt diff --git a/shared/src/main/kotlin/icu/samnyan/aqua/net/utils/ApiException.kt b/shared/src/main/kotlin/icu/samnyan/aqua/net/utils/ApiException.kt new file mode 100644 index 00000000..891ea584 --- /dev/null +++ b/shared/src/main/kotlin/icu/samnyan/aqua/net/utils/ApiException.kt @@ -0,0 +1,10 @@ +package icu.samnyan.aqua.net.utils + +import ext.Str +import org.slf4j.LoggerFactory + +class ApiException(val code: Int, message: Str) : RuntimeException(message) { + companion object { + val log = LoggerFactory.getLogger(ApiException::class.java) + } +} diff --git a/planet/src/main/kotlin/icu/samnyan/aqua/sega/util/ZLib.kt b/shared/src/main/kotlin/icu/samnyan/aqua/sega/util/ZLib.kt similarity index 100% rename from planet/src/main/kotlin/icu/samnyan/aqua/sega/util/ZLib.kt rename to shared/src/main/kotlin/icu/samnyan/aqua/sega/util/ZLib.kt