diff --git a/build.gradle.kts b/build.gradle.kts index be535aa924..dbd98975e8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ plugins { id("org.jetbrains.dokka") version("1.8.20") id("com.github.jk1.dependency-license-report") version("2.5") id("org.jetbrains.kotlinx.binary-compatibility-validator") version("0.13.2") - id("org.graalvm.buildtools.native") version("0.9.24") apply(false) + id("org.graalvm.buildtools.native") version("0.9.25") apply(false) id("io.gitlab.arturbosch.detekt") version("1.23.1") apply(false) id("me.champeau.jmh") version("0.7.1") apply(false) } @@ -153,6 +153,7 @@ apiValidation { // Experimental modules "rest", "rest_tools", +// "serverless", "web", ) ) diff --git a/core/api/core.api b/core/api/core.api index bbccc64f2d..0e1d25e965 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -263,11 +263,11 @@ public abstract interface class com/hexagonkt/core/logging/LoggingPort { public abstract fun setLoggerLevel (Ljava/lang/String;Lcom/hexagonkt/core/logging/LoggingLevel;)V } -public final class com/hexagonkt/core/logging/PrintLogger : com/hexagonkt/core/logging/LoggerPort { +public final class com/hexagonkt/core/logging/SystemLogger : com/hexagonkt/core/logging/LoggerPort { public fun (Ljava/lang/String;)V public final fun component1 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;)Lcom/hexagonkt/core/logging/PrintLogger; - public static synthetic fun copy$default (Lcom/hexagonkt/core/logging/PrintLogger;Ljava/lang/String;ILjava/lang/Object;)Lcom/hexagonkt/core/logging/PrintLogger; + public final fun copy (Ljava/lang/String;)Lcom/hexagonkt/core/logging/SystemLogger; + public static synthetic fun copy$default (Lcom/hexagonkt/core/logging/SystemLogger;Ljava/lang/String;ILjava/lang/Object;)Lcom/hexagonkt/core/logging/SystemLogger; public fun equals (Ljava/lang/Object;)Z public final fun getName ()Ljava/lang/String; public fun hashCode ()I @@ -276,7 +276,7 @@ public final class com/hexagonkt/core/logging/PrintLogger : com/hexagonkt/core/l public fun toString ()Ljava/lang/String; } -public final class com/hexagonkt/core/logging/PrintLoggingAdapter : com/hexagonkt/core/logging/LoggingPort { +public final class com/hexagonkt/core/logging/SystemLoggingAdapter : com/hexagonkt/core/logging/LoggingPort { public fun ()V public fun (Lcom/hexagonkt/core/logging/LoggingLevel;)V public synthetic fun (Lcom/hexagonkt/core/logging/LoggingLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/core/src/main/kotlin/com/hexagonkt/core/logging/LoggingManager.kt b/core/src/main/kotlin/com/hexagonkt/core/logging/LoggingManager.kt index 902c7fcaef..9532e6b604 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/logging/LoggingManager.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/logging/LoggingManager.kt @@ -3,11 +3,11 @@ package com.hexagonkt.core.logging import kotlin.reflect.KClass /** - * Manages Logs using [PrintLoggingAdapter] + * Manages Logs using [SystemLoggingAdapter] */ object LoggingManager { var useColor: Boolean = true - var adapter: LoggingPort = PrintLoggingAdapter() + var adapter: LoggingPort = SystemLoggingAdapter() var defaultLoggerName: String = "com.hexagonkt.core.logging" set(value) { require(value.isNotEmpty()) { "Default logger name cannot be empty string" } diff --git a/core/src/main/kotlin/com/hexagonkt/core/logging/PrintLogger.kt b/core/src/main/kotlin/com/hexagonkt/core/logging/PrintLogger.kt deleted file mode 100644 index 9b80e0072d..0000000000 --- a/core/src/main/kotlin/com/hexagonkt/core/logging/PrintLogger.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.hexagonkt.core.logging - -import com.hexagonkt.core.toText - -data class PrintLogger(val name: String) : LoggerPort { - - override fun log(level: LoggingLevel, exception: E, message: (E) -> Any?) { - if (LoggingManager.isLoggerLevelEnabled(name, level)) - println("$level $name - ${message(exception)}:\n${exception.toText()}") - } - - override fun log(level: LoggingLevel, message: () -> Any?) { - if (LoggingManager.isLoggerLevelEnabled(name, level)) - println("$level $name - ${message()}") - } -} diff --git a/core/src/main/kotlin/com/hexagonkt/core/logging/SystemLogger.kt b/core/src/main/kotlin/com/hexagonkt/core/logging/SystemLogger.kt new file mode 100644 index 0000000000..fcff7910a9 --- /dev/null +++ b/core/src/main/kotlin/com/hexagonkt/core/logging/SystemLogger.kt @@ -0,0 +1,28 @@ +package com.hexagonkt.core.logging + +import com.hexagonkt.core.logging.LoggingLevel.* + +data class SystemLogger(val name: String) : LoggerPort { + + private val logger: System.Logger = System.getLogger(name) + + override fun log(level: LoggingLevel, exception: E, message: (E) -> Any?) { + if (LoggingManager.isLoggerLevelEnabled(name, level)) + logger.log(level(level), message(exception).toString(), exception) + } + + override fun log(level: LoggingLevel, message: () -> Any?) { + if (LoggingManager.isLoggerLevelEnabled(name, level)) + logger.log(level(level), message()) + } + + private fun level(level: LoggingLevel): System.Logger.Level = + when (level) { + TRACE -> System.Logger.Level.TRACE + DEBUG -> System.Logger.Level.DEBUG + INFO -> System.Logger.Level.INFO + WARN -> System.Logger.Level.WARNING + ERROR -> System.Logger.Level.ERROR + OFF -> System.Logger.Level.OFF + } +} diff --git a/core/src/main/kotlin/com/hexagonkt/core/logging/PrintLoggingAdapter.kt b/core/src/main/kotlin/com/hexagonkt/core/logging/SystemLoggingAdapter.kt similarity index 78% rename from core/src/main/kotlin/com/hexagonkt/core/logging/PrintLoggingAdapter.kt rename to core/src/main/kotlin/com/hexagonkt/core/logging/SystemLoggingAdapter.kt index 0d17250f4e..ab9ea645ae 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/logging/PrintLoggingAdapter.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/logging/SystemLoggingAdapter.kt @@ -3,14 +3,12 @@ package com.hexagonkt.core.logging import com.hexagonkt.core.logging.LoggingLevel.INFO import com.hexagonkt.core.require -// TODO Wrap these loggers using System.Logger: -// https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/System.Logger.html -class PrintLoggingAdapter(defaultLevel: LoggingLevel = INFO) : LoggingPort { +class SystemLoggingAdapter(defaultLevel: LoggingLevel = INFO) : LoggingPort { private val loggerLevels: MutableMap = mutableMapOf("" to defaultLevel) override fun createLogger(name: String): LoggerPort = - PrintLogger(name) + SystemLogger(name) override fun setLoggerLevel(name: String, level: LoggingLevel) { loggerLevels[name] = level diff --git a/core/src/test/kotlin/com/hexagonkt/core/logging/LoggingManagerTest.kt b/core/src/test/kotlin/com/hexagonkt/core/logging/LoggingManagerTest.kt index 5553a857df..1d6e541cf9 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/logging/LoggingManagerTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/logging/LoggingManagerTest.kt @@ -17,7 +17,7 @@ internal class LoggingManagerTest { // TODO Repeat this test on other logging adapters @Test fun `Loggers are enabled and disabled at runtime`() { - LoggingManager.adapter = PrintLoggingAdapter() + LoggingManager.adapter = SystemLoggingAdapter() val allLevels = LoggingLevel.values() val ch = Logger("com.hx") diff --git a/gradle.properties b/gradle.properties index 5fce4fc953..c5050d5bf3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.warning.mode=all org.gradle.console=plain # Gradle -version=3.0.3 +version=3.0.4 group=com.hexagonkt description=The atoms of your platform @@ -37,12 +37,12 @@ mockkVersion=1.13.7 junitVersion=5.10.0 gatlingVersion=3.9.5 jmhVersion=1.37 -mkdocsMaterialVersion=9.2.4 +mkdocsMaterialVersion=9.2.6 mermaidDokkaVersion=0.4.4 -nativeToolsVersion=0.9.24 +nativeToolsVersion=0.9.25 # http_server_netty -nettyVersion=4.1.96.Final +nettyVersion=4.1.97.Final nettyTcNativeVersion=2.0.61.Final # http_server_nima @@ -50,10 +50,11 @@ nimaVersion=4.0.0-M1 # http_server_servlet servletVersion=6.0.0 +#jettyVersion=12.0.1 Failing with HTTP client gzip encoding jettyVersion=12.0.0 # rest_tools -vertxVersion=4.4.4 +vertxVersion=4.4.5 swaggerParserVersion=2.1.16 # logging @@ -62,7 +63,7 @@ logbackVersion=1.4.11 # serialization jacksonVersion=2.15.2 -dslJsonVersion=2.0.1 +dslJsonVersion=2.0.2 # templates_freemarker freemarkerVersion=2.3.32 diff --git a/gradle/application.gradle b/gradle/application.gradle index 3e698c560b..7f65e82c79 100644 --- a/gradle/application.gradle +++ b/gradle/application.gradle @@ -66,7 +66,7 @@ tasks.register("jpackage") { final String options = findProperty("options") final String icon = findProperty("icon") - final String modules = findProperty("modules") ?: "java.logging" + final String modules = findProperty("modules") final String jarAllName = "$name-all-${version}.jar" final java.nio.file.Path jarAll = buildDir.resolve("libs/$jarAllName") final java.nio.file.Path jpackageJar = buildDir.resolve("jpackage/$jarAllName") @@ -78,10 +78,12 @@ tasks.register("jpackage") { "--description", project.description ?: name, "--name", name, "--input", tmp.absolutePath, - "--add-modules", modules, "--main-jar", jarAllName ] + if (modules != null) + command += [ "--add-modules", modules ] + if (options != null) command += [ "--java-options", options ] diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index 9094227b9d..6ad888ac56 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -11,7 +11,7 @@ apply(plugin: "maven-publish") defaultTasks("build") java { - sourceCompatibility = JavaVersion.toVersion(findProperty("jvmTarget") ?: "11") + sourceCompatibility = JavaVersion.toVersion(findProperty("jvmTarget") ?: "17") targetCompatibility = sourceCompatibility } diff --git a/handlers/src/main/kotlin/com/hexagonkt/handlers/OnHandler.kt b/handlers/src/main/kotlin/com/hexagonkt/handlers/OnHandler.kt index 4466f535e5..9cefea0e1d 100644 --- a/handlers/src/main/kotlin/com/hexagonkt/handlers/OnHandler.kt +++ b/handlers/src/main/kotlin/com/hexagonkt/handlers/OnHandler.kt @@ -7,7 +7,11 @@ data class OnHandler( override fun process(context: Context): Context = try { - callback(context).with(handled = true).next() + val callbackContext = callback(context) + if (callbackContext.handled) + callbackContext.next() + else + callbackContext.with(handled = true).next() } catch (e: Exception) { context.with(exception = e).next() diff --git a/http/http_client/api/http_client.api b/http/http_client/api/http_client.api index 5737fb826f..ce12764cf8 100644 --- a/http/http_client/api/http_client.api +++ b/http/http_client/api/http_client.api @@ -3,23 +3,23 @@ public final class com/hexagonkt/http/client/HttpClient : java/io/Closeable { public synthetic fun (Lcom/hexagonkt/http/client/HttpClientPort;Lcom/hexagonkt/http/client/HttpClientSettings;Lcom/hexagonkt/http/handlers/HttpHandler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close ()V public final fun cookiesMap ()Ljava/util/Map; - public final fun delete (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun delete$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; - public final fun get (Ljava/lang/String;Lcom/hexagonkt/http/model/Headers;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun get$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Lcom/hexagonkt/http/model/Headers;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun delete (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun delete$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun get (Ljava/lang/String;Lcom/hexagonkt/http/model/Headers;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun get$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Lcom/hexagonkt/http/model/Headers;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; public final fun getCookies ()Ljava/util/List; public final fun getHandler ()Lcom/hexagonkt/http/handlers/HttpHandler; public final fun getSettings ()Lcom/hexagonkt/http/client/HttpClientSettings; public final fun head (Ljava/lang/String;Lcom/hexagonkt/http/model/Headers;)Lcom/hexagonkt/http/model/HttpResponsePort; public static synthetic fun head$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Lcom/hexagonkt/http/model/Headers;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; - public final fun options (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/Headers;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun options$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/Headers;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; - public final fun patch (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun patch$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; - public final fun post (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun post$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; - public final fun put (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun put$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun options (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/Headers;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun options$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/Headers;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun patch (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun patch$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun post (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun post$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun put (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun put$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; public final fun request (Lkotlin/jvm/functions/Function1;)V public final fun send (Lcom/hexagonkt/http/model/HttpRequest;Ljava/util/Map;)Lcom/hexagonkt/http/model/HttpResponsePort; public static synthetic fun send$default (Lcom/hexagonkt/http/client/HttpClient;Lcom/hexagonkt/http/model/HttpRequest;Ljava/util/Map;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; @@ -29,8 +29,8 @@ public final class com/hexagonkt/http/client/HttpClient : java/io/Closeable { public final fun start ()V public final fun started ()Z public final fun stop ()V - public final fun trace (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;)Lcom/hexagonkt/http/model/HttpResponsePort; - public static synthetic fun trace$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; + public final fun trace (Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;)Lcom/hexagonkt/http/model/HttpResponsePort; + public static synthetic fun trace$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Ljava/lang/Object;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/model/HttpResponsePort; public final fun ws (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lcom/hexagonkt/http/model/ws/WsSession; public static synthetic fun ws$default (Lcom/hexagonkt/http/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/hexagonkt/http/model/ws/WsSession; } @@ -50,19 +50,21 @@ public final class com/hexagonkt/http/client/HttpClientPort$DefaultImpls { public final class com/hexagonkt/http/client/HttpClientSettings { public fun ()V - public fun (Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;Z)V - public synthetic fun (Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;Z)V + public synthetic fun (Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/net/URL; public final fun component2 ()Lcom/hexagonkt/http/model/ContentType; - public final fun component3 ()Z - public final fun component4 ()Lcom/hexagonkt/http/model/Headers; - public final fun component5 ()Z - public final fun component6 ()Lcom/hexagonkt/http/SslSettings; - public final fun component7 ()Lcom/hexagonkt/http/model/Authorization; - public final fun component8 ()Z - public final fun copy (Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;Z)Lcom/hexagonkt/http/client/HttpClientSettings; - public static synthetic fun copy$default (Lcom/hexagonkt/http/client/HttpClientSettings;Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;ZILjava/lang/Object;)Lcom/hexagonkt/http/client/HttpClientSettings; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Z + public final fun component5 ()Lcom/hexagonkt/http/model/Headers; + public final fun component6 ()Z + public final fun component7 ()Lcom/hexagonkt/http/SslSettings; + public final fun component8 ()Lcom/hexagonkt/http/model/Authorization; + public final fun component9 ()Z + public final fun copy (Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;Z)Lcom/hexagonkt/http/client/HttpClientSettings; + public static synthetic fun copy$default (Lcom/hexagonkt/http/client/HttpClientSettings;Ljava/net/URL;Lcom/hexagonkt/http/model/ContentType;Ljava/util/List;ZLcom/hexagonkt/http/model/Headers;ZLcom/hexagonkt/http/SslSettings;Lcom/hexagonkt/http/model/Authorization;ZILjava/lang/Object;)Lcom/hexagonkt/http/client/HttpClientSettings; public fun equals (Ljava/lang/Object;)Z + public final fun getAccept ()Ljava/util/List; public final fun getAuthorization ()Lcom/hexagonkt/http/model/Authorization; public final fun getBaseUrl ()Ljava/net/URL; public final fun getContentType ()Lcom/hexagonkt/http/model/ContentType; diff --git a/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClient.kt b/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClient.kt index c479ea9c8b..1883f8c64b 100644 --- a/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClient.kt +++ b/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClient.kt @@ -94,14 +94,18 @@ class HttpClient( path: String = "", headers: Headers = Headers(), body: Any? = null, - contentType: ContentType? = settings.contentType): HttpResponsePort = + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, + ): HttpResponsePort = send( HttpRequest( method = GET, path = path, body = body ?: "", headers = headers, - contentType = contentType) + contentType = contentType, + accept = accept, + ) ) fun head(path: String = "", headers: Headers = Headers()): HttpResponsePort = @@ -110,36 +114,73 @@ class HttpClient( fun post( path: String = "", body: Any? = null, - contentType: ContentType? = settings.contentType + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(HttpRequest(POST, path = path, body = body ?: "", contentType = contentType)) + send( + HttpRequest( + method = POST, + path = path, + body = body ?: "", + contentType = contentType, + accept = accept, + ) + ) fun put( path: String = "", body: Any? = null, - contentType: ContentType? = settings.contentType + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(HttpRequest(PUT, path = path, body = body ?: "", contentType = contentType)) + send( + HttpRequest( + method = PUT, + path = path, + body = body ?: "", + contentType = contentType, + accept = accept, + ) + ) fun delete( path: String = "", body: Any? = null, - contentType: ContentType? = settings.contentType + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(HttpRequest(DELETE, path = path, body = body ?: "", contentType = contentType)) + send( + HttpRequest( + method = DELETE, + path = path, + body = body ?: "", + contentType = contentType, + accept = accept, + ) + ) fun trace( path: String = "", body: Any? = null, - contentType: ContentType? = settings.contentType + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(HttpRequest(TRACE, path = path, body = body ?: "", contentType = contentType)) + send( + HttpRequest( + method = TRACE, + path = path, + body = body ?: "", + contentType = contentType, + accept = accept, + ) + ) fun options( path: String = "", body: Any? = null, headers: Headers = Headers(), - contentType: ContentType? = settings.contentType + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = send( HttpRequest( @@ -147,16 +188,26 @@ class HttpClient( path = path, body = body ?: "", headers = headers, - contentType = contentType + contentType = contentType, + accept = accept, ) ) fun patch( path: String = "", body: Any? = null, - contentType: ContentType? = settings.contentType + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(HttpRequest(PATCH, path = path, body = body ?: "", contentType = contentType)) + send( + HttpRequest( + method = PATCH, + path = path, + body = body ?: "", + contentType = contentType, + accept = accept, + ) + ) private fun HttpHandler.process( request: HttpRequestPort, attributes: Map diff --git a/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClientSettings.kt b/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClientSettings.kt index 57ca1c6791..f76fd65efd 100644 --- a/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClientSettings.kt +++ b/http/http_client/src/main/kotlin/com/hexagonkt/http/client/HttpClientSettings.kt @@ -8,6 +8,7 @@ import java.net.URL data class HttpClientSettings( val baseUrl: URL? = null, val contentType: ContentType? = null, + val accept: List = emptyList(), val useCookies: Boolean = true, val headers: Headers = Headers(), val insecure: Boolean = false, diff --git a/http/http_client/src/test/kotlin/com/hexagonkt/http/client/HttpClientSettingsTest.kt b/http/http_client/src/test/kotlin/com/hexagonkt/http/client/HttpClientSettingsTest.kt index 051da6f935..b5d471ee67 100644 --- a/http/http_client/src/test/kotlin/com/hexagonkt/http/client/HttpClientSettingsTest.kt +++ b/http/http_client/src/test/kotlin/com/hexagonkt/http/client/HttpClientSettingsTest.kt @@ -13,6 +13,7 @@ internal class HttpClientSettingsTest { HttpClientSettings().let { assertNull(it.baseUrl) assertNull(it.contentType) + assertEquals(emptyList(), it.accept) assertTrue(it.useCookies) assertEquals(Headers(), it.headers) assertFalse(it.insecure) diff --git a/http/http_client_jetty/src/main/kotlin/com/hexagonkt/http/client/jetty/JettyClientAdapter.kt b/http/http_client_jetty/src/main/kotlin/com/hexagonkt/http/client/jetty/JettyClientAdapter.kt index 1e65b9c2fc..2c5cd95420 100644 --- a/http/http_client_jetty/src/main/kotlin/com/hexagonkt/http/client/jetty/JettyClientAdapter.kt +++ b/http/http_client_jetty/src/main/kotlin/com/hexagonkt/http/client/jetty/JettyClientAdapter.kt @@ -172,6 +172,7 @@ open class JettyClientAdapter : HttpClientPort { val settings = adapterHttpClient.settings val contentType = request.contentType ?: settings.contentType + val accept = request.accept.ifEmpty(settings::accept) val authorization = request.authorization ?: settings.authorization val baseUrl = settings.baseUrl @@ -193,7 +194,7 @@ open class JettyClientAdapter : HttpClientPort { .forEach { (k, v) -> it.put(k, v.map(Any::toString)) } } .body(createBody(request)) - .accept(*request.accept.map { it.text }.toTypedArray()) + .accept(*accept.map { it.text }.toTypedArray()) request.queryParameters .forEach { (k, v) -> v.strings().forEach { jettyRequest.param(k, it) } } diff --git a/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/HttpPredicate.kt b/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/HttpPredicate.kt index bebbd71ed4..83ed2934f9 100644 --- a/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/HttpPredicate.kt +++ b/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/HttpPredicate.kt @@ -17,7 +17,9 @@ data class HttpPredicate( val status: HttpStatus? = null, ) : (Context) -> Boolean { - private val logger: Logger = Logger(HttpPredicate::class) + private companion object { + val logger: Logger = Logger(HttpPredicate::class) + } private fun PathPattern.isEmpty(): Boolean = pattern.isEmpty() diff --git a/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/PathHandler.kt b/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/PathHandler.kt index a10e61a69a..db6bbab023 100644 --- a/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/PathHandler.kt +++ b/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/PathHandler.kt @@ -20,9 +20,8 @@ data class PathHandler( ) { - private val logger: Logger = Logger(PathHandler::class) - private companion object { + val logger: Logger = Logger(PathHandler::class) fun nestedMethods(handlers: List): Set = handlers .flatMap { it.handlerPredicate.methods.ifEmpty { ALL } } diff --git a/http/http_server/api/http_server.api b/http/http_server/api/http_server.api index 1c28b8ff96..095b8878fc 100644 --- a/http/http_server/api/http_server.api +++ b/http/http_server/api/http_server.api @@ -10,6 +10,7 @@ public final class com/hexagonkt/http/server/HttpServer : java/io/Closeable { public final fun copy (Lcom/hexagonkt/http/server/HttpServerPort;Lcom/hexagonkt/http/handlers/HttpHandler;Lcom/hexagonkt/http/server/HttpServerSettings;)Lcom/hexagonkt/http/server/HttpServer; public static synthetic fun copy$default (Lcom/hexagonkt/http/server/HttpServer;Lcom/hexagonkt/http/server/HttpServerPort;Lcom/hexagonkt/http/handlers/HttpHandler;Lcom/hexagonkt/http/server/HttpServerSettings;ILjava/lang/Object;)Lcom/hexagonkt/http/server/HttpServer; public fun equals (Ljava/lang/Object;)Z + public final fun getBinding ()Ljava/net/URL; public final fun getHandler ()Lcom/hexagonkt/http/handlers/HttpHandler; public final fun getPortName ()Ljava/lang/String; public final fun getRuntimePort ()I diff --git a/http/http_server/src/main/kotlin/com/hexagonkt/http/server/HttpServer.kt b/http/http_server/src/main/kotlin/com/hexagonkt/http/server/HttpServer.kt index f78e872177..c9602ab4cb 100644 --- a/http/http_server/src/main/kotlin/com/hexagonkt/http/server/HttpServer.kt +++ b/http/http_server/src/main/kotlin/com/hexagonkt/http/server/HttpServer.kt @@ -21,12 +21,14 @@ import com.hexagonkt.core.Jvm.timeZone import com.hexagonkt.core.Jvm.totalMemory import com.hexagonkt.core.Jvm.usedMemory import com.hexagonkt.core.prependIndent +import com.hexagonkt.core.urlOf import com.hexagonkt.http.server.HttpServerFeature.ZIP import com.hexagonkt.http.handlers.HttpHandler import com.hexagonkt.http.handlers.HandlerBuilder import com.hexagonkt.http.handlers.path import java.io.Closeable import java.lang.System.nanoTime +import java.net.URL /** * Server that listen to HTTP connections on a port and address and route requests to handlers. @@ -38,6 +40,8 @@ data class HttpServer( ) : Closeable { companion object { + private val logger: Logger = Logger(this::class) + val banner: String = """ $CYAN _________ $CYAN / \ @@ -54,8 +58,6 @@ data class HttpServer( """.trimIndent() } - private val logger: Logger = Logger(this::class) - /** * Create a server with a builder ([HandlerBuilder]) to set up handlers. * @@ -94,9 +96,17 @@ data class HttpServer( * * @exception IllegalStateException Throw an exception if the server hasn't been started. */ - val runtimePort + val runtimePort: Int get() = if (started()) adapter.runtimePort() else error("Server is not running") + /** + * Runtime binding of the server. + * + * @exception IllegalStateException Throw an exception if the server hasn't been started. + */ + val binding: URL + get() = urlOf("${settings.bindUrl}:$runtimePort") + /** * The port name of the server. */ @@ -142,7 +152,6 @@ data class HttpServer( val startUpTime = "%,.0f".format(startUpTimestamp / 1e6) val protocol = settings.protocol - val binding = "${settings.bindUrl}:$runtimePort" val banner = settings.banner ?: return " at $binding ($startUpTime ms)" val jvmMemoryValue = "$BLUE${totalMemory()} KB$RESET" diff --git a/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/FileCallback.kt b/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/FileCallback.kt index d33d111c38..f42f466fca 100644 --- a/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/FileCallback.kt +++ b/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/FileCallback.kt @@ -18,7 +18,9 @@ import java.io.File * @param file Base file used to resolve paths passed on the request. */ class FileCallback(private val file: File) : (HttpContext) -> HttpContext { - private val logger: Logger = Logger(FileCallback::class) + private companion object { + val logger: Logger = Logger(FileCallback::class) + } override fun invoke(context: HttpContext): HttpContext { val file = when (context.pathParameters.size) { diff --git a/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/UrlCallback.kt b/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/UrlCallback.kt index 4024688a64..167a56d5d6 100644 --- a/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/UrlCallback.kt +++ b/http/http_server/src/main/kotlin/com/hexagonkt/http/server/callbacks/UrlCallback.kt @@ -10,7 +10,9 @@ import com.hexagonkt.http.handlers.HttpContext import java.net.URL class UrlCallback(private val url: URL) : (HttpContext) -> HttpContext { - private val logger: Logger = Logger(UrlCallback::class) + private companion object { + val logger: Logger = Logger(UrlCallback::class) + } override fun invoke(context: HttpContext): HttpContext { val requestPath = when (context.pathParameters.size) { diff --git a/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletFilter.kt b/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletFilter.kt index 338e55ed41..13f1263636 100644 --- a/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletFilter.kt +++ b/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletFilter.kt @@ -15,7 +15,9 @@ import jakarta.servlet.http.HttpServletResponse class ServletFilter(pathHandler: HttpHandler) : HttpFilter() { - private val logger: Logger = Logger(ServletFilter::class) + private companion object { + val logger: Logger = Logger(ServletFilter::class) + } private val handlers: Map = pathHandler.byMethod().mapKeys { it.key.toString() } diff --git a/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletServer.kt b/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletServer.kt index 11c9671ea2..c760b6546c 100644 --- a/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletServer.kt +++ b/http/http_server_servlet/src/main/kotlin/com/hexagonkt/http/server/servlet/ServletServer.kt @@ -27,7 +27,9 @@ abstract class ServletServer( private val settings: HttpServerSettings = HttpServerSettings(), ) : ServletContextListener { - private val logger: Logger = Logger(ServletServer::class) + private companion object { + val logger: Logger = Logger(ServletServer::class) + } private val pathHandler: PathHandler = path(settings.contextPath, handlers) diff --git a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/ClientTest.kt b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/ClientTest.kt index 7a5c33e62d..24581f768a 100644 --- a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/ClientTest.kt +++ b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/ClientTest.kt @@ -97,8 +97,7 @@ abstract class ClientTest( assertEquals(FOUND_302, response.status) assertEquals("/foo?ok", response.headers["location"]?.value) - val baseUrl = urlOf("http://localhost:${server.runtimePort}") - val settings = HttpClientSettings(baseUrl, followRedirects = true) + val settings = HttpClientSettings(server.binding, followRedirects = true) val redirectClient = HttpClient(clientAdapter(), settings).apply { start() } val redirectedResponse = redirectClient.get() @@ -301,7 +300,7 @@ abstract class ClientTest( @Test fun `Parameters are set properly` () { val clientHeaders = Headers(Header("header1", "val1", "val2")) val settings = HttpClientSettings( - baseUrl = urlOf("http://localhost:${server.runtimePort}"), + baseUrl = server.binding, contentType = ContentType(APPLICATION_JSON), useCookies = false, headers = clientHeaders, @@ -425,10 +424,7 @@ abstract class ClientTest( } // We'll use the same certificate for the client (in a real scenario it would be different) - val clientSettings = HttpClientSettings( - baseUrl = urlOf("https://localhost:${server.runtimePort}"), - sslSettings = sslSettings - ) + val clientSettings = HttpClientSettings(baseUrl = server.binding, sslSettings = sslSettings) // Create an HTTP client and make an HTTPS request val client = HttpClient(clientAdapter(), clientSettings) diff --git a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/FiltersTest.kt b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/FiltersTest.kt index 8b51223f36..4bae3489c1 100644 --- a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/FiltersTest.kt +++ b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/FiltersTest.kt @@ -1,7 +1,6 @@ package com.hexagonkt.http.test.examples import com.hexagonkt.core.decodeBase64 -import com.hexagonkt.core.urlOf import com.hexagonkt.http.client.HttpClient import com.hexagonkt.http.client.HttpClientPort import com.hexagonkt.http.client.HttpClientSettings @@ -132,7 +131,7 @@ abstract class FiltersTest( private fun authorizedClient(user: String, password: String): HttpClient { val settings = HttpClientSettings( - baseUrl = urlOf("http://localhost:${server.runtimePort}"), + baseUrl = server.binding, headers = Headers(Header("authorization", basicAuth(user, password))) ) return HttpClient(clientAdapter(), settings).apply { start() } diff --git a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/HttpsTest.kt b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/HttpsTest.kt index b35f953a9c..2388599c3c 100644 --- a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/HttpsTest.kt +++ b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/HttpsTest.kt @@ -105,8 +105,7 @@ abstract class HttpsTest( val clientSettings = HttpClientSettings(sslSettings = sslSettings) // Create an HTTP client and make an HTTPS request - val contextPath = urlOf("https://localhost:${server.runtimePort}") - val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = contextPath)) + val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = server.binding)) client.start() client.get("/hello").apply { // Assure the certificate received (and returned) by the server is correct @@ -123,8 +122,7 @@ abstract class HttpsTest( val server = serve(serverAdapter(), handler, http2ServerSettings.copy(protocol = HTTPS)) - val contextPath = urlOf("https://localhost:${server.runtimePort}") - val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = contextPath)) + val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = server.binding)) client.start() client.get("/hello").apply { assert(headers.require("cert").string()?.startsWith("CN=hexagonkt.com") ?: false) @@ -139,8 +137,7 @@ abstract class HttpsTest( val server = serve(serverAdapter(), handler, http2ServerSettings) - val contextPath = urlOf("https://localhost:${server.runtimePort}") - val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = contextPath)) + val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = server.binding)) client.start() client.get("/hello").apply { assert(headers.require("cert").string()?.startsWith("CN=hexagonkt.com") ?: false) @@ -188,7 +185,7 @@ abstract class HttpsTest( ) // Create an HTTP client and make an HTTPS request - val contextPath = urlOf("https://localhost:${server.runtimePort}") + val contextPath = server.binding val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = contextPath)) client.start() client.get("/hello").apply { diff --git a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/SamplesTest.kt b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/SamplesTest.kt index 3117ac8fc3..c67fb1dd66 100644 --- a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/SamplesTest.kt +++ b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/SamplesTest.kt @@ -73,7 +73,7 @@ abstract class SamplesTest( // Servers implement closeable, you can use them inside a block assuring they will be closed runningServer.use { s -> - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assert(s.started()) assertEquals("Hello World!", it.get("/context/hello").body) @@ -87,7 +87,7 @@ abstract class SamplesTest( // serverCreation defaultSettingsServer.use { s -> - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assert(s.started()) assertEquals("Hello World!", it.get("/hello").body) @@ -109,7 +109,7 @@ abstract class SamplesTest( } server.use { s -> - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assertEquals("Get greeting", it.get("/hello").body) assertEquals("Put greeting", it.put("/hello").body) @@ -136,7 +136,7 @@ abstract class SamplesTest( } server.use { s -> - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assertEquals("Greeting", it.get("/nested/hello").body) assertEquals("Second level greeting", it.get("/nested/secondLevel/hello").body) @@ -161,7 +161,7 @@ abstract class SamplesTest( server.use { s -> s.start() - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${server.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assertEquals("Get client", it.get("/clients").body) @@ -347,7 +347,7 @@ abstract class SamplesTest( server.use { s -> s.start() - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.cookies += Cookie("foo", "bar") it.start() @@ -420,7 +420,7 @@ abstract class SamplesTest( server.use { s -> s.start() - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${server.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assertResponse(it.get("/filters/route"), "filters route", "b-filters", "a-filters") assertResponse(it.get("/filters"), "filters") @@ -473,7 +473,7 @@ abstract class SamplesTest( } server.use { s -> - val settings = HttpClientSettings(urlOf("http://localhost:${s.runtimePort}")) + val settings = HttpClientSettings(s.binding) HttpClient(clientAdapter(), settings).use { it.start() @@ -516,7 +516,7 @@ abstract class SamplesTest( } server.use { s -> - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assert(it.get("/web/file.txt").bodyString().startsWith("It matches this route")) @@ -545,7 +545,7 @@ abstract class SamplesTest( val server = serve(serverAdapter(), router, serverSettings) server.use { s -> - HttpClient(clientAdapter(), HttpClientSettings(urlOf("http://localhost:${s.runtimePort}"))).use { + HttpClient(clientAdapter(), HttpClientSettings(s.binding)).use { it.start() assertEquals("Hi!", it.get("/hello").body) } diff --git a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/WebSocketsTest.kt b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/WebSocketsTest.kt index a548c0dca8..fdd3d1f65e 100644 --- a/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/WebSocketsTest.kt +++ b/http/http_test/src/main/kotlin/com/hexagonkt/http/test/examples/WebSocketsTest.kt @@ -131,26 +131,24 @@ abstract class WebSocketsTest( } @Test fun `Serve WS works properly`() { - wsTest("http", serverSettings.copy(bindPort = 0), clientSettings) + wsTest(serverSettings.copy(bindPort = 0), clientSettings) } @Test fun `Serve WSS works properly`() { - wsTest("https", http2ServerSettings.copy(protocol = HTTPS), clientSettings) + wsTest(http2ServerSettings.copy(protocol = HTTPS), clientSettings) } @Test fun `Serve WSS over HTTP2 works properly`() { - wsTest("https", http2ServerSettings, clientSettings) + wsTest(http2ServerSettings, clientSettings) } private fun wsTest( - protocol: String, serverSettings: HttpServerSettings, clientSettings: HttpClientSettings, ) { val server = serve(serverAdapter(), handler, serverSettings) - val contextPath = urlOf("$protocol://localhost:${server.runtimePort}") - val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = contextPath)) + val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = server.binding)) client.start() // ws_client diff --git a/http/rest/src/main/kotlin/com/hexagonkt/rest/Rest.kt b/http/rest/src/main/kotlin/com/hexagonkt/rest/Rest.kt index fc966cc78f..0dbf8e0265 100644 --- a/http/rest/src/main/kotlin/com/hexagonkt/rest/Rest.kt +++ b/http/rest/src/main/kotlin/com/hexagonkt/rest/Rest.kt @@ -1,24 +1,23 @@ package com.hexagonkt.rest +import com.hexagonkt.core.media.MediaType import com.hexagonkt.http.model.HttpBase -import com.hexagonkt.http.handlers.HttpContext -import com.hexagonkt.serialization.SerializationManager -import com.hexagonkt.serialization.parseList -import com.hexagonkt.serialization.parseMap -import com.hexagonkt.serialization.serialize +import com.hexagonkt.serialization.* fun HttpBase.bodyList(): List<*> = bodyString().parseList(mediaType()) -fun HttpBase.bodyMap(): Map<*, *> = +fun HttpBase.bodyMap(): Map = bodyString().parseMap(mediaType()) -fun HttpBase.mediaType() = - contentType?.mediaType ?: SerializationManager.requireDefaultFormat().mediaType +fun HttpBase.bodyMaps(): List> = + bodyString().parseMaps(mediaType()) -val serializeCallback: (HttpContext) -> HttpContext = { context -> - context.request.contentType?.mediaType - ?.let(SerializationManager::formatOfOrNull) - ?.let { context.request(body = context.request.body.serialize(it)) } - ?: context -} +fun HttpBase.bodyObjects(converter: (Map) -> T): List = + bodyMaps().map(converter) + +fun HttpBase.bodyObject(converter: (Map) -> T): T = + bodyMap().let(converter) + +fun HttpBase.mediaType(): MediaType = + contentType?.mediaType ?: error("Missing content type") diff --git a/http/rest/src/main/kotlin/com/hexagonkt/rest/SerializeRequestCallback.kt b/http/rest/src/main/kotlin/com/hexagonkt/rest/SerializeRequestCallback.kt new file mode 100644 index 0000000000..021a3a79fb --- /dev/null +++ b/http/rest/src/main/kotlin/com/hexagonkt/rest/SerializeRequestCallback.kt @@ -0,0 +1,15 @@ +package com.hexagonkt.rest + +import com.hexagonkt.http.handlers.HttpContext +import com.hexagonkt.serialization.SerializationManager +import com.hexagonkt.serialization.serialize + +class SerializeRequestCallback: (HttpContext) -> HttpContext { + + // TODO Short circuit if body is empty + override fun invoke(context: HttpContext): HttpContext = + context.request.contentType?.mediaType + ?.let(SerializationManager::formatOfOrNull) + ?.let { context.request(body = context.request.body.serialize(it)) } + ?: context +} diff --git a/http/rest/src/main/kotlin/com/hexagonkt/rest/SerializeResponseCallback.kt b/http/rest/src/main/kotlin/com/hexagonkt/rest/SerializeResponseCallback.kt new file mode 100644 index 0000000000..31d7027940 --- /dev/null +++ b/http/rest/src/main/kotlin/com/hexagonkt/rest/SerializeResponseCallback.kt @@ -0,0 +1,22 @@ +package com.hexagonkt.rest + +import com.hexagonkt.http.handlers.HttpContext +import com.hexagonkt.http.model.ContentType +import com.hexagonkt.serialization.SerializationManager +import com.hexagonkt.serialization.serialize + +class SerializeResponseCallback: (HttpContext) -> HttpContext { + + fun HttpContext.accept(): List = + request.accept.ifEmpty { response.contentType?.let(::listOf) ?: emptyList() } + + // TODO Short circuit if body is empty + override fun invoke(context: HttpContext): HttpContext = + context.accept() + .associateWith { SerializationManager.formatOfOrNull(it.mediaType) } + .mapNotNull { (k, v) -> v?.let { k to it } } + .firstOrNull() + ?.let { (ct, sf) -> ct to context.response.body.serialize(sf) } + ?.let { (ct, c) -> context.send(body = c, contentType = ct) } + ?: context +} diff --git a/http/rest/src/test/kotlin/com/hexagonkt/rest/RestTest.kt b/http/rest/src/test/kotlin/com/hexagonkt/rest/RestTest.kt index 7b3dfd56f6..ec486ceae6 100644 --- a/http/rest/src/test/kotlin/com/hexagonkt/rest/RestTest.kt +++ b/http/rest/src/test/kotlin/com/hexagonkt/rest/RestTest.kt @@ -2,24 +2,43 @@ package com.hexagonkt.rest import com.hexagonkt.core.media.APPLICATION_JSON import com.hexagonkt.core.media.APPLICATION_YAML +import com.hexagonkt.core.requireInt import com.hexagonkt.http.model.HttpResponse import com.hexagonkt.http.model.ContentType import com.hexagonkt.http.model.HttpRequest import com.hexagonkt.serialization.SerializationManager import com.hexagonkt.serialization.jackson.json.Json +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +@TestInstance(PER_CLASS) internal class RestTest { + private data class Record( + val a: Int, + val b: Int, + val c: Int, + ) { + constructor(data: Map) : this( + data.requireInt(Record::a), + data.requireInt(Record::b), + data.requireInt(Record::c), + ) + } + + private val json: ContentType = ContentType(APPLICATION_JSON) + + @BeforeAll fun setUp() { + SerializationManager.formats = setOf(Json) + } + @Test fun `Media type is calculated properly`() { - SerializationManager.defaultFormat = null assertFailsWith { HttpRequest().mediaType() } assertFailsWith { HttpResponse().mediaType() } - SerializationManager.defaultFormat = Json - assertEquals(APPLICATION_JSON, HttpRequest().mediaType()) - assertEquals(APPLICATION_JSON, HttpResponse().mediaType()) HttpRequest(contentType = ContentType(APPLICATION_YAML)).apply { assertEquals(APPLICATION_YAML, mediaType()) } @@ -29,26 +48,43 @@ internal class RestTest { } @Test fun `Body is parsed to list`() { - SerializationManager.defaultFormat = Json - HttpRequest(body = """[ "a", "b", "c" ]""").apply { + HttpRequest(body = """[ "a", "b", "c" ]""", contentType = json).apply { assertEquals(APPLICATION_JSON, mediaType()) assertEquals(listOf("a", "b", "c"), bodyList()) } - HttpResponse(body = """[ "a", "b", "c" ]""").apply { + HttpResponse(body = """[ "a", "b", "c" ]""", contentType = json).apply { assertEquals(APPLICATION_JSON, mediaType()) assertEquals(listOf("a", "b", "c"), bodyList()) } } @Test fun `Body is parsed to map`() { - SerializationManager.defaultFormat = Json - HttpRequest(body = """{ "a" : 0, "b" : 1, "c" : 2 }""").apply { + HttpRequest(body = """{ "a" : 0, "b" : 1, "c" : 2 }""", contentType = json).apply { assertEquals(APPLICATION_JSON, mediaType()) assertEquals(mapOf("a" to 0, "b" to 1, "c" to 2), bodyMap()) } - HttpResponse(body = """{ "a" : 0, "b" : 1, "c" : 2 }""").apply { + HttpResponse(body = """{ "a" : 0, "b" : 1, "c" : 2 }""", contentType = json).apply { assertEquals(APPLICATION_JSON, mediaType()) assertEquals(mapOf("a" to 0, "b" to 1, "c" to 2), bodyMap()) } } + + @Test fun `Body is parsed to objects`() { + HttpRequest(body = """{ "a" : 0, "b" : 1, "c" : 2 }""", contentType = json).apply { + assertEquals(APPLICATION_JSON, mediaType()) + assertEquals(Record(mapOf("a" to 0, "b" to 1, "c" to 2)), bodyObject(::Record)) + } + HttpResponse(body = """{ "a" : 0, "b" : 1, "c" : 2 }""", contentType = json).apply { + assertEquals(APPLICATION_JSON, mediaType()) + assertEquals(Record(mapOf("a" to 0, "b" to 1, "c" to 2)), bodyObject(::Record)) + } + HttpRequest(body = """[ { "a" : 0, "b" : 1, "c" : 2 } ]""", contentType = json).apply { + assertEquals(APPLICATION_JSON, mediaType()) + assertEquals(listOf(Record(0, 1, 2)), bodyObjects(::Record)) + } + HttpResponse(body = """[ { "a" : 0, "b" : 1, "c" : 2 } ]""", contentType = json).apply { + assertEquals(APPLICATION_JSON, mediaType()) + assertEquals(listOf(Record(0, 1, 2)), bodyObjects(::Record)) + } + } } diff --git a/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/DynamicServer.kt b/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/DynamicServer.kt index 5f2f804dfa..7fdc792033 100644 --- a/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/DynamicServer.kt +++ b/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/DynamicServer.kt @@ -1,11 +1,13 @@ package com.hexagonkt.http.test +import com.hexagonkt.http.handlers.HandlerBuilder import com.hexagonkt.http.model.NOT_FOUND_404 import com.hexagonkt.http.server.HttpServer import com.hexagonkt.http.server.HttpServerPort -import com.hexagonkt.http.handlers.HttpContext import com.hexagonkt.http.handlers.PathHandler import com.hexagonkt.http.server.HttpServerSettings +import com.hexagonkt.rest.SerializeResponseCallback +import java.net.URL /** * Server with dynamic handler (delegated to [path]). Root handler can be replaced at any time @@ -13,19 +15,25 @@ import com.hexagonkt.http.server.HttpServerSettings */ data class DynamicServer( private val adapter: HttpServerPort, - var path: PathHandler = PathHandler(), private val settings: HttpServerSettings = HttpServerSettings(), + var path: PathHandler = PathHandler(), ) { val runtimePort: Int by lazy { server.runtimePort } + val binding: URL by lazy { server.binding } private val server: HttpServer by lazy { HttpServer(adapter, settings) { + after("*", SerializeResponseCallback()) after(pattern = "*", status = NOT_FOUND_404) { - HttpContext(response = this@DynamicServer.path.process(request).response) + send(response = this@DynamicServer.path.process(request).response) } } } + fun path(block: HandlerBuilder.() -> Unit) { + path = com.hexagonkt.http.handlers.path(block = block) + } + fun start() { server.start() } diff --git a/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/Http.kt b/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/Http.kt index ffe0f0df09..bb02250902 100644 --- a/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/Http.kt +++ b/http/rest_tools/src/main/kotlin/com/hexagonkt/http/test/Http.kt @@ -13,50 +13,28 @@ import com.hexagonkt.http.model.* import com.hexagonkt.http.model.HttpMethod.* import com.hexagonkt.http.model.HttpStatusType.SUCCESS import com.hexagonkt.http.patterns.createPathPattern -import com.hexagonkt.rest.serializeCallback +import com.hexagonkt.rest.SerializeRequestCallback data class Http( val adapter: HttpClientPort, val url: String? = null, - val contentType: ContentType? = null, - val headers: Map = emptyMap(), + val httpContentType: ContentType? = null, + val httpAccept: List = emptyList(), + val httpHeaders: Map = emptyMap(), val sslSettings: SslSettings? = SslSettings(), val handler: HttpHandler? = serializeHandler, ) { companion object { - val serializeHandler: HttpHandler = BeforeHandler("*", serializeCallback) - - fun http( - adapter: HttpClientPort, - url: String? = null, - contentType: ContentType? = null, - headers: Map = emptyMap(), - sslSettings: SslSettings? = SslSettings(), - handler: HttpHandler? = serializeHandler, - block: Http.() -> Unit - ) { - Http(adapter, url, contentType, headers, sslSettings, handler).request(block) - } - - fun http( - adapter: HttpClientPort, - url: String? = null, - mediaType: MediaType, - headers: Map = emptyMap(), - sslSettings: SslSettings? = SslSettings(), - handler: HttpHandler? = serializeHandler, - block: Http.() -> Unit - ) { - Http(adapter, url, mediaType, headers, sslSettings, handler).request(block) - } + val serializeHandler: HttpHandler = BeforeHandler("*", SerializeRequestCallback()) } private val settings = HttpClientSettings( baseUrl = url?.let(::urlOf), - contentType = contentType, + contentType = httpContentType, + accept = httpAccept, useCookies = true, - headers = toHeaders(headers), + headers = toHeaders(httpHeaders), insecure = true, sslSettings = sslSettings, ) @@ -68,15 +46,29 @@ data class Http( val request: HttpRequest get() = lastRequest val attributes: Map get() = lastAttributes val response: HttpResponsePort get() = lastResponse + val status: HttpStatus get() = lastResponse.status + val body: Any get() = lastResponse.body + val cookies: Map get() = lastResponse.cookiesMap() + val headers: Headers get() = lastResponse.headers + val contentType: ContentType? get() = lastResponse.contentType constructor( adapter: HttpClientPort, url: String? = null, mediaType: MediaType, + accept: List = emptyList(), headers: Map = emptyMap(), sslSettings: SslSettings? = SslSettings(), handler: HttpHandler? = serializeHandler, - ) : this(adapter, url, ContentType(mediaType), headers, sslSettings, handler) + ) : this( + adapter, + url, + ContentType(mediaType), + accept.map(::ContentType), + headers, + sslSettings, + handler + ) fun start() { if (!client.started()) @@ -135,7 +127,8 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, attributes: Map = emptyMap(), ): HttpResponsePort = client @@ -152,6 +145,7 @@ data class Http( formParameters = FormParameters(formParameters), parts = parts, contentType = contentType, + accept = accept, ) } .send(lastRequest, attributes = attributes) @@ -165,7 +159,8 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, attributes: Map = emptyMap(), ): HttpResponsePort = send( @@ -176,6 +171,7 @@ data class Http( formParameters = formParameters, parts = parts, contentType = contentType, + accept = accept, attributes = attributes + mapOf("pathPattern" to pathPattern, "pathParameters" to pathParameters), ) @@ -186,9 +182,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(GET, path, headers, body, formParameters, parts, contentType) + send(GET, path, headers, body, formParameters, parts, contentType, accept) fun put( path: String = "/", @@ -196,19 +193,21 @@ data class Http( headers: Map = emptyMap(), formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(PUT, path, headers, body, formParameters, parts, contentType) + send(PUT, path, headers, body, formParameters, parts, contentType, accept) fun put( path: String = "/", formParameters: List = emptyList(), headers: Map = emptyMap(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, body: () -> Any, ): HttpResponsePort = - send(PUT, path, headers, body(), formParameters, parts, contentType) + send(PUT, path, headers, body(), formParameters, parts, contentType, accept) fun post( path: String = "/", @@ -216,9 +215,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(POST, path, headers, body, formParameters, parts, contentType) + send(POST, path, headers, body, formParameters, parts, contentType, accept) fun options( path: String = "/", @@ -226,9 +226,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(OPTIONS, path, headers, body, formParameters, parts, contentType) + send(OPTIONS, path, headers, body, formParameters, parts, contentType, accept) fun delete( path: String = "/", @@ -236,9 +237,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(DELETE, path, headers, body, formParameters, parts, contentType) + send(DELETE, path, headers, body, formParameters, parts, contentType, accept) fun patch( path: String = "/", @@ -246,9 +248,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(PATCH, path, headers, body, formParameters, parts, contentType) + send(PATCH, path, headers, body, formParameters, parts, contentType, accept) fun trace( path: String = "/", @@ -256,9 +259,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(TRACE, path, headers, body, formParameters, parts, contentType) + send(TRACE, path, headers, body, formParameters, parts, contentType, accept) fun get( pathPattern: String, @@ -267,9 +271,12 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(GET, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + GET, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) fun put( pathPattern: String, @@ -278,19 +285,25 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(PUT, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + PUT, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) fun put( pathPattern: String, pathParameters: Map, formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, body: () -> Any, ): HttpResponsePort = - send(PUT, pathPattern, pathParameters, headers, body(), formParameters, parts, contentType) + send( + PUT, pathPattern, pathParameters, Headers(), body(), formParameters, parts, contentType, accept + ) fun post( pathPattern: String, @@ -299,9 +312,12 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(POST, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + POST, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) fun options( pathPattern: String, @@ -310,9 +326,12 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(OPTIONS, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + OPTIONS, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) fun delete( pathPattern: String, @@ -321,9 +340,12 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(DELETE, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + DELETE, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) fun patch( pathPattern: String, @@ -332,9 +354,12 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(PATCH, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + PATCH, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) fun trace( pathPattern: String, @@ -343,7 +368,10 @@ data class Http( body: Any = "", formParameters: List = emptyList(), parts: List = emptyList(), - contentType: ContentType? = this.contentType, + contentType: ContentType? = settings.contentType, + accept: List = settings.accept, ): HttpResponsePort = - send(TRACE, pathPattern, pathParameters, headers, body, formParameters, parts, contentType) + send( + TRACE, pathPattern, pathParameters, headers, body, formParameters, parts, contentType, accept + ) } diff --git a/http/rest_tools/src/test/kotlin/com/hexagonkt/http/test/DynamicServerTest.kt b/http/rest_tools/src/test/kotlin/com/hexagonkt/http/test/DynamicServerTest.kt index b6f64685bf..f01b72e6ce 100644 --- a/http/rest_tools/src/test/kotlin/com/hexagonkt/http/test/DynamicServerTest.kt +++ b/http/rest_tools/src/test/kotlin/com/hexagonkt/http/test/DynamicServerTest.kt @@ -1,13 +1,20 @@ package com.hexagonkt.http.test +import com.hexagonkt.core.logging.info +import com.hexagonkt.core.media.APPLICATION_JSON import com.hexagonkt.core.media.TEXT_PLAIN +import com.hexagonkt.core.require import com.hexagonkt.http.client.jetty.JettyClientAdapter import com.hexagonkt.http.model.ContentType import com.hexagonkt.http.model.OK_200 import com.hexagonkt.http.handlers.path import com.hexagonkt.http.model.HttpResponsePort +import com.hexagonkt.http.model.HttpStatusType.SUCCESS +import com.hexagonkt.http.server.HttpServerSettings import com.hexagonkt.http.server.jetty.JettyServletAdapter -import com.hexagonkt.http.test.Http.Companion.http +import com.hexagonkt.rest.bodyMap +import com.hexagonkt.serialization.SerializationManager +import com.hexagonkt.serialization.jackson.json.Json import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import kotlin.test.Test @@ -29,7 +36,7 @@ class DynamicServerTest { } @Test fun `Do HTTP requests`() { - dynamicServer.path = path { + dynamicServer.path { get("/hello/{name}") { val name = pathParameters["name"] @@ -37,7 +44,7 @@ class DynamicServerTest { } } - http(JettyClientAdapter(), "http://localhost:${dynamicServer.runtimePort}") { + Http(JettyClientAdapter(), "http://localhost:${dynamicServer.runtimePort}").request { get("/hello/mike") assertEquals(OK_200, response.status) } @@ -77,7 +84,7 @@ class DynamicServerTest { val port = dynamicServer.runtimePort val adapter = JettyClientAdapter() val headers = mapOf("alfa" to "beta", "charlie" to listOf("delta", "echo")) - val http = Http(adapter, headers = headers) + val http = Http(adapter, httpHeaders = headers) http.get("http://localhost:$port/hello/mike").assertBody("GET /hello/mike", headers) http.get("http://localhost:$port").assertBody("GET / ", headers) @@ -102,8 +109,8 @@ class DynamicServerTest { } @Test fun `Check all HTTP methods`() { - dynamicServer.path = path { - on("*") { + dynamicServer.path { + before("*") { ok("$method $path ${request.headers}", contentType = ContentType(TEXT_PLAIN)) } } @@ -111,7 +118,7 @@ class DynamicServerTest { val url = "http://localhost:${dynamicServer.runtimePort}" val adapter = JettyClientAdapter() val headers = mapOf("alfa" to "beta", "charlie" to listOf("delta", "echo")) - val http = Http(adapter, url, headers = headers) + val http = Http(adapter, url, httpHeaders = headers) http.get("/hello/mike").assertBody("GET /hello/mike", headers) http.get().assertBody("GET / ", headers) @@ -133,6 +140,119 @@ class DynamicServerTest { http.trace("/hello/mike").assertBody("TRACE /hello/mike", headers) http.trace().assertBody("TRACE / ", headers) + + http.request { + get("/hello/mike").assertBody("GET /hello/mike", headers) + get().assertBody("GET / ", headers) + + put("/hello/mike").assertBody("PUT /hello/mike", headers) + put().assertBody("PUT / ", headers) + + post("/hello/mike").assertBody("POST /hello/mike", headers) + post().assertBody("POST / ", headers) + + options("/hello/mike").assertBody("OPTIONS /hello/mike", headers) + options().assertBody("OPTIONS / ", headers) + + delete("/hello/mike").assertBody("DELETE /hello/mike", headers) + delete().assertBody("DELETE / ", headers) + + patch("/hello/mike").assertBody("PATCH /hello/mike", headers) + patch().assertBody("PATCH / ", headers) + + trace("/hello/mike").assertBody("TRACE /hello/mike", headers) + trace().assertBody("TRACE / ", headers) + } + } + + @Suppress("unused") // Object only used for serialization + @Test fun `Check HTTP helper`() { + SerializationManager.formats = linkedSetOf(Json) + + val settings = HttpServerSettings(bindPort = 0) + val server = DynamicServer(JettyServletAdapter(), settings).apply(DynamicServer::start) + val headers = mapOf("alfa" to "beta", "charlie" to listOf("delta", "echo")) + val text = ContentType(TEXT_PLAIN) + val json = ContentType(APPLICATION_JSON) + val binding = server.binding.toString() + val adapter = JettyClientAdapter() + val http = Http(adapter, url = binding, httpHeaders = headers, httpContentType = json) + + server.path { + before("*") { + ok("$method $path ${request.headers}", contentType = text) + } + + put("/data/{id}") { + val id = pathParameters.require("id") + val data = request.bodyMap() + val content = mapOf(id to data) + + ok(content, contentType = json) + } + } + + http.request { + put("/data/{id}", mapOf("id" to 102030)) { + object { + val title = "Casino Royale" + val tags = listOf("007", "action") + } + } + + assertOk() + response.body.info("BODY: ") + response.contentType.info("CONTENT TYPE: ") + } + + http.request { + get("/hello/mike") + assertBody("GET /hello/mike", headers) + get() + assertBody("GET / ", headers) + + put("/hello/mike") + assertBody("PUT /hello/mike", headers) + put() + assertBody("PUT / ", headers) + + post("/hello/mike") + assertBody("POST /hello/mike", headers) + post() + assertBody("POST / ", headers) + + options("/hello/mike") + assertBody("OPTIONS /hello/mike", headers) + options() + assertBody("OPTIONS / ", headers) + + delete("/hello/mike") + assertBody("DELETE /hello/mike", headers) + delete() + assertBody("DELETE / ", headers) + + patch("/hello/mike") + assertBody("PATCH /hello/mike", headers) + patch() + assertBody("PATCH / ", headers) + + trace("/hello/mike") + assertBody("TRACE /hello/mike", headers) + trace() + assertBody("TRACE / ", headers) + } + } + + private fun Http.assertBody(expectedBody: String, checkedHeaders: Map) { + assertOk() + assertSuccess() + assertStatus(OK_200) + assertStatus(SUCCESS) + assertBodyContains(expectedBody) + + for ((k, v) in checkedHeaders.entries) { + assertBodyContains(k, v.toString()) + } } private fun HttpResponsePort.assertBody( diff --git a/http/web/src/test/kotlin/com/hexagonkt/web/examples/TodoTest.kt b/http/web/src/test/kotlin/com/hexagonkt/web/examples/TodoTest.kt index 38b10b7458..4fc5f35639 100644 --- a/http/web/src/test/kotlin/com/hexagonkt/web/examples/TodoTest.kt +++ b/http/web/src/test/kotlin/com/hexagonkt/web/examples/TodoTest.kt @@ -1,13 +1,10 @@ package com.hexagonkt.web.examples -import com.hexagonkt.core.fieldsMapOfNotNull -import com.hexagonkt.core.require -import com.hexagonkt.core.requirePath +import com.hexagonkt.core.* import com.hexagonkt.core.logging.Logger import com.hexagonkt.core.logging.LoggingLevel.DEBUG import com.hexagonkt.core.logging.LoggingManager import com.hexagonkt.core.media.APPLICATION_JSON -import com.hexagonkt.core.urlOf import com.hexagonkt.logging.jul.JulLoggingAdapter import com.hexagonkt.http.client.HttpClient import com.hexagonkt.http.client.HttpClientSettings @@ -39,15 +36,16 @@ abstract class TodoTest(adapter: HttpServerPort) { val title: String = "", val description: String = "" ) : Data { - override fun data(): Map = - fieldsMapOfNotNull( + + override val data: Map = + fieldsMapOf( Task::description to description, Task::number to number, Task::title to title, ) - override fun with(data: Map): Task = - Task( + override fun copy(data: Map): Task = + copy( description = data.requirePath(Task::description), number = data.requirePath(Task::number), title = data.requirePath(Task::title), @@ -79,13 +77,13 @@ abstract class TodoTest(adapter: HttpServerPort) { path("/tasks") { post { - val task = Task().with(request.bodyString().parseMap(Json)) + val task = Task().copy(request.bodyString().parseMap(Json)) tasks += task.number to task send(CREATED_201, task.number.toString()) } put { - val task = Task().with(request.bodyString().parseMap(Json)) + val task = Task().copy(request.bodyString().parseMap(Json)) tasks += task.number to task ok("Task with id '${task.number}' updated") } @@ -114,7 +112,7 @@ abstract class TodoTest(adapter: HttpServerPort) { val task = tasks[taskId] if (task != null) ok( - body = task.data().serialize(Json), + body = task.data.serialize(Json), contentType = ContentType(APPLICATION_JSON) ) else @@ -133,7 +131,7 @@ abstract class TodoTest(adapter: HttpServerPort) { } get { - val body = tasks.values.map { it.data() }.serialize(Json) + val body = tasks.values.map { it.data }.serialize(Json) ok(body, contentType = ContentType(APPLICATION_JSON)) } } diff --git a/serialization/serialization/README.md b/serialization/serialization/README.md index 10e9889f4e..01be519c7e 100644 --- a/serialization/serialization/README.md +++ b/serialization/serialization/README.md @@ -3,9 +3,10 @@ This module holds serialization utilities. ### Install the Dependency -This module is not meant to be imported directly. It will be included by using any other part of the -toolkit. However, if you only want to use the utilities, logging or serialization (i.e., for a -desktop application), you can import it with the following code: +This module is not meant to be used directly. You should include an Adapter implementing this +feature (as [serialization_dsl_json]) in order to parse/serialize data. + +[serialization_dsl_json]: /serialization_dsl_json === "build.gradle" diff --git a/serialization/serialization/api/serialization.api b/serialization/serialization/api/serialization.api index 68a2d24c70..e0b3c8e491 100644 --- a/serialization/serialization/api/serialization.api +++ b/serialization/serialization/api/serialization.api @@ -1,20 +1,22 @@ public abstract interface class com/hexagonkt/serialization/Data : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { public abstract fun containsKey (Ljava/lang/String;)Z public abstract fun containsValue (Ljava/lang/Object;)Z - public abstract fun data ()Ljava/util/Map; + public abstract fun copy (Ljava/util/Map;)Ljava/lang/Object; public abstract fun get (Ljava/lang/String;)Ljava/lang/Object; + public abstract fun get (Lkotlin/reflect/KProperty1;)Ljava/lang/Object; + public abstract fun getData ()Ljava/util/Map; public abstract fun getEntries ()Ljava/util/Set; public abstract fun getKeys ()Ljava/util/Set; public abstract fun getSize ()I public abstract fun getValues ()Ljava/util/Collection; public abstract fun isEmpty ()Z - public abstract fun with (Ljava/util/Map;)Ljava/lang/Object; } public final class com/hexagonkt/serialization/Data$DefaultImpls { public static fun containsKey (Lcom/hexagonkt/serialization/Data;Ljava/lang/String;)Z public static fun containsValue (Lcom/hexagonkt/serialization/Data;Ljava/lang/Object;)Z public static fun get (Lcom/hexagonkt/serialization/Data;Ljava/lang/String;)Ljava/lang/Object; + public static fun get (Lcom/hexagonkt/serialization/Data;Lkotlin/reflect/KProperty1;)Ljava/lang/Object; public static fun getEntries (Lcom/hexagonkt/serialization/Data;)Ljava/util/Set; public static fun getKeys (Lcom/hexagonkt/serialization/Data;)Ljava/util/Set; public static fun getSize (Lcom/hexagonkt/serialization/Data;)I @@ -22,6 +24,30 @@ public final class com/hexagonkt/serialization/Data$DefaultImpls { public static fun isEmpty (Lcom/hexagonkt/serialization/Data;)Z } +public abstract interface class com/hexagonkt/serialization/MutableData : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { + public abstract fun containsKey (Ljava/lang/String;)Z + public abstract fun containsValue (Ljava/lang/Object;)Z + public abstract fun data ()Ljava/util/Map; + public abstract fun get (Ljava/lang/String;)Ljava/lang/Object; + public abstract fun getEntries ()Ljava/util/Set; + public abstract fun getKeys ()Ljava/util/Set; + public abstract fun getSize ()I + public abstract fun getValues ()Ljava/util/Collection; + public abstract fun isEmpty ()Z + public abstract fun with (Ljava/util/Map;)V +} + +public final class com/hexagonkt/serialization/MutableData$DefaultImpls { + public static fun containsKey (Lcom/hexagonkt/serialization/MutableData;Ljava/lang/String;)Z + public static fun containsValue (Lcom/hexagonkt/serialization/MutableData;Ljava/lang/Object;)Z + public static fun get (Lcom/hexagonkt/serialization/MutableData;Ljava/lang/String;)Ljava/lang/Object; + public static fun getEntries (Lcom/hexagonkt/serialization/MutableData;)Ljava/util/Set; + public static fun getKeys (Lcom/hexagonkt/serialization/MutableData;)Ljava/util/Set; + public static fun getSize (Lcom/hexagonkt/serialization/MutableData;)I + public static fun getValues (Lcom/hexagonkt/serialization/MutableData;)Ljava/util/Collection; + public static fun isEmpty (Lcom/hexagonkt/serialization/MutableData;)Z +} + public abstract interface class com/hexagonkt/serialization/SerializationFormat { public static final field PARSING_ERROR Ljava/lang/String; public static final field SERIALIZATION_ERROR Ljava/lang/String; @@ -68,15 +94,21 @@ public final class com/hexagonkt/serialization/SerializationKt { public static final fun parseMap (Ljava/nio/file/Path;)Ljava/util/Map; public static synthetic fun parseMap$default (Ljava/io/InputStream;Lcom/hexagonkt/serialization/SerializationFormat;ILjava/lang/Object;)Ljava/util/Map; public static synthetic fun parseMap$default (Ljava/lang/String;Lcom/hexagonkt/serialization/SerializationFormat;ILjava/lang/Object;)Ljava/util/Map; + public static final fun parseMaps (Ljava/io/File;)Ljava/util/List; + public static final fun parseMaps (Ljava/io/InputStream;Lcom/hexagonkt/core/media/MediaType;)Ljava/util/List; + public static final fun parseMaps (Ljava/io/InputStream;Lcom/hexagonkt/serialization/SerializationFormat;)Ljava/util/List; + public static final fun parseMaps (Ljava/lang/String;Lcom/hexagonkt/core/media/MediaType;)Ljava/util/List; + public static final fun parseMaps (Ljava/lang/String;Lcom/hexagonkt/serialization/SerializationFormat;)Ljava/util/List; + public static final fun parseMaps (Ljava/net/URL;)Ljava/util/List; + public static final fun parseMaps (Ljava/nio/file/Path;)Ljava/util/List; + public static synthetic fun parseMaps$default (Ljava/io/InputStream;Lcom/hexagonkt/serialization/SerializationFormat;ILjava/lang/Object;)Ljava/util/List; + public static synthetic fun parseMaps$default (Ljava/lang/String;Lcom/hexagonkt/serialization/SerializationFormat;ILjava/lang/Object;)Ljava/util/List; public static final fun serialize (Ljava/lang/Object;Lcom/hexagonkt/core/media/MediaType;)Ljava/lang/String; public static final fun serialize (Ljava/lang/Object;Lcom/hexagonkt/serialization/SerializationFormat;)Ljava/lang/String; public static synthetic fun serialize$default (Ljava/lang/Object;Lcom/hexagonkt/serialization/SerializationFormat;ILjava/lang/Object;)Ljava/lang/String; public static final fun serializeBytes (Ljava/lang/Object;Lcom/hexagonkt/core/media/MediaType;)[B public static final fun serializeBytes (Ljava/lang/Object;Lcom/hexagonkt/serialization/SerializationFormat;)[B public static synthetic fun serializeBytes$default (Ljava/lang/Object;Lcom/hexagonkt/serialization/SerializationFormat;ILjava/lang/Object;)[B - public static final fun toData (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/util/List; - public static final fun toData (Ljava/util/List;Lkotlin/jvm/functions/Function0;)Ljava/util/List; - public static final fun toData (Ljava/util/Map;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; } public final class com/hexagonkt/serialization/SerializationManager { diff --git a/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Data.kt b/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Data.kt index 4fdf16e88a..58c431c833 100644 --- a/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Data.kt +++ b/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Data.kt @@ -1,32 +1,36 @@ package com.hexagonkt.serialization import kotlin.collections.Map.Entry +import kotlin.reflect.KProperty1 interface Data : Map { - fun data(): Map - fun with(data: Map): T + val data: Map + fun copy(data: Map): T - override val entries: Set> - get() = data().entries + override val entries: Set> + get() = data.entries override val keys: Set - get() = data().keys + get() = data.keys override val size: Int - get() = data().size + get() = data.size - override val values: Collection - get() = data().values + override val values: Collection<*> + get() = data.values override fun isEmpty(): Boolean = - data().isEmpty() + data.isEmpty() override fun get(key: String): Any? = - data()[key] + data[key] override fun containsValue(value: Any?): Boolean = - data().containsValue(value) + data.containsValue(value) override fun containsKey(key: String): Boolean = - data().containsKey(key) + data.containsKey(key) + + operator fun get(key: KProperty1): Any? = + data[key.name] } diff --git a/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/MutableData.kt b/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/MutableData.kt new file mode 100644 index 0000000000..4ab2e7f8b0 --- /dev/null +++ b/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/MutableData.kt @@ -0,0 +1,32 @@ +package com.hexagonkt.serialization + +import kotlin.collections.Map.Entry + +interface MutableData : Map { + fun data(): Map + fun with(data: Map) + + override val entries: Set> + get() = data().entries + + override val keys: Set + get() = data().keys + + override val size: Int + get() = data().size + + override val values: Collection<*> + get() = data().values + + override fun isEmpty(): Boolean = + data().isEmpty() + + override fun get(key: String): Any? = + data()[key] + + override fun containsValue(value: Any?): Boolean = + data().containsValue(value) + + override fun containsKey(key: String): Boolean = + data().containsKey(key) +} diff --git a/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Serialization.kt b/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Serialization.kt index 538463845c..672d2208f0 100644 --- a/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Serialization.kt +++ b/serialization/serialization/src/main/kotlin/com/hexagonkt/serialization/Serialization.kt @@ -50,54 +50,62 @@ fun File.parseMap(): Map = fun File.parseList(): List<*> = this.parse().castToList() +fun File.parseMaps(): List> = + this.parseList().map(Any?::castToMap) + fun Path.parseMap(): Map = this.parse().castToMap() fun Path.parseList(): List<*> = this.parse().castToList() +fun Path.parseMaps(): List> = + this.parseList().map(Any?::castToMap) + fun URL.parseMap(): Map = this.parse().castToMap() fun URL.parseList(): List<*> = this.parse().castToList() +fun URL.parseMaps(): List> = + this.parseList().map(Any?::castToMap) + fun String.parseMap(format: SerializationFormat = requireDefaultFormat()): Map = this.parse(format).castToMap() fun String.parseList(format: SerializationFormat = requireDefaultFormat()): List<*> = this.parse(format).castToList() +fun String.parseMaps(format: SerializationFormat = requireDefaultFormat()): List> = + this.parseList(format).map(Any?::castToMap) + fun String.parseMap(mediaType: MediaType): Map = this.parse(mediaType).castToMap() fun String.parseList(mediaType: MediaType): List<*> = this.parse(mediaType).castToList() +fun String.parseMaps(mediaType: MediaType): List> = + this.parseList(mediaType).map(Any?::castToMap) + fun InputStream.parseMap(format: SerializationFormat = requireDefaultFormat()): Map = this.parse(format).castToMap() fun InputStream.parseList(format: SerializationFormat = requireDefaultFormat()): List<*> = this.parse(format).castToList() +fun InputStream.parseMaps(format: SerializationFormat = requireDefaultFormat()): List> = + this.parseList(format).map(Any?::castToMap) + fun InputStream.parseMap(mediaType: MediaType): Map = this.parse(mediaType).castToMap() fun InputStream.parseList(mediaType: MediaType): List<*> = this.parse(mediaType).castToList() -fun Any.toData(data: () -> Data): List = - when (this) { - is Map<*, *> -> listOf(this.castToMap().toData(data)) - is List<*> -> toData(data) - else -> error("Instance of type: ${this::class.simpleName} cannot be transformed to data") - } - -fun Map.toData(data: () -> Data): T = - data().with(this) - -fun List<*>.toData(data: () -> Data): List = - map { it.castToMap() }.map { it.toData(data) } +fun InputStream.parseMaps(mediaType: MediaType): List> = + this.parseList(mediaType).map(Any?::castToMap) @Suppress("UNCHECKED_CAST") // Cast exception handled in function private fun Any?.castToMap(): Map = diff --git a/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/MutableDataTest.kt b/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/MutableDataTest.kt new file mode 100644 index 0000000000..c013ea0c70 --- /dev/null +++ b/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/MutableDataTest.kt @@ -0,0 +1,74 @@ +package com.hexagonkt.serialization + +import com.hexagonkt.core.fieldsMapOf +import com.hexagonkt.core.requirePath +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +internal class MutableDataTest { + + data class MutableTask( + var number: Int = 0, + var title: String = "", + var description: String = "" + ) : MutableData { + + override fun data(): Map = + fieldsMapOf( + MutableTask::description to description, + MutableTask::number to number, + MutableTask::title to title, + ) + + override fun with(data: Map) { + description = data.requirePath(MutableTask::description) + number = data.requirePath(MutableTask::number) + title = data.requirePath(MutableTask::title) + } + } + + @Test fun `Mutable data use case`() { + val task = MutableTask() + + assertEquals( + fieldsMapOf( + MutableTask::description to task.description, + MutableTask::number to task.number, + MutableTask::title to task.title, + ), + task.data() + ) + + task.number = 1 + + assertEquals( + fieldsMapOf( + MutableTask::description to task.description, + MutableTask::number to task.number, + MutableTask::title to task.title, + ), + task.data() + ) + + task.with( + fieldsMapOf( + MutableTask::description to "description", + MutableTask::title to "title", + MutableTask::number to task.number, + ) + ) + + assertEquals( + fieldsMapOf( + MutableTask::description to task.description, + MutableTask::number to task.number, + MutableTask::title to task.title, + ), + task.data() + ) + + assertEquals(1, task[MutableTask::number.name]) + assertEquals("description", task[MutableTask::description.name]) + assertEquals("title", task[MutableTask::title.name]) + } +} diff --git a/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/SerializationTest.kt b/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/SerializationTest.kt index f7b01cae61..f883515f4d 100644 --- a/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/SerializationTest.kt +++ b/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/SerializationTest.kt @@ -50,9 +50,9 @@ internal class SerializationTest { } """ - val company = jsonCompany.parseMap(Json).toData(::Company) + val company = jsonCompany.parseMap(Json).let { Company().copy(it) } val serializedCompany = company.copy(notes = "${company.notes} updated").serialize(Json) - val parsedCompany = serializedCompany.parseMap(Json).toData(::Company) + val parsedCompany = serializedCompany.parseMap(Json).let { Company().copy(it) } assertEquals(setOf(DESIGN, DEVELOPMENT), company.departments) assertEquals(0.13782355F, company.averageMargin) @@ -99,7 +99,7 @@ internal class SerializationTest { @Test fun `Data serialization helpers convert data properly`() { SerializationManager.formats = setOf(Json) - urlOf("classpath:data/company.json").parse().toData(::Company).first().apply { + urlOf("classpath:data/company.json").parseMap().let { Company().copy(it) }.apply { assertEquals("id1", id) assertEquals(LocalDate.of(2014, 1, 25), foundation) assertEquals(LocalTime.of(11, 42), closeTime) @@ -110,7 +110,7 @@ internal class SerializationTest { assertEquals(InetAddress.getByName("127.0.0.1"), host) } - urlOf("classpath:data/companies.json").parse().toData(::Company).first().apply { + urlOf("classpath:data/companies.json").parseMaps().map { Company().copy(it) }.first().apply { val clientList = listOf(urlOf("http://c1.example.org"), urlOf("http://c2.example.org")) assertEquals("id", id) @@ -126,8 +126,5 @@ internal class SerializationTest { assertEquals(LocalDateTime.of(2016, 1, 1, 0, 0), creationDate) assertEquals(InetAddress.getByName("127.0.0.1"), host) } - - val e = assertFailsWith { "text".toData(::Company) } - assertEquals("Instance of type: String cannot be transformed to data", e.message) } } diff --git a/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/TestUtilities.kt b/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/TestUtilities.kt index b34f343407..e2e90647d5 100644 --- a/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/TestUtilities.kt +++ b/serialization/serialization/src/test/kotlin/com/hexagonkt/serialization/TestUtilities.kt @@ -17,10 +17,10 @@ import java.time.format.DateTimeFormatter.* internal enum class Department { DESIGN, DEVELOPMENT } internal data class Person(val name: String) : Data { - override fun data(): Map = - fieldsMapOfNotNull(Person::name to name) - override fun with(data: Map): Person = + override val data: Map = fieldsMapOf(Person::name to name) + + override fun copy(data: Map): Person = copy(name = data.getOrDefault(Person::name, name)) } @@ -51,8 +51,8 @@ internal data class Company( averageMargin = 0.0F, ) - override fun data(): Map = - fieldsMapOfNotNull( + override val data: Map = + fieldsMapOf( Company::id to id, Company::foundation to ISO_LOCAL_DATE.format(foundation), Company::closeTime to ISO_LOCAL_TIME.format(closeTime), @@ -68,7 +68,7 @@ internal data class Company( Company::averageMargin to averageMargin, ) - override fun with(data: Map): Company = + override fun copy(data: Map): Company = copy( id = data.getString(Company::id) ?: id, foundation = data.getString(Company::foundation)?.let(LocalDate::parse) ?: foundation, diff --git a/serialization/serialization_dsl_json/README.md b/serialization/serialization_dsl_json/README.md new file mode 100644 index 0000000000..b8cc11449a --- /dev/null +++ b/serialization/serialization_dsl_json/README.md @@ -0,0 +1,31 @@ + +# Module http_client_jetty +[Serialization] implementation using the [DSL JSON] library. + +[Serialization]: /serialization +[DSL JSON]: https://github.com/ngs-doo/dsl-json + +### Install the Dependency + +=== "build.gradle" + + ```groovy + repositories { + mavenCentral() + } + + implementation("com.hexagonkt:serialization_dsl_json:$hexagonVersion") + ``` + +=== "pom.xml" + + ```xml + + com.hexagonkt + serialization_dsl_json + $hexagonVersion + + ``` + +# Package com.hexagonkt.serialization.dsl.json +DSL JSON implementation classes. diff --git a/serialization/serialization_test/src/test/kotlin/com/hexagonkt/serialization/SerializationHelpersTest.kt b/serialization/serialization_test/src/test/kotlin/com/hexagonkt/serialization/SerializationHelpersTest.kt index 8ec4c75045..8f52a3046c 100644 --- a/serialization/serialization_test/src/test/kotlin/com/hexagonkt/serialization/SerializationHelpersTest.kt +++ b/serialization/serialization_test/src/test/kotlin/com/hexagonkt/serialization/SerializationHelpersTest.kt @@ -24,7 +24,7 @@ internal class SerializationHelpersTest { } @Test fun `Parse URL helpers generates the correct collection`() { - assert(urlOf("classpath:companies.json").parseList().isNotEmpty()) + assert(urlOf("classpath:companies.json").parseMaps().isNotEmpty()) assert(urlOf("classpath:company.json").parseMap().isNotEmpty()) } @@ -34,28 +34,28 @@ internal class SerializationHelpersTest { else "src/test/resources" } - assert(File("$baseDir/companies.json").parseList().isNotEmpty()) + assert(File("$baseDir/companies.json").parseMaps().isNotEmpty()) assert(File("$baseDir/company.json").parseMap().isNotEmpty()) - assert(Path.of("$baseDir/companies.json").parseList().isNotEmpty()) + assert(Path.of("$baseDir/companies.json").parseMaps().isNotEmpty()) assert(Path.of("$baseDir/company.json").parseMap().isNotEmpty()) } @Test fun `Parse string helpers generates the correct collection`() { - assert("""[ { "a": "b" } ]""".parseList().isNotEmpty()) + assert("""[ { "a": "b" } ]""".parseMaps().isNotEmpty()) assert("""{ "a": "b" }""".parseMap().isNotEmpty()) - assert("""[ { "a": "b" } ]""".parseList(Json).isNotEmpty()) + assert("""[ { "a": "b" } ]""".parseMaps(Json).isNotEmpty()) assert("""{ "a": "b" }""".parseMap(Json).isNotEmpty()) - assert("""[ { "a": "b" } ]""".parseList(APPLICATION_JSON).isNotEmpty()) + assert("""[ { "a": "b" } ]""".parseMaps(APPLICATION_JSON).isNotEmpty()) assert("""{ "a": "b" }""".parseMap(APPLICATION_JSON).isNotEmpty()) } @Test fun `Parse stream helpers generates the correct collection`() { - assert("""[ { "a": "b" } ]""".toStream().parseList().isNotEmpty()) + assert("""[ { "a": "b" } ]""".toStream().parseMaps().isNotEmpty()) assert("""{ "a": "b" }""".toStream().parseMap().isNotEmpty()) - assert("""[ { "a": "b" } ]""".toStream().parseList(Json).isNotEmpty()) + assert("""[ { "a": "b" } ]""".toStream().parseMaps(Json).isNotEmpty()) assert("""{ "a": "b" }""".toStream().parseMap(Json).isNotEmpty()) - assert("""[ { "a": "b" } ]""".toStream().parseList(APPLICATION_JSON).isNotEmpty()) + assert("""[ { "a": "b" } ]""".toStream().parseMaps(APPLICATION_JSON).isNotEmpty()) assert("""{ "a": "b" }""".toStream().parseMap(APPLICATION_JSON).isNotEmpty()) } } diff --git a/serverless/serverless/build.gradle.kts b/serverless/serverless/build.gradle.kts new file mode 100644 index 0000000000..4591718a85 --- /dev/null +++ b/serverless/serverless/build.gradle.kts @@ -0,0 +1,16 @@ + +plugins { + id("java-library") +} + +apply(from = "$rootDir/gradle/kotlin.gradle") +apply(from = "$rootDir/gradle/publish.gradle") +apply(from = "$rootDir/gradle/dokka.gradle") +apply(from = "$rootDir/gradle/native.gradle") +apply(from = "$rootDir/gradle/detekt.gradle") + +description = "HTTP serverless functions. Requires an adapter to be used." + +dependencies { + "api"(project(":http:http_handlers")) +} diff --git a/serverless/serverless/src/main/kotlin/com/hexagonkt/serverless/ServerlessFunction.kt b/serverless/serverless/src/main/kotlin/com/hexagonkt/serverless/ServerlessFunction.kt new file mode 100644 index 0000000000..f321e2927e --- /dev/null +++ b/serverless/serverless/src/main/kotlin/com/hexagonkt/serverless/ServerlessFunction.kt @@ -0,0 +1,3 @@ +package com.hexagonkt.serverless + +class ServerlessFunction diff --git a/serverless/serverless/src/main/kotlin/com/hexagonkt/serverless/ServerlessPort.kt b/serverless/serverless/src/main/kotlin/com/hexagonkt/serverless/ServerlessPort.kt new file mode 100644 index 0000000000..33ba7aba0d --- /dev/null +++ b/serverless/serverless/src/main/kotlin/com/hexagonkt/serverless/ServerlessPort.kt @@ -0,0 +1,3 @@ +package com.hexagonkt.serverless + +interface ServerlessPort diff --git a/serverless/serverless/src/test/kotlin/com/hexagonkt/serverless/ServerlessFunctionTest.kt b/serverless/serverless/src/test/kotlin/com/hexagonkt/serverless/ServerlessFunctionTest.kt new file mode 100644 index 0000000000..56f787d6bf --- /dev/null +++ b/serverless/serverless/src/test/kotlin/com/hexagonkt/serverless/ServerlessFunctionTest.kt @@ -0,0 +1,10 @@ +package com.hexagonkt.serverless + +import org.junit.jupiter.api.Test + +internal class ServerlessFunctionTest { + + @Test fun `Serverless test`() { + + } +} diff --git a/serverless/serverless/src/test/resources/META-INF/native-image/com.hexagonkt/serverless/native-image.properties b/serverless/serverless/src/test/resources/META-INF/native-image/com.hexagonkt/serverless/native-image.properties new file mode 100644 index 0000000000..f68b9ce179 --- /dev/null +++ b/serverless/serverless/src/test/resources/META-INF/native-image/com.hexagonkt/serverless/native-image.properties @@ -0,0 +1,3 @@ +Args= \ + --initialize-at-build-time=kotlin.annotation.AnnotationRetention \ + --initialize-at-build-time=kotlin.annotation.AnnotationTarget diff --git a/settings.gradle.kts b/settings.gradle.kts index 7cad87df8c..7735c77955 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ includeNestedModules( "http", "logging", "serialization", +// "serverless", "templates" ) diff --git a/site/mkdocs.yml b/site/mkdocs.yml index 21e9d2451f..935e6dabe9 100644 --- a/site/mkdocs.yml +++ b/site/mkdocs.yml @@ -53,6 +53,7 @@ nav: - Core: core.md - HTTP Server: http_server.md - HTTP Client: http_client.md + - Serialization: serialization.md - Templates: templates.md - Gradle Helpers: gradle.md - Maven Parent POM: maven.md diff --git a/site/pages/gradle.md b/site/pages/gradle.md index a4a01bee99..d999872f33 100644 --- a/site/pages/gradle.md +++ b/site/pages/gradle.md @@ -4,10 +4,10 @@ The build process and imported build scripts (like the ones documented here) use customize their behavior. It is possible to add/change variables of a build from the following places: -1. In the project's `gradle.properties` file. -2. In your user's gradle configuration: `~/.gradle/gradle.properties`. -3. Passing them from the command line with the following switch: `-Pkey=val`. -4. Defining a project's extra property inside `build.gradle`. Ie: `project.ext.key='val'`. +1. Passing them from the command line with the following switch: `-P key=val`. +2. In the project's `gradle.properties` file. +3. Defining a project's extra property inside `build.gradle.kts`. Ie: `project.ext["key"] = "val"`. +4. In your user's gradle configuration: `~/.gradle/gradle.properties`. For examples and reference, check [build.gradle.kts] and [gradle.properties]. @@ -20,25 +20,29 @@ These scripts can be added to your build to include a whole new capability to yo To use them, you can import the online versions, or copy them to your `gradle` directory before importing the script. -You can import these scripts by adding `apply from: $gradleScripts/$script.gradle` to your -`build.gradle` file some of them may require additional plugins inside the `plugins` section in the -root `build.gradle`. Check toolkit `build.gradle` files for examples. +You can import these scripts by adding `apply(from = "$gradleScripts/$script.gradle")` to your +`build.gradle.kts`, some of them may require additional plugins inside the `plugins` section in the +root `build.gradle.kts`. Check toolkit `build.gradle.kts` files for examples. ## Publish This script set up the project/module for publishing in [Maven Central]. It publishes all artifacts attached to the `mavenJava` publication (check [kotlin.gradle] publishing -section) at the bare minimum binaries are published. For an Open Source project, you must include -sources and javadoc. +section). + +The binaries are always published. For an Open Source project, you must also include sources and +javadoc. To use it, apply `$gradleScripts/publish.gradle`. To set up this script's parameters, check the [build variables section]. These helper settings are: -* bintrayKey (REQUIRED): if not defined will try to load BINTRAY_KEY environment variable. -* bintrayUser (REQUIRED): or BINTRAY_USER environment variable if not defined. -* license (REQUIRED): the license used in published POMs. -* vcsUrl (REQUIRED): code repository location. +* signingKey (REQUIRED): if not defined will try to load SIGNING_KEY environment variable. +* signingPassword (REQUIRED): or SIGNING_PASSWORD environment variable if not defined. +* ossrhUsername (REQUIRED): or OSSRH_USERNAME if not found. +* ossrhPassword (REQUIRED): or OSSRH_PASSWORD if not found. +* licenses (REQUIRED): the licenses used in published POMs. +* siteHost (REQUIRED): project's website. [Maven Central]: https://search.maven.org [kotlin.gradle]: https://github.com/hexagonkt/hexagon/blob/master/gradle/kotlin.gradle @@ -54,7 +58,7 @@ All modules' Markdown files are added to the documentation and test classes endi are available to be referenced as samples. To use it, apply `$gradleScripts/dokka.gradle` and add the -`id 'org.jetbrains.dokka' version 'VERSION'` plugin to the root `build.gradle`. +`id("org.jetbrains.dokka") version("VERSION")` plugin to the root `build.gradle.kts`. The format for the generated documentation will be `javadoc` to make it compatible with current IDEs. @@ -67,7 +71,7 @@ Create web icons (favicon and thumbnails for browsers/mobile) from SVG images (l For image rendering you will need [rsvg] (librsvg2-bin) and [imagemagick] installed in the development machine. -To use it, apply `$gradleScripts/icons.gradle` to your `build.gradle`. +To use it, apply `$gradleScripts/icons.gradle` to your `build.gradle.kts`. To set up this script's parameters, check the [build variables section]. These helper settings are: @@ -97,7 +101,7 @@ It sets up: - Published artifacts (binaries and sources): sourcesJar task To use it, apply `$gradleScripts/kotlin.gradle` and add the -`id 'org.jetbrains.kotlin.jvm' version 'VERSION'` plugin to the root `build.gradle`. +`id("org.jetbrains.kotlin.jvm") version("VERSION")` plugin to the root `build.gradle.kts`. To set up this script's parameters, check the [build variables section]. These helper settings are: @@ -121,17 +125,16 @@ Gradle's script for a service or application. It adds these extra tasks: * jpackage: create a jpackage distribution including a JVM with a subset of the modules. * tarJlink: compress Jpackage distribution in a single file. -To use it, apply `$gradleScripts/application.gradle` to your `build.gradle`. +To use it, apply `$gradleScripts/application.gradle` to your `build.gradle.kts`. To set up this script's parameters, check the [build variables section]. These helper settings are: -* modules: comma separated list of modules to include in the generated JRE. By default: - `java.logging,java.management`. +* modules: comma separated list of modules to include in the generated JRE. * options: JVM options passed to the jpackage generated launcher. * icon: icon to be used in the jpackage distribution. * applicationClass (REQUIRED): fully qualified name of the main class of the application. -To set up this script you need to add the main class name to your `build.gradle` file with the +To set up this script you need to add the main class name to your `build.gradle.kts` file with the following code: ```groovy @@ -163,7 +166,7 @@ The defined tasks are: * createCa: creates `ca.p12` and import its public certificate inside `trust.p12`. * createIdentities: creates the `.p12` store for all `sslDomain` variables. -To use it, apply `$gradleScripts/certificates.gradle` to your `build.gradle`. +To use it, apply `$gradleScripts/certificates.gradle` to your `build.gradle.kts`. To set up this script's parameters, check the [build variables section]. These helper settings are: @@ -186,7 +189,7 @@ To set up this script's parameters, check the [build variables section]. These h ## Lean This script changes the default Gradle source layout to be less bulky. To use it you must apply the -`$gradleScripts/lean.gradle` script to your `build.gradle` file. It must be applied after the +`$gradleScripts/lean.gradle` script to your `build.gradle.kts` file. It must be applied after the Kotlin plugin. After applying this script, the source folders will be `${projectDir}/main` and @@ -194,7 +197,7 @@ After applying this script, the source folders will be `${projectDir}/main` and ## Detekt This script sets up the build to analyze the code with the [Detekt] static code analyzer. To use it -you must apply the `$gradleScripts/detekt.gradle` script to your `build.gradle` file. It must be +you must apply the `$gradleScripts/detekt.gradle` script to your `build.gradle.kts` file. It must be applied after the Kotlin plugin. For the script to work you need to add the plugin to the plugins build section before importing the @@ -202,7 +205,7 @@ script. I.e.: ```kotlin plugins { - id("io.gitlab.arturbosch.detekt") version "VERSION" apply false + id("io.gitlab.arturbosch.detekt") version("VERSION") apply(false) } ``` @@ -223,8 +226,8 @@ The defined tasks are: * upx: compress the native executable using 'upx'. NOTE: Makes binaries use more RAM!!! * tarNative: compress native executable into a TAR file. -To use it you must apply the `$gradleScripts/native.gradle` script to your `build.gradle` file. It -must be applied after the Kotlin plugin. +To use it you must apply the `$gradleScripts/native.gradle` script to your `build.gradle.kts` file. +It must be applied after the Kotlin plugin. For the script to work you need to add the plugin to the plugins build section before importing the script. I.e.: @@ -235,22 +238,30 @@ plugins { } ``` -To add configuration metadata in your libraries. you should run these commands: +If you want to create a native image for your application you should execute: ```bash -./gradlew -Pagent build -./gradlew metadataCopy -# After including the metadata you can publish your artifacts. I.e.: -./gradlew publishToMavenLocal +./gradlew nativeCompile ``` -And if you want to create a native image for your application you should execute: +To set up this script's parameters, check the [build variables section]. These helper settings are: -```bash -./gradlew -Pagent nativeCompile +[GraalVM]: https://www.graalvm.org +[native image]: https://graalvm.github.io/native-build-tools/latest/index.html + +## JMH +Sets up the build to run JMH benchmarks. To use it you must apply the `$gradleScripts/jmh.gradle` +script to your `build.gradle.kts` file. + +For the script to work you need to add the plugin to the plugins build section before importing the +script. I.e.: + +```kotlin +plugins { + id("me.champeau.jmh") version("VERSION") apply(false) +} ``` To set up this script's parameters, check the [build variables section]. These helper settings are: -[GraalVM]: https://www.graalvm.org -[native image]: https://graalvm.github.io/native-build-tools/latest/index.html +* jmhVersion: JMH version to be used. If not specified a tested JMH version will be used. diff --git a/site/pages/index.md b/site/pages/index.md index 284de737fd..b106a57e8e 100644 --- a/site/pages/index.md +++ b/site/pages/index.md @@ -3,7 +3,7 @@ template: index.html --- The Hexagon Toolkit provides several libraries to build server applications. These libraries provide -single standalone features[^1] and are referred to as ["Ports"][Ports and Adapters Architecture]. +single standalone features and are referred to as ["Ports"][Ports and Adapters Architecture]. The main ports are: @@ -11,6 +11,8 @@ The main ports are: upload), forms processing, cookies, CORS and more. * [The HTTP client]: which supports mutual TLS, HTTP/2, WebSockets, cookies, form fields and files among other features. +* [Serialization]: provides a common way of using different data formats. Data formats are pluggable + and are handled in the same way regardless of their library. * [Template Processing]: allows template processing from URLs (local files, resources or HTTP content) binding name patterns to different engines. @@ -21,11 +23,9 @@ Hexagon is designed to fit in applications that conform to the [Hexagonal Archit [Clean Architecture], [Onion Architecture] or [Ports and Adapters Architecture]). Its design principles also fit into this architecture. -[^1]: Except the Core module that contains a set of utilities like logging. However, some of these -capacities can be replaced by other third party libraries. - [The HTTP server]: /http_server [The HTTP client]: /http_client +[Serialization]: /serialization [Template Processing]: /templates [Hexagonal Architecture]: http://fideloper.com/hexagonal-architecture [Clean Architecture]: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html @@ -93,16 +93,21 @@ engines. [http_client_jetty], and [http_server_jetty] are examples of this type of module. Adapter names must start with their Port name. +## Composite Port +These modules provide functionality on top of a set of ports combining them, but without relying on +any specific adapters. An example would be the [Web] module that uses [http_server] and +[template][Template Processing] Ports, but leaves clients the decision of picking the adapters they +want. + ## Library -Module that provide functionality that does not depend on different implementations, like [core]. -These modules can depend on several Ports, but never on Adapters. An example would be the [Web] -module that uses [http_server] and [template][Template Processing] Ports, but leaves clients the -decission of picking the adapters they want. +Module that provide functionality that does not depend on different implementations, like [core] and +[handlers]. ## Manager Singleton object to manage a cross toolkit aspect. I.e., Serialization, Logging or Templates. [core]: /core +[handlers]: /handlers [http_server]: /http_server [templates]: /templates @@ -111,12 +116,9 @@ Singleton object to manage a cross toolkit aspect. I.e., Serialization, Logging [http_server_jetty]: /http_server_jetty # Hexagon Extras -The libraries inside the `hexagon_extra` repository provide extra features not bound to different -implementations (rely on ports to work). They will not use dependencies outside the Hexagon -toolkit. +The libraries inside the `hexagon_extra` repository provide extra features. They may be useful to +develop applications, but not strictly required. Some of these modules are: -* [Web]: this module is meant to ease web application development. Provides helpers for - generating HTML and depends on the [HTTP Server] and [Templates] ports. * [Schedulers]: Provides repeated tasks execution based on [Cron] expressions. * [Models]: Contain classes that model common data objects. @@ -138,18 +140,19 @@ How Hexagon fits in your architecture in a picture. # Ports Ports with their provided implementations (Adapters). -| PORT | ADAPTERS | -|-------------------------|--------------------------------------------| -| [HTTP Server] | [Netty], [Netty Epoll], [Jetty], [Servlet] | -| [HTTP Client] | [Jetty][Jetty Client] | -| [Templates] | [Pebble], [FreeMarker], [Rocker] | -| [Serialization Formats] | [JSON], [YAML], [CSV], [XML], [TOML] | +| PORT | ADAPTERS | +|-------------------------|----------------------------------------------------| +| [HTTP Server] | [Netty], [Netty Epoll], [Jetty], [Servlet], [Nima] | +| [HTTP Client] | [Jetty][Jetty Client] | +| [Templates] | [Pebble], [FreeMarker], [Rocker] | +| [Serialization Formats] | [JSON], [YAML], [CSV], [XML], [TOML] | [HTTP Server]: /http_server [Netty]: /http_server_netty [Netty Epoll]: /http_server_netty_epoll [Jetty]: /http_server_jetty [Servlet]: /http_server_servlet +[Nima]: /http_server_nima [HTTP Client]: /http_client [Jetty Client]: /http_client_jetty [Templates]: /templates @@ -168,10 +171,13 @@ Module dependencies (including extra modules): ```mermaid graph TD - http_server -->|uses| http - http_client -->|uses| http + http_handlers -->|uses| http + http_handlers -->|uses| handlers + http_server -->|uses| http_handlers + http_client -->|uses| http_handlers web -->|uses| http_server web -->|uses| templates rest -->|uses| http_server rest -->|uses| serialization + rest_tools -->|uses| rest ``` diff --git a/site/pages/maven.md b/site/pages/maven.md index 4e51a55acb..66cc768347 100644 --- a/site/pages/maven.md +++ b/site/pages/maven.md @@ -10,11 +10,11 @@ on the directory schema you want to use): * The [lean layout POM] ## Standard Parent POM -This layout is the well-known standard one, it has more directories but its widely used. +This layout is the well-known standard one, it has more directories but its widely used. These are +the features it provides: * Set up the Kotlin plugin -* Define Hexagon dependencies' versions -* Use [JUnit 5] and [MockK] for testing +* Use [JUnit 5] and [Kotlin Test] for testing * Configure [Jacoco] coverage report ```xml @@ -47,5 +47,6 @@ using this approach is that it differs of the standard one. [lean layout POM]: https://search.maven.org/search?q=a:kotlin_lean_pom [JUnit 5]: https://junit.org/junit5 +[Kotlin Test]: https://kotlinlang.org/api/latest/kotlin.test/ [MockK]: https://mockk.io [Jacoco]: https://www.eclemma.org/jacoco