diff --git a/build.gradle.kts b/build.gradle.kts index 6bc9c9c99e..55efe295fb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ import org.gradle.api.tasks.wrapper.Wrapper.DistributionType.ALL */ plugins { - kotlin("jvm") version("1.9.20-RC") apply(false) + kotlin("jvm") version("1.9.20") apply(false) id("idea") id("eclipse") @@ -27,8 +27,8 @@ plugins { 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.28") apply(false) - id("io.gitlab.arturbosch.detekt") version("1.23.1") apply(false) - id("me.champeau.jmh") version("0.7.1") apply(false) + id("io.gitlab.arturbosch.detekt") version("1.23.3") apply(false) + id("me.champeau.jmh") version("0.7.2") apply(false) } apply(from = "gradle/certificates.gradle") diff --git a/contributing.md b/contributing.md index 6ba78e0769..85d1a6ee9e 100644 --- a/contributing.md +++ b/contributing.md @@ -138,3 +138,42 @@ git log 1.2.0...1.3.0 \ git log 1.2.0...1.3.0 --date=iso8601 --reverse --pretty=format:'%an %ae'|sort|uniq >>CHANGELOG.md ``` + +## Documentation Guidelines +ONLY public members require documentation. + +Some hints to write the comments are: +* Use the imperative form. +* Capitalize descriptions (parameters, receivers, return types, etc.) and end sentences with a dot. +* Complete all KDoc tags (I.e.: `@param`, `@receiver`, etc. for the methods that have them. +* Focus on what they do, not how, neither what it is. +* Not saying *method* or *property* (that is clear from the context). +* Comments to ignore warnings (false positives) should include an explanation. +* Explain corner cases, default values and allowed values/formats if it applies. +* These are recommendations, feel free to make an exception if you think it is required to explain + the use/structure of the code better. + +## Logging Guidelines +Take care of the level assigned to the log statements: +* `error` some error stopped the correct processing of the request or the process. +* `warn` something failed and was ignored (it wasn't a big deal to stop request or process), but it + could be an issue later or with other data. +* `info` only for really useful information that is not written very often. +* `debug` for information with useful information to diagnose problems or failures (that could be + used to diagnose client code bugs). +* `trace` for low level details that are logged very often (information that could be used to fix + this library's bugs). + +Rules of thumb: +* Prefer to group related information in a single log statement rather than using many of them. +* Do not log re-thrown exceptions. If this is done, chances are that this exception is logged twice + (making diagnosis harder). All not handled exceptions are logged at entry points (main or request + handlers), there is no need to log them in every catch, or where they are thrown. +* Catching an exception doesn't mean it is an error or warning. Assign categories based on the rules + above. +* Generally is a good idea to log the places where the program makes a decision (adding the + information that lead to the program flow selection). I.e.: "User not deleted (not found in + the data store)". +* If some condition leads to default return values, it is a good place to put a logging statement to + add more information about it. I.e.: HTTP Request timeout (), returning + empty array. diff --git a/core/api/core.api b/core/api/core.api index d27decaa7d..cc7684d42d 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -82,6 +82,7 @@ public final class com/hexagonkt/core/DataKt { } public final class com/hexagonkt/core/DatesKt { + public static final fun getGMT_ZONE ()Ljava/time/ZoneId; public static final fun parseDuration (Ljava/lang/String;)Ljava/time/Duration; public static final fun parseLocalDate (Ljava/lang/String;)Ljava/time/LocalDate; public static final fun parsePeriod (Ljava/lang/String;)Ljava/time/Period; @@ -96,11 +97,13 @@ public final class com/hexagonkt/core/DatesKt { public static final fun toNumber (Ljava/time/LocalDate;)I public static final fun toNumber (Ljava/time/LocalDateTime;)J public static final fun toNumber (Ljava/time/LocalTime;)I + public static final fun toTotalDays (Ljava/time/Period;)D public static final fun withZone (Ljava/time/LocalDateTime;Ljava/time/ZoneId;)Ljava/time/ZonedDateTime; public static synthetic fun withZone$default (Ljava/time/LocalDateTime;Ljava/time/ZoneId;ILjava/lang/Object;)Ljava/time/ZonedDateTime; } public final class com/hexagonkt/core/ExceptionsKt { + public static final fun check (Ljava/lang/String;[Lkotlin/jvm/functions/Function0;)V public static final fun filterStackTrace (Ljava/lang/Throwable;Ljava/lang/String;)[Ljava/lang/StackTraceElement; public static final fun getAssertEnabled ()Z public static final fun getFail ()Ljava/lang/Void; @@ -109,7 +112,6 @@ public final class com/hexagonkt/core/ExceptionsKt { } public final class com/hexagonkt/core/HelpersKt { - public static final fun check (Ljava/lang/String;[Lkotlin/jvm/functions/Function0;)V public static final fun exec (Ljava/lang/String;Ljava/io/File;JZ)Ljava/lang/String; public static final fun exec (Ljava/util/List;Ljava/io/File;JZ)Ljava/lang/String; public static synthetic fun exec$default (Ljava/lang/String;Ljava/io/File;JZILjava/lang/Object;)Ljava/lang/String; @@ -141,16 +143,22 @@ public final class com/hexagonkt/core/I18nKt { public final class com/hexagonkt/core/Jvm { public static final field INSTANCE Lcom/hexagonkt/core/Jvm; public final fun getCharset ()Ljava/nio/charset/Charset; + public final fun getConsole ()Ljava/io/Console; public final fun getCpuCount ()I public final fun getHostName ()Ljava/lang/String; public final fun getIp ()Ljava/lang/String; public final fun getLocale ()Ljava/util/Locale; public final fun getLocaleCode ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; + public final fun getOs ()Ljava/lang/String; + public final fun getOsKind ()Lcom/hexagonkt/core/OsKind; public final fun getRuntime ()Ljava/lang/Runtime; public final fun getTimeZone ()Ljava/util/TimeZone; public final fun getVersion ()Ljava/lang/String; public final fun getZoneId ()Ljava/time/ZoneId; + public final fun isConsole ()Z + public final fun loadSystemSettings (Ljava/util/Map;Z)V + public static synthetic fun loadSystemSettings$default (Lcom/hexagonkt/core/Jvm;Ljava/util/Map;ZILjava/lang/Object;)V public final fun systemFlag (Ljava/lang/String;)Z public final fun systemSetting (Lkotlin/reflect/KClass;Ljava/lang/String;)Ljava/lang/Object; public final fun systemSettingOrNull (Lkotlin/reflect/KClass;Ljava/lang/String;)Ljava/lang/Object; @@ -190,6 +198,16 @@ public final class com/hexagonkt/core/NetworkKt { public static final fun urlOf (Ljava/lang/String;)Ljava/net/URL; } +public final class com/hexagonkt/core/OsKind : java/lang/Enum { + public static final field LINUX Lcom/hexagonkt/core/OsKind; + public static final field MACOS Lcom/hexagonkt/core/OsKind; + public static final field UNIX Lcom/hexagonkt/core/OsKind; + public static final field WINDOWS Lcom/hexagonkt/core/OsKind; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/hexagonkt/core/OsKind; + public static fun values ()[Lcom/hexagonkt/core/OsKind; +} + public final class com/hexagonkt/core/ResourceNotFoundException : java/io/IOException { public fun (Ljava/lang/String;)V } @@ -440,14 +458,17 @@ public final class com/hexagonkt/core/security/KeyStoresKt { } public final class com/hexagonkt/core/text/Ansi { + public static final field CSI Ljava/lang/String; + public static final field INSTANCE Lcom/hexagonkt/core/text/Ansi; + public static final field RESET Ljava/lang/String; + public final fun getREGEX ()Lkotlin/text/Regex; +} + +public final class com/hexagonkt/core/text/AnsiColor { public static final field BLACK Ljava/lang/String; public static final field BLACK_BG Ljava/lang/String; - public static final field BLINK Ljava/lang/String; - public static final field BLINK_OFF Ljava/lang/String; public static final field BLUE Ljava/lang/String; public static final field BLUE_BG Ljava/lang/String; - public static final field BOLD Ljava/lang/String; - public static final field BOLD_OFF Ljava/lang/String; public static final field BRIGHT_BLACK Ljava/lang/String; public static final field BRIGHT_BLACK_BG Ljava/lang/String; public static final field BRIGHT_BLUE Ljava/lang/String; @@ -464,28 +485,33 @@ public final class com/hexagonkt/core/text/Ansi { public static final field BRIGHT_WHITE_BG Ljava/lang/String; public static final field BRIGHT_YELLOW Ljava/lang/String; public static final field BRIGHT_YELLOW_BG Ljava/lang/String; - public static final field CSI Ljava/lang/String; public static final field CYAN Ljava/lang/String; public static final field CYAN_BG Ljava/lang/String; public static final field DEFAULT Ljava/lang/String; public static final field DEFAULT_BG Ljava/lang/String; public static final field GREEN Ljava/lang/String; public static final field GREEN_BG Ljava/lang/String; - public static final field INSTANCE Lcom/hexagonkt/core/text/Ansi; - public static final field INVERSE Ljava/lang/String; - public static final field INVERSE_OFF Ljava/lang/String; + public static final field INSTANCE Lcom/hexagonkt/core/text/AnsiColor; public static final field MAGENTA Ljava/lang/String; public static final field MAGENTA_BG Ljava/lang/String; public static final field RED Ljava/lang/String; public static final field RED_BG Ljava/lang/String; - public static final field RESET Ljava/lang/String; - public static final field UNDERLINE Ljava/lang/String; - public static final field UNDERLINE_OFF Ljava/lang/String; public static final field WHITE Ljava/lang/String; public static final field WHITE_BG Ljava/lang/String; public static final field YELLOW Ljava/lang/String; public static final field YELLOW_BG Ljava/lang/String; - public final fun getREGEX ()Lkotlin/text/Regex; +} + +public final class com/hexagonkt/core/text/AnsiEffect { + public static final field BLINK Ljava/lang/String; + public static final field BLINK_OFF Ljava/lang/String; + public static final field BOLD Ljava/lang/String; + public static final field BOLD_OFF Ljava/lang/String; + public static final field INSTANCE Lcom/hexagonkt/core/text/AnsiEffect; + public static final field INVERSE Ljava/lang/String; + public static final field INVERSE_OFF Ljava/lang/String; + public static final field UNDERLINE Ljava/lang/String; + public static final field UNDERLINE_OFF Ljava/lang/String; } public final class com/hexagonkt/core/text/CasesKt { diff --git a/core/src/main/kotlin/com/hexagonkt/core/Dates.kt b/core/src/main/kotlin/com/hexagonkt/core/Dates.kt index 58c0c99b5f..637b35a019 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/Dates.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/Dates.kt @@ -10,6 +10,10 @@ private const val HOUR_OFFSET: Int = 10_000_000 private const val MINUTE_OFFSET: Int = 100_000 private const val SECOND_OFFSET: Int = 1_000 private const val NANO_OFFSET: Int = 1_000_000 +private const val DAYS_PER_MONTH: Double = 30.4375 + +/** GMT zone ID. */ +val GMT_ZONE: ZoneId by lazy { ZoneId.of("GMT") } /** * Convert a date time to a number with the following format: `YYYYMMDDHHmmss`. @@ -143,6 +147,15 @@ fun Date.toLocalDateTime(): LocalDateTime = fun Date.toLocalDate(): LocalDate = this.toLocalDateTime().toLocalDate() +/** + * Calculate the aproximate number of days comprised in a time period. + * + * @receiver Period from which calculate the number of days. + * @return Aproximate number of days of the period. + */ +fun Period.toTotalDays(): Double = + (toTotalMonths() * DAYS_PER_MONTH) + days + /** * Parse a time period allowing a more relaxed format: with spaces or commas, lowercase characters * and not forcing the text to start with 'P'. diff --git a/core/src/main/kotlin/com/hexagonkt/core/Exceptions.kt b/core/src/main/kotlin/com/hexagonkt/core/Exceptions.kt index 144674aa95..d93f8426e3 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/Exceptions.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/Exceptions.kt @@ -44,3 +44,21 @@ fun Throwable.toText(prefix: String = ""): String = this.filterStackTrace(prefix).joinToString(eol, eol) { "\tat $it" } + if (this.cause == null) "" else "${eol}Caused by: " + (this.cause as Throwable).toText(prefix) + +/** + * [TODO](https://github.com/hexagonkt/hexagon/issues/271). + */ +fun check(message: String, vararg blocks: () -> Unit) { + val exceptions: List = blocks.mapNotNull { + try { + it() + null + } + catch(e: Exception) { + e + } + } + + if (exceptions.isNotEmpty()) + throw MultipleException(message, exceptions) +} diff --git a/core/src/main/kotlin/com/hexagonkt/core/Helpers.kt b/core/src/main/kotlin/com/hexagonkt/core/Helpers.kt index e0fdc9d538..b062ca143c 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/Helpers.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/Helpers.kt @@ -22,7 +22,6 @@ fun properties(url: URL): Map = .mapKeys { it.key as String } .mapValues { it.value as String } -// PROCESSES /////////////////////////////////////////////////////////////////////////////////////// /** * Execute a lambda until no exception is thrown or a number of times is reached. * @@ -130,22 +129,3 @@ fun String.shell( ): String = listOf(getenv("SHELL") ?: "bash", "-c", replace("""(\s+\\\s*)?\n""".toRegex(), "")) .exec(workingDirectory, timeout, fail) - -// ERROR HANDLING ////////////////////////////////////////////////////////////////////////////////// -/** - * [TODO](https://github.com/hexagonkt/hexagon/issues/271). - */ -fun check(message: String, vararg blocks: () -> Unit) { - val exceptions: List = blocks.mapNotNull { - try { - it() - null - } - catch(e: Exception) { - e - } - } - - if (exceptions.isNotEmpty()) - throw MultipleException(message, exceptions) -} diff --git a/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt b/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt index aa65650a4a..ad71d12757 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt @@ -1,6 +1,7 @@ package com.hexagonkt.core import com.hexagonkt.core.text.parseOrNull +import java.io.Console import java.net.InetAddress import java.nio.charset.Charset import java.time.ZoneId @@ -12,6 +13,25 @@ import kotlin.reflect.KClass * Object with utilities to gather information about the running JVM. */ object Jvm { + private val systemSettingPattern: Regex by lazy { Regex("[a-zA-Z_]+[a-zA-Z0-9_]*") } + + /** Operating system name ('os.name' property). If `null` throws an exception. */ + val os: String by lazy { os() } + + /** Operating system type. */ + val osKind: OsKind by lazy { osKind() } + + /** + * JVM Console, if the program don't have a console (i.e.: input or output redirected), an + * exception is thrown. + */ + val console: Console by lazy { + System.console() ?: error("Program doesn't have a console (I/O may be redirected)") + } + + /** True if the program has a console (terminal, TTY, PTY...), false if I/O is piped. */ + val isConsole: Boolean by lazy { System.console() != null } + /** Current JVM runtime. */ val runtime: Runtime by lazy { Runtime.getRuntime() } @@ -63,6 +83,21 @@ object Jvm { fun usedMemory(): String = (runtime.totalMemory() - runtime.freeMemory()).let { "%,d".format(it / 1024) } + fun loadSystemSettings(settings: Map, overwrite: Boolean = false) { + settings.keys.forEach { + check(it.matches(systemSettingPattern)) { + "Property name must match $systemSettingPattern ($it)" + } + } + + val systemProperties = System.getProperties() + val properties = + if (overwrite) settings.entries + else settings.entries.filter { !systemProperties.containsKey(it.key) } + + properties.forEach { (k, v) -> System.setProperty(k, v) } + } + /** * Retrieve a setting by name by looking in the JVM system properties first and in OS * environment variables if not found. @@ -106,7 +141,24 @@ object Jvm { systemSetting(T::class, name) private fun systemSettingRaw(name: String): String? { - require(name.isNotBlank()) { "Setting name can not be blank" } + val correctName = name.matches(systemSettingPattern) + require(correctName) { "Setting name must match $systemSettingPattern" } return System.getProperty(name, System.getenv(name)) } + + /** Operating system name ('os.name' property). If `null` throws an exception. */ + internal fun os(): String = + System.getProperty("os.name") ?: error("OS property ('os.name') not found") + + /** Operating system type. */ + internal fun osKind(): OsKind = + os().lowercase().let { + when { + it.contains("win") -> OsKind.WINDOWS + it.contains("mac") -> OsKind.MACOS + it.contains("nux") -> OsKind.LINUX + it.contains("nix") || it.contains("aix") -> OsKind.UNIX + else -> error("Unsupported OS: ${os()}") + } + } } diff --git a/core/src/main/kotlin/com/hexagonkt/core/OsKind.kt b/core/src/main/kotlin/com/hexagonkt/core/OsKind.kt new file mode 100644 index 0000000000..bdcdec0a5c --- /dev/null +++ b/core/src/main/kotlin/com/hexagonkt/core/OsKind.kt @@ -0,0 +1,8 @@ +package com.hexagonkt.core + +enum class OsKind { + WINDOWS, + MACOS, + LINUX, + UNIX, +} diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt b/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt index 3014e51e7c..2e8a0d636f 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt @@ -3,6 +3,8 @@ package com.hexagonkt.core.text /** * Constants for console formatting with [ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code) * codes. They can be used in strings to enable or disable a display option. + * + * TODO Add other sequences (like OSC) */ object Ansi { /** Regex that matches ANSI escape sequences. */ @@ -13,94 +15,4 @@ object Ansi { /** Disable all options applied before. */ const val RESET = "${CSI}0m" - - /** Set black as the foreground color. */ - const val BLACK = "${CSI}30m" - /** Set red as the foreground color. */ - const val RED = "${CSI}31m" - /** Set green as the foreground color. */ - const val GREEN = "${CSI}32m" - /** Set yellow as the foreground color. */ - const val YELLOW = "${CSI}33m" - /** Set blue as the foreground color. */ - const val BLUE = "${CSI}34m" - /** Set magenta as the foreground color. */ - const val MAGENTA = "${CSI}35m" - /** Set cyan as the foreground color. */ - const val CYAN = "${CSI}36m" - /** Set white as the foreground color. */ - const val WHITE = "${CSI}37m" - /** Set back the default foreground color. */ - const val DEFAULT = "${CSI}39m" - - /** Set black as the background color. */ - const val BLACK_BG = "${CSI}40m" - /** Set red as the background color. */ - const val RED_BG = "${CSI}41m" - /** Set green as the background color. */ - const val GREEN_BG = "${CSI}42m" - /** Set yellow as the background color. */ - const val YELLOW_BG = "${CSI}43m" - /** Set blue as the background color. */ - const val BLUE_BG = "${CSI}44m" - /** Set magenta as the background color. */ - const val MAGENTA_BG = "${CSI}45m" - /** Set cyan as the background color. */ - const val CYAN_BG = "${CSI}46m" - /** Set white as the background color. */ - const val WHITE_BG = "${CSI}47m" - /** Set back the default background color. */ - const val DEFAULT_BG = "${CSI}49m" - - /** Set bright black as the foreground color. */ - const val BRIGHT_BLACK = "${CSI}90m" - /** Set bright red as the foreground color. */ - const val BRIGHT_RED = "${CSI}91m" - /** Set bright green as the foreground color. */ - const val BRIGHT_GREEN = "${CSI}92m" - /** Set bright yellow as the foreground color. */ - const val BRIGHT_YELLOW = "${CSI}93m" - /** Set bright blue as the foreground color. */ - const val BRIGHT_BLUE = "${CSI}94m" - /** Set bright magenta as the foreground color. */ - const val BRIGHT_MAGENTA = "${CSI}95m" - /** Set bright cyan as the foreground color. */ - const val BRIGHT_CYAN = "${CSI}96m" - /** Set bright white as the foreground color. */ - const val BRIGHT_WHITE = "${CSI}97m" - - /** Set bright black as the background color. */ - const val BRIGHT_BLACK_BG = "${CSI}100m" - /** Set bright red as the background color. */ - const val BRIGHT_RED_BG = "${CSI}101m" - /** Set bright green as the background color. */ - const val BRIGHT_GREEN_BG = "${CSI}102m" - /** Set bright yellow as the background color. */ - const val BRIGHT_YELLOW_BG = "${CSI}103m" - /** Set bright blue as the background color. */ - const val BRIGHT_BLUE_BG = "${CSI}104m" - /** Set bright magenta as the background color. */ - const val BRIGHT_MAGENTA_BG = "${CSI}105m" - /** Set bright cyan as the background color. */ - const val BRIGHT_CYAN_BG = "${CSI}106m" - /** Set bright white as the background color. */ - const val BRIGHT_WHITE_BG = "${CSI}107m" - - /** Enable bold text. */ - const val BOLD = "${CSI}1m" - /** Enable underline text. */ - const val UNDERLINE = "${CSI}4m" - /** Enable blinking text. */ - const val BLINK = "${CSI}5m" - /** Enable inverse color text. */ - const val INVERSE = "${CSI}7m" - - /** Disable bold text. */ - const val BOLD_OFF = "${CSI}21m" - /** Disable underline text. */ - const val UNDERLINE_OFF = "${CSI}24m" - /** Disable blinking text. */ - const val BLINK_OFF = "${CSI}25m" - /** Disable inverse color text. */ - const val INVERSE_OFF = "${CSI}27m" } diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt new file mode 100644 index 0000000000..7d304a2dd1 --- /dev/null +++ b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt @@ -0,0 +1,76 @@ +package com.hexagonkt.core.text + +// TODO Add RGB colors +object AnsiColor { + /** Set black as the foreground color. */ + const val BLACK = "${Ansi.CSI}30m" + /** Set red as the foreground color. */ + const val RED = "${Ansi.CSI}31m" + /** Set green as the foreground color. */ + const val GREEN = "${Ansi.CSI}32m" + /** Set yellow as the foreground color. */ + const val YELLOW = "${Ansi.CSI}33m" + /** Set blue as the foreground color. */ + const val BLUE = "${Ansi.CSI}34m" + /** Set magenta as the foreground color. */ + const val MAGENTA = "${Ansi.CSI}35m" + /** Set cyan as the foreground color. */ + const val CYAN = "${Ansi.CSI}36m" + /** Set white as the foreground color. */ + const val WHITE = "${Ansi.CSI}37m" + /** Set back the default foreground color. */ + const val DEFAULT = "${Ansi.CSI}39m" + + /** Set black as the background color. */ + const val BLACK_BG = "${Ansi.CSI}40m" + /** Set red as the background color. */ + const val RED_BG = "${Ansi.CSI}41m" + /** Set green as the background color. */ + const val GREEN_BG = "${Ansi.CSI}42m" + /** Set yellow as the background color. */ + const val YELLOW_BG = "${Ansi.CSI}43m" + /** Set blue as the background color. */ + const val BLUE_BG = "${Ansi.CSI}44m" + /** Set magenta as the background color. */ + const val MAGENTA_BG = "${Ansi.CSI}45m" + /** Set cyan as the background color. */ + const val CYAN_BG = "${Ansi.CSI}46m" + /** Set white as the background color. */ + const val WHITE_BG = "${Ansi.CSI}47m" + /** Set back the default background color. */ + const val DEFAULT_BG = "${Ansi.CSI}49m" + + /** Set bright black as the foreground color. */ + const val BRIGHT_BLACK = "${Ansi.CSI}90m" + /** Set bright red as the foreground color. */ + const val BRIGHT_RED = "${Ansi.CSI}91m" + /** Set bright green as the foreground color. */ + const val BRIGHT_GREEN = "${Ansi.CSI}92m" + /** Set bright yellow as the foreground color. */ + const val BRIGHT_YELLOW = "${Ansi.CSI}93m" + /** Set bright blue as the foreground color. */ + const val BRIGHT_BLUE = "${Ansi.CSI}94m" + /** Set bright magenta as the foreground color. */ + const val BRIGHT_MAGENTA = "${Ansi.CSI}95m" + /** Set bright cyan as the foreground color. */ + const val BRIGHT_CYAN = "${Ansi.CSI}96m" + /** Set bright white as the foreground color. */ + const val BRIGHT_WHITE = "${Ansi.CSI}97m" + + /** Set bright black as the background color. */ + const val BRIGHT_BLACK_BG = "${Ansi.CSI}100m" + /** Set bright red as the background color. */ + const val BRIGHT_RED_BG = "${Ansi.CSI}101m" + /** Set bright green as the background color. */ + const val BRIGHT_GREEN_BG = "${Ansi.CSI}102m" + /** Set bright yellow as the background color. */ + const val BRIGHT_YELLOW_BG = "${Ansi.CSI}103m" + /** Set bright blue as the background color. */ + const val BRIGHT_BLUE_BG = "${Ansi.CSI}104m" + /** Set bright magenta as the background color. */ + const val BRIGHT_MAGENTA_BG = "${Ansi.CSI}105m" + /** Set bright cyan as the background color. */ + const val BRIGHT_CYAN_BG = "${Ansi.CSI}106m" + /** Set bright white as the background color. */ + const val BRIGHT_WHITE_BG = "${Ansi.CSI}107m" +} diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt new file mode 100644 index 0000000000..a8c98f1584 --- /dev/null +++ b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt @@ -0,0 +1,22 @@ +package com.hexagonkt.core.text + +// TODO Add '2', '3', '6' and '9' codes +object AnsiEffect { + /** Enable bold text. */ + const val BOLD = "${Ansi.CSI}1m" + /** Enable underline text. */ + const val UNDERLINE = "${Ansi.CSI}4m" + /** Enable blinking text. */ + const val BLINK = "${Ansi.CSI}5m" + /** Enable inverse color text. */ + const val INVERSE = "${Ansi.CSI}7m" + + /** Disable bold text. */ + const val BOLD_OFF = "${Ansi.CSI}21m" + /** Disable underline text. */ + const val UNDERLINE_OFF = "${Ansi.CSI}24m" + /** Disable blinking text. */ + const val BLINK_OFF = "${Ansi.CSI}25m" + /** Disable inverse color text. */ + const val INVERSE_OFF = "${Ansi.CSI}27m" +} diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/Strings.kt b/core/src/main/kotlin/com/hexagonkt/core/text/Strings.kt index 792ae322ae..9cbc279eda 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/text/Strings.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/text/Strings.kt @@ -233,6 +233,9 @@ fun String.banner(bannerDelimiter: String = "*"): String = ) .let { "$it$eol$this$eol$it" } +// TODO Add `box` (create a rectangle text) and doubleSpace (add a space between letters) +// TODO These and other implemented methods can fit in a Effects.kt file + /** * [TODO](https://github.com/hexagonkt/hexagon/issues/271). * diff --git a/core/src/test/kotlin/com/hexagonkt/core/DatesTest.kt b/core/src/test/kotlin/com/hexagonkt/core/DatesTest.kt index 1f2dc9c74d..250989327c 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/DatesTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/DatesTest.kt @@ -52,6 +52,11 @@ internal class DatesTest { assertFailsWith { (-1).toLocalTime() } } + @Test fun `Period days are calculated correctly`() { + assertEquals(1.0, parsePeriod("1d").toTotalDays()) + assertEquals(403.6875, parsePeriod("1y 1m 1w 1d").toTotalDays()) + } + @Test fun `Parse periods work with not standard input`() { assertEquals(Period.parse("P1Y1W1D"), parsePeriod("P1Y1W1D")) assertEquals(Period.parse("P1Y1W1D"), parsePeriod("p1y1w1d")) diff --git a/core/src/test/kotlin/com/hexagonkt/core/ExceptionsTest.kt b/core/src/test/kotlin/com/hexagonkt/core/ExceptionsTest.kt index a42d717d59..bf8d445824 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/ExceptionsTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/ExceptionsTest.kt @@ -58,4 +58,26 @@ internal class ExceptionsTest { assert(filteredTrace.contains("\tat ${ExceptionsTest::class.java.name}")) assertFalse(filteredTrace.contains("\tat org.junit.platform")) } + + @Test fun `Check multiple errors`() { + val e = assertFailsWith { + check( + "Test multiple exceptions", + { require(false) { "Sample error" } }, + { println("Good block") }, + { error("Bad state") }, + ) + } + + assertEquals("Test multiple exceptions", e.message) + assertEquals(2, e.causes.size) + assertEquals("Sample error", e.causes[0].message) + assertEquals("Bad state", e.causes[1].message) + + check( + "No exception thrown", + { println("Good block") }, + { println("Shouldn't throw an exception") }, + ) + } } diff --git a/core/src/test/kotlin/com/hexagonkt/core/HelpersTest.kt b/core/src/test/kotlin/com/hexagonkt/core/HelpersTest.kt index 7f90acdb3b..cca454be40 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/HelpersTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/HelpersTest.kt @@ -22,28 +22,6 @@ internal class HelpersTest { } } - @Test fun `Check multiple errors`() { - val e = assertFailsWith { - check( - "Test multiple exceptions", - { require(false) { "Sample error" } }, - { println("Good block") }, - { error("Bad state") }, - ) - } - - assertEquals("Test multiple exceptions", e.message) - assertEquals(2, e.causes.size) - assertEquals("Sample error", e.causes[0].message) - assertEquals("Bad state", e.causes[1].message) - - check( - "No exception thrown", - { println("Good block") }, - { println("Shouldn't throw an exception") }, - ) - } - @Test fun `Process execution works as expected`() { assertFailsWith { " ".exec() } assertFailsWith { "echo test".exec(timeout = -1) } diff --git a/core/src/test/kotlin/com/hexagonkt/core/JvmTest.kt b/core/src/test/kotlin/com/hexagonkt/core/JvmTest.kt index cd739a7b80..3e6dc9cc75 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/JvmTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/JvmTest.kt @@ -10,10 +10,79 @@ import kotlin.test.* internal class JvmTest { - @Test fun `Jvm utilities`() { - // jvm - // TODO - // jvm + @Test fun `Console availability works ok`() { + // From tests, you never have a TTY, positive case hard to test + assertFalse(Jvm.isConsole) + assertEquals( + "Program doesn't have a console (I/O may be redirected)", + assertFailsWith { Jvm.console }.message + ) + } + + @Test fun `System settings are loaded properly properly`() { + mapOf("s1" to "v1", "s2" to "v2").forEach { (k, v) -> System.setProperty(k, v) } + + Jvm.loadSystemSettings(mapOf("s1" to "x1", "s2" to "x2")) + assertEquals("v1", System.getProperty("s1")) + assertEquals("v2", System.getProperty("s2")) + + Jvm.loadSystemSettings(mapOf("s1" to "x1", "s2" to "x2", "s3" to "x3")) + assertEquals("v1", System.getProperty("s1")) + assertEquals("v2", System.getProperty("s2")) + assertEquals("x3", System.getProperty("s3")) + + Jvm.loadSystemSettings(mapOf("s1" to "x1", "s2" to "x2"), true) + assertEquals("x1", System.getProperty("s1")) + assertEquals("x2", System.getProperty("s2")) + + Jvm.loadSystemSettings(mapOf("s1" to "z1", "s2" to "z2", "s3" to "z3"), true) + assertEquals("z1", System.getProperty("s1")) + assertEquals("z2", System.getProperty("s2")) + assertEquals("z3", System.getProperty("s3")) + + val e = assertFailsWith { Jvm.loadSystemSettings(mapOf("1" to "v")) } + assertEquals("Property name must match [a-zA-Z_]+[a-zA-Z0-9_]* (1)", e.message) + } + + @Test fun `OS kind is fetched properly`() { + val os = Jvm.os + assert(os.isNotBlank()) + assert(Jvm.osKind in OsKind.entries) + + System.clearProperty("os.name") + assertEquals( + "OS property ('os.name') not found", + assertFailsWith { Jvm.os() }.message + ) + + System.setProperty("os.name", "MS-DOS") + assertEquals( + "Unsupported OS: MS-DOS", + assertFailsWith { Jvm.osKind() }.message + ) + + checkOsKind("Windows", OsKind.WINDOWS) + checkOsKind("windows", OsKind.WINDOWS) + checkOsKind("win", OsKind.WINDOWS) + checkOsKind("Win", OsKind.WINDOWS) + + checkOsKind("macOS", OsKind.MACOS) + checkOsKind("macos", OsKind.MACOS) + checkOsKind("mac", OsKind.MACOS) + checkOsKind("MAC", OsKind.MACOS) + + checkOsKind("Linux", OsKind.LINUX) + checkOsKind("linux", OsKind.LINUX) + checkOsKind("Debian Linux", OsKind.LINUX) + checkOsKind("debian linux", OsKind.LINUX) + + checkOsKind("aix", OsKind.UNIX) + checkOsKind("AIX", OsKind.UNIX) + checkOsKind("IBM AIX", OsKind.UNIX) + checkOsKind("BSD Unix", OsKind.UNIX) + checkOsKind("bsd unix", OsKind.UNIX) + + System.setProperty("os.name", os) } @Test fun `'systemFlag' fails with a blank setting name`() { @@ -21,6 +90,8 @@ internal class JvmTest { assertFailsWith { Jvm.systemFlag(" ") } assertFailsWith { Jvm.systemSettingOrNull("") } assertFailsWith { Jvm.systemSettingOrNull(" ") } + assertFailsWith { Jvm.systemSettingOrNull("1") } + assertFailsWith { Jvm.systemSettingOrNull("#") } } @Test fun `'systemFlag' returns true on defined boolean parameter`() { @@ -147,4 +218,9 @@ internal class JvmTest { System.setProperty("PATH", "path override") assert(Jvm.systemSetting("PATH") == "path override") } + + private fun checkOsKind(osName: String, osKind: OsKind) { + System.setProperty("os.name", osName) + assertEquals(osKind, Jvm.osKind()) + } } diff --git a/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt b/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt index 1b5b542fe2..f13c6d771a 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt @@ -1,20 +1,15 @@ package com.hexagonkt.core.text import com.hexagonkt.core.logging.Logger +import com.hexagonkt.core.text.AnsiColor.BLACK +import com.hexagonkt.core.text.AnsiColor.BLUE_BG +import com.hexagonkt.core.text.AnsiEffect.UNDERLINE import org.junit.jupiter.api.Test internal class AnsiTest { private val logger: Logger by lazy { Logger(this::class) } - @Test fun `Ansi utilities`() { - // ansi - // TODO - // Apply effects - // Strip ANSI escape codes (stripAnsi) - // ansi - } - @Test fun `ANSI codes are printed properly`() { fun test(message: String) { @@ -22,54 +17,54 @@ internal class AnsiTest { } test("${Ansi.CSI}30m black") - test("${Ansi.BLACK}black") - test("${Ansi.RED}red") - test("${Ansi.GREEN}green") - test("${Ansi.YELLOW}yellow") - test("${Ansi.BLUE}blue") - test("${Ansi.MAGENTA}magenta") - test("${Ansi.CYAN}cyan") - test("${Ansi.WHITE}white") - test("${Ansi.DEFAULT}default") + test("${BLACK}black") + test("${AnsiColor.RED}red") + test("${AnsiColor.GREEN}green") + test("${AnsiColor.YELLOW}yellow") + test("${AnsiColor.BLUE}blue") + test("${AnsiColor.MAGENTA}magenta") + test("${AnsiColor.CYAN}cyan") + test("${AnsiColor.WHITE}white") + test("${AnsiColor.DEFAULT}default") - test("${Ansi.BLACK_BG}${Ansi.BRIGHT_BLACK}black bg") - test("${Ansi.RED_BG}${Ansi.BRIGHT_BLACK}red bg") - test("${Ansi.GREEN_BG}${Ansi.BRIGHT_BLACK}green bg") - test("${Ansi.YELLOW_BG}${Ansi.BRIGHT_BLACK}yellow bg") - test("${Ansi.BLUE_BG}${Ansi.BRIGHT_BLACK}blue bg") - test("${Ansi.MAGENTA_BG}${Ansi.BRIGHT_BLACK}magenta bg") - test("${Ansi.CYAN_BG}${Ansi.BRIGHT_BLACK}cyan bg") - test("${Ansi.WHITE_BG}${Ansi.BRIGHT_BLACK}white bg") - test("${Ansi.DEFAULT_BG}${Ansi.BRIGHT_BLACK}default bg") + test("${AnsiColor.BLACK_BG}${AnsiColor.BRIGHT_BLACK}black bg") + test("${AnsiColor.RED_BG}${AnsiColor.BRIGHT_BLACK}red bg") + test("${AnsiColor.GREEN_BG}${AnsiColor.BRIGHT_BLACK}green bg") + test("${AnsiColor.YELLOW_BG}${AnsiColor.BRIGHT_BLACK}yellow bg") + test("$BLUE_BG${AnsiColor.BRIGHT_BLACK}blue bg") + test("${AnsiColor.MAGENTA_BG}${AnsiColor.BRIGHT_BLACK}magenta bg") + test("${AnsiColor.CYAN_BG}${AnsiColor.BRIGHT_BLACK}cyan bg") + test("${AnsiColor.WHITE_BG}${AnsiColor.BRIGHT_BLACK}white bg") + test("${AnsiColor.DEFAULT_BG}${AnsiColor.BRIGHT_BLACK}default bg") - test("${Ansi.BRIGHT_BLACK}bright black") - test("${Ansi.BRIGHT_RED}bright red") - test("${Ansi.BRIGHT_GREEN}bright green") - test("${Ansi.BRIGHT_YELLOW}bright yellow") - test("${Ansi.BRIGHT_BLUE}bright blue") - test("${Ansi.BRIGHT_MAGENTA}bright magenta") - test("${Ansi.BRIGHT_CYAN}bright cyan") - test("${Ansi.BRIGHT_WHITE}bright white") + test("${AnsiColor.BRIGHT_BLACK}bright black") + test("${AnsiColor.BRIGHT_RED}bright red") + test("${AnsiColor.BRIGHT_GREEN}bright green") + test("${AnsiColor.BRIGHT_YELLOW}bright yellow") + test("${AnsiColor.BRIGHT_BLUE}bright blue") + test("${AnsiColor.BRIGHT_MAGENTA}bright magenta") + test("${AnsiColor.BRIGHT_CYAN}bright cyan") + test("${AnsiColor.BRIGHT_WHITE}bright white") - test("${Ansi.BRIGHT_BLACK_BG}${Ansi.BRIGHT_BLACK}bright black bg") - test("${Ansi.BRIGHT_RED_BG}${Ansi.BRIGHT_BLACK}bright red bg") - test("${Ansi.BRIGHT_GREEN_BG}${Ansi.BRIGHT_BLACK}bright green bg") - test("${Ansi.BRIGHT_YELLOW_BG}${Ansi.BRIGHT_BLACK}bright yellow bg") - test("${Ansi.BRIGHT_BLUE_BG}${Ansi.BRIGHT_BLACK}bright blue bg") - test("${Ansi.BRIGHT_MAGENTA_BG}${Ansi.BRIGHT_BLACK}bright magenta bg") - test("${Ansi.BRIGHT_CYAN_BG}${Ansi.BRIGHT_BLACK}bright cyan bg") - test("${Ansi.BRIGHT_WHITE_BG}${Ansi.BRIGHT_BLACK}bright white bg") + test("${AnsiColor.BRIGHT_BLACK_BG}${AnsiColor.BRIGHT_BLACK}bright black bg") + test("${AnsiColor.BRIGHT_RED_BG}${AnsiColor.BRIGHT_BLACK}bright red bg") + test("${AnsiColor.BRIGHT_GREEN_BG}${AnsiColor.BRIGHT_BLACK}bright green bg") + test("${AnsiColor.BRIGHT_YELLOW_BG}${AnsiColor.BRIGHT_BLACK}bright yellow bg") + test("${AnsiColor.BRIGHT_BLUE_BG}${AnsiColor.BRIGHT_BLACK}bright blue bg") + test("${AnsiColor.BRIGHT_MAGENTA_BG}${AnsiColor.BRIGHT_BLACK}bright magenta bg") + test("${AnsiColor.BRIGHT_CYAN_BG}${AnsiColor.BRIGHT_BLACK}bright cyan bg") + test("${AnsiColor.BRIGHT_WHITE_BG}${AnsiColor.BRIGHT_BLACK}bright white bg") - test("${Ansi.BOLD}bold") - test("${Ansi.UNDERLINE}underline") - test("${Ansi.BLINK}blink") - test("${Ansi.INVERSE}inverse") + test("${AnsiEffect.BOLD}bold") + test("${UNDERLINE}underline") + test("${AnsiEffect.BLINK}blink") + test("${AnsiEffect.INVERSE}inverse") - test("${Ansi.BOLD_OFF}bold off") - test("${Ansi.UNDERLINE_OFF}underline off") - test("${Ansi.BLINK_OFF}blink off") - test("${Ansi.INVERSE_OFF}inverse off") + test("${AnsiEffect.BOLD_OFF}bold off") + test("${AnsiEffect.UNDERLINE_OFF}underline off") + test("${AnsiEffect.BLINK_OFF}blink off") + test("${AnsiEffect.INVERSE_OFF}inverse off") - test("${Ansi.BLACK}${Ansi.BLUE_BG}${Ansi.UNDERLINE}black fg blue bg underline") + test("$BLACK$BLUE_BG${UNDERLINE}black fg blue bg underline") } } diff --git a/core/src/test/kotlin/com/hexagonkt/core/text/StringsTest.kt b/core/src/test/kotlin/com/hexagonkt/core/text/StringsTest.kt index 4d458b7495..3fff5af84f 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/text/StringsTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/text/StringsTest.kt @@ -1,5 +1,9 @@ package com.hexagonkt.core.text +import com.hexagonkt.core.text.Ansi.RESET +import com.hexagonkt.core.text.AnsiColor.BRIGHT_WHITE +import com.hexagonkt.core.text.AnsiColor.RED_BG +import com.hexagonkt.core.text.AnsiEffect.UNDERLINE import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.condition.DisabledInNativeImage @@ -198,7 +202,7 @@ internal class StringsTest { } @Test fun `ANSI testing`() { - val message = "${Ansi.RED_BG}${Ansi.BRIGHT_WHITE}${Ansi.UNDERLINE}ANSI${Ansi.RESET} normal" + val message = "$RED_BG$BRIGHT_WHITE${UNDERLINE}ANSI$RESET normal" val noAnsiMessage = message.stripAnsi() assertNotEquals(message, noAnsiMessage) assertContentEquals(noAnsiMessage.toByteArray(), "ANSI normal".toByteArray()) diff --git a/gradle.properties b/gradle.properties index c43ac2bab6..a4c3e6f5de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.warning.mode=all org.gradle.console=plain # Gradle -version=3.4.0 +version=3.4.1 group=com.hexagonkt description=The atoms of your platform @@ -31,13 +31,13 @@ logoSmall=assets/img/logo.svg iconsDirectory=content # VERSIONS -kotlinVersion=1.9.20-RC +kotlinVersion=1.9.20 dokkaVersion=1.9.10 mockkVersion=1.13.8 junitVersion=5.10.0 gatlingVersion=3.9.5 jmhVersion=1.37 -mkdocsMaterialVersion=9.4.6 +mkdocsMaterialVersion=9.4.7 mermaidDokkaVersion=0.4.4 nativeToolsVersion=0.9.28 @@ -46,11 +46,11 @@ nettyVersion=4.1.100.Final nettyTcNativeVersion=2.0.62.Final # http_server_helidon -helidonVersion=4.0.0-RC1 +helidonVersion=4.0.0 # http_server_servlet servletVersion=6.0.0 -jettyVersion=12.0.2 +jettyVersion=12.0.3 # rest_tools swaggerValidatorVersion=2.38.0 @@ -67,7 +67,7 @@ dslJsonVersion=2.0.2 freemarkerVersion=2.3.32 # templates_jte -jteVersion=3.1.3 +jteVersion=3.1.4 # templates_pebble pebbleVersion=3.2.1 diff --git a/http/http/api/http.api b/http/http/api/http.api index e9c83aa105..4400dc7aa7 100644 --- a/http/http/api/http.api +++ b/http/http/api/http.api @@ -1,12 +1,7 @@ public final class com/hexagonkt/http/HttpKt { - public static final fun bodyToBytes (Ljava/lang/Object;)[B public static final fun checkHeaders (Lcom/hexagonkt/http/model/Headers;)V public static final fun formatQueryString (Lcom/hexagonkt/http/model/QueryParameters;)Ljava/lang/String; - public static final fun getBODY_TYPES ()Ljava/util/Set; - public static final fun getBODY_TYPES_NAMES ()Ljava/lang/String; public static final fun getCHECKED_HEADERS ()Ljava/util/List; - public static final fun getGMT_ZONE ()Ljava/time/ZoneId; - public static final fun getHTTP_DATE_FORMATTER ()Ljava/time/format/DateTimeFormatter; public static final fun parseContentType (Ljava/lang/String;)Lcom/hexagonkt/http/model/ContentType; public static final fun parseQueryString (Ljava/lang/String;)Lcom/hexagonkt/http/model/QueryParameters; public static final fun toHttpFormat (Ljava/time/Instant;)Ljava/lang/String; diff --git a/http/http/src/main/kotlin/com/hexagonkt/http/Http.kt b/http/http/src/main/kotlin/com/hexagonkt/http/Http.kt index 9f73c1d7ec..1cc564049f 100644 --- a/http/http/src/main/kotlin/com/hexagonkt/http/Http.kt +++ b/http/http/src/main/kotlin/com/hexagonkt/http/Http.kt @@ -1,30 +1,24 @@ package com.hexagonkt.http +import com.hexagonkt.core.GMT_ZONE import com.hexagonkt.core.assertEnabled import com.hexagonkt.core.Jvm -import com.hexagonkt.core.logging.Logger import com.hexagonkt.core.media.MediaType import com.hexagonkt.http.model.* -import java.lang.IllegalStateException -import java.math.BigInteger import java.net.URLDecoder import java.net.URLEncoder import java.nio.charset.Charset import java.time.* +import java.time.ZoneOffset.UTC import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME -val CHECKED_HEADERS: List = listOf("content-type", "accept", "set-cookie", "authorization") - -val GMT_ZONE: ZoneId = ZoneId.of("GMT") - -val HTTP_DATE_FORMATTER: DateTimeFormatter = RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC) - -val BODY_TYPES = setOf(String::class, ByteArray::class, Int::class, Long::class) - -val BODY_TYPES_NAMES = BODY_TYPES.joinToString(", ") { it.simpleName.toString() } +/** Headers handled by HTTP model as headers with special meaning. */ +val CHECKED_HEADERS: List by lazy { + listOf("content-type", "accept", "set-cookie", "authorization") +} -private val logger: Logger = Logger(SslSettings::class.java.packageName) +internal val HTTP_DATE_FORMATTER: DateTimeFormatter by lazy { RFC_1123_DATE_TIME.withZone(UTC) } fun checkHeaders(headers: Headers) { if (!assertEnabled) @@ -121,19 +115,3 @@ fun parseContentType(contentType: String): ContentType { else -> error("Invalid content type format: $contentType") } } - -fun bodyToBytes(body: Any): ByteArray = - when (body) { - is String -> body.toByteArray() - is ByteArray -> body - is Int -> BigInteger.valueOf(body.toLong()).toByteArray() - is Long -> BigInteger.valueOf(body).toByteArray() - else -> { - val className = body.javaClass.simpleName - val message = "Unsupported body type: $className. Must be: $BODY_TYPES_NAMES" - val exception = IllegalStateException(message) - - logger.error(exception) - throw exception - } - } diff --git a/http/http/src/test/kotlin/com/hexagonkt/http/HttpTest.kt b/http/http/src/test/kotlin/com/hexagonkt/http/HttpTest.kt index 4941f5617f..0fa0e3afa3 100644 --- a/http/http/src/test/kotlin/com/hexagonkt/http/HttpTest.kt +++ b/http/http/src/test/kotlin/com/hexagonkt/http/HttpTest.kt @@ -1,14 +1,13 @@ package com.hexagonkt.http +import com.hexagonkt.core.GMT_ZONE import com.hexagonkt.core.Jvm import com.hexagonkt.http.model.Header import com.hexagonkt.http.model.QueryParameters import com.hexagonkt.http.model.Headers import com.hexagonkt.http.model.QueryParameter import org.junit.jupiter.api.Test -import java.math.BigInteger import java.time.* -import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -34,15 +33,6 @@ internal class HttpTest { testParseFormat("a+=+b+", "a%20=%20b%20") } - @Test fun `Basic types can be converted to byte arrays to be sent as bodies`() { - assertContentEquals("text".toByteArray(), bodyToBytes("text")) - assertContentEquals("text".toByteArray(), bodyToBytes("text".toByteArray())) - assertContentEquals(BigInteger.valueOf(42).toByteArray(), bodyToBytes(42)) - assertContentEquals(BigInteger.valueOf(1_234_567L).toByteArray(), bodyToBytes(1_234_567L)) - - assertFailsWith { bodyToBytes(LocalDate.now()) } - } - @Test fun `Check headers fails when using reserved headers when not in production mode` () { val forbiddenHeaders = listOf("content-type", "accept", "set-cookie") .map { Headers(Header(it, "value")) } 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 c6f7eecbc0..6a776f44d3 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 @@ -2,7 +2,7 @@ package com.hexagonkt.http.client.jetty import com.hexagonkt.core.media.TEXT_EVENT_STREAM import com.hexagonkt.core.security.loadKeyStore -import com.hexagonkt.http.bodyToBytes +import com.hexagonkt.http.handlers.bodyToBytes import com.hexagonkt.http.CHECKED_HEADERS import com.hexagonkt.http.client.HttpClient import com.hexagonkt.http.client.HttpClientPort diff --git a/http/http_handlers/api/http_handlers.api b/http/http_handlers/api/http_handlers.api index ea015f6216..fec1a4d64a 100644 --- a/http/http_handlers/api/http_handlers.api +++ b/http/http_handlers/api/http_handlers.api @@ -160,6 +160,7 @@ public final class com/hexagonkt/http/handlers/HandlersKt { public static synthetic fun Trace$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/hexagonkt/http/handlers/OnHandler; public static final fun Ws (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/hexagonkt/http/handlers/OnHandler; public static synthetic fun Ws$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/hexagonkt/http/handlers/OnHandler; + public static final fun bodyToBytes (Ljava/lang/Object;)[B public static final fun path (Ljava/lang/String;Ljava/util/List;)Lcom/hexagonkt/http/handlers/PathHandler; public static final fun path (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/hexagonkt/http/handlers/PathHandler; public static synthetic fun path$default (Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/hexagonkt/http/handlers/PathHandler; diff --git a/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/Handlers.kt b/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/Handlers.kt index e4d94eb81e..3316a4deaf 100644 --- a/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/Handlers.kt +++ b/http/http_handlers/src/main/kotlin/com/hexagonkt/http/handlers/Handlers.kt @@ -2,12 +2,15 @@ package com.hexagonkt.http.handlers +import com.hexagonkt.core.logging.Logger import com.hexagonkt.handlers.Context import com.hexagonkt.http.model.* import com.hexagonkt.http.model.HttpMethod.* import com.hexagonkt.http.model.HttpProtocol.HTTP import com.hexagonkt.http.model.HttpCall import com.hexagonkt.http.model.HttpRequest +import java.lang.IllegalStateException +import java.math.BigInteger import java.security.cert.X509Certificate import kotlin.reflect.KClass import kotlin.reflect.cast @@ -15,6 +18,12 @@ import kotlin.reflect.cast typealias HttpCallback = HttpContext.() -> HttpContext typealias HttpExceptionCallback = HttpContext.(T) -> HttpContext +private val logger: Logger by lazy { Logger(HttpHandler::class.java.packageName) } +private val BODY_TYPES_NAMES: String by lazy { + val bodyTypes = setOf(String::class, ByteArray::class, Int::class, Long::class) + bodyTypes.joinToString(", ") { it.simpleName.toString() } +} + internal fun toCallback(block: HttpCallback): (Context) -> Context = { context -> HttpContext(context).block() } @@ -127,3 +136,19 @@ fun Options(pattern: String = "", callback: HttpCallback): OnHandler = fun Patch(pattern: String = "", callback: HttpCallback): OnHandler = OnHandler(PATCH, pattern, callback) + +fun bodyToBytes(body: Any): ByteArray = + when (body) { + is String -> body.toByteArray() + is ByteArray -> body + is Int -> BigInteger.valueOf(body.toLong()).toByteArray() + is Long -> BigInteger.valueOf(body).toByteArray() + else -> { + val className = body.javaClass.simpleName + val message = "Unsupported body type: $className. Must be: $BODY_TYPES_NAMES" + val exception = IllegalStateException(message) + + logger.error(exception) + throw exception + } + } diff --git a/http/http_handlers/src/test/kotlin/com/hexagonkt/http/handlers/HandlersTest.kt b/http/http_handlers/src/test/kotlin/com/hexagonkt/http/handlers/HandlersTest.kt index 24f97eb165..b278a61173 100644 --- a/http/http_handlers/src/test/kotlin/com/hexagonkt/http/handlers/HandlersTest.kt +++ b/http/http_handlers/src/test/kotlin/com/hexagonkt/http/handlers/HandlersTest.kt @@ -5,9 +5,12 @@ import com.hexagonkt.http.model.INTERNAL_SERVER_ERROR_500 import com.hexagonkt.http.model.NOT_FOUND_404 import com.hexagonkt.http.model.OK_200 import org.junit.jupiter.api.Test +import java.math.BigInteger +import java.time.LocalDate import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.reflect.KClass +import kotlin.test.assertContentEquals import kotlin.test.assertNull internal class HandlersTest { @@ -94,6 +97,15 @@ internal class HandlersTest { } } + @Test fun `Basic types can be converted to byte arrays to be sent as bodies`() { + assertContentEquals("text".toByteArray(), bodyToBytes("text")) + assertContentEquals("text".toByteArray(), bodyToBytes("text".toByteArray())) + assertContentEquals(BigInteger.valueOf(42).toByteArray(), bodyToBytes(42)) + assertContentEquals(BigInteger.valueOf(1_234_567L).toByteArray(), bodyToBytes(1_234_567L)) + + assertFailsWith { bodyToBytes(LocalDate.now()) } + } + private fun PathHandler.handlersPredicates(): List = handlers.map { it.handlerPredicate } } 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 c5ecaff2c7..0b6a1382e5 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 @@ -10,13 +10,13 @@ import com.hexagonkt.core.Jvm.localeCode import com.hexagonkt.http.model.HttpProtocol.HTTP2 import java.lang.Runtime.getRuntime -import com.hexagonkt.core.text.Ansi.BLUE -import com.hexagonkt.core.text.Ansi.BOLD -import com.hexagonkt.core.text.Ansi.CYAN -import com.hexagonkt.core.text.Ansi.DEFAULT -import com.hexagonkt.core.text.Ansi.MAGENTA +import com.hexagonkt.core.text.AnsiColor.BLUE +import com.hexagonkt.core.text.AnsiColor.CYAN +import com.hexagonkt.core.text.AnsiColor.DEFAULT +import com.hexagonkt.core.text.AnsiColor.MAGENTA import com.hexagonkt.core.text.Ansi.RESET -import com.hexagonkt.core.text.Ansi.UNDERLINE +import com.hexagonkt.core.text.AnsiEffect.BOLD +import com.hexagonkt.core.text.AnsiEffect.UNDERLINE import com.hexagonkt.core.Jvm.timeZone import com.hexagonkt.core.Jvm.totalMemory import com.hexagonkt.core.Jvm.usedMemory diff --git a/http/http_server_helidon/src/main/kotlin/com/hexagonkt/http/server/helidon/HelidonServerAdapter.kt b/http/http_server_helidon/src/main/kotlin/com/hexagonkt/http/server/helidon/HelidonServerAdapter.kt index 88165a2ff4..09bc9ab159 100644 --- a/http/http_server_helidon/src/main/kotlin/com/hexagonkt/http/server/helidon/HelidonServerAdapter.kt +++ b/http/http_server_helidon/src/main/kotlin/com/hexagonkt/http/server/helidon/HelidonServerAdapter.kt @@ -4,7 +4,7 @@ import com.hexagonkt.core.fieldsMapOf import com.hexagonkt.core.security.loadKeyStore import com.hexagonkt.core.toText import com.hexagonkt.http.SslSettings -import com.hexagonkt.http.bodyToBytes +import com.hexagonkt.http.handlers.bodyToBytes import com.hexagonkt.http.handlers.HttpHandler import com.hexagonkt.http.model.HttpProtocol import com.hexagonkt.http.model.HttpProtocol.* diff --git a/http/http_server_helidon/src/test/kotlin/com/hexagonkt/http/server/helidon/AdapterExamplesTest.kt b/http/http_server_helidon/src/test/kotlin/com/hexagonkt/http/server/helidon/AdapterExamplesTest.kt index 37285d13a7..4ee5055248 100644 --- a/http/http_server_helidon/src/test/kotlin/com/hexagonkt/http/server/helidon/AdapterExamplesTest.kt +++ b/http/http_server_helidon/src/test/kotlin/com/hexagonkt/http/server/helidon/AdapterExamplesTest.kt @@ -1,10 +1,6 @@ package com.hexagonkt.http.server.helidon -import com.hexagonkt.core.urlOf -import com.hexagonkt.http.client.HttpClient -import com.hexagonkt.http.client.HttpClientSettings import com.hexagonkt.http.client.jetty.JettyClientAdapter -import com.hexagonkt.http.server.HttpServer import com.hexagonkt.http.test.examples.* import com.hexagonkt.serialization.jackson.JacksonTextFormat import com.hexagonkt.serialization.jackson.json.Json @@ -13,8 +9,6 @@ import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.OS.WINDOWS -import kotlin.test.assertEquals -import kotlin.test.assertNull val clientAdapter: () -> JettyClientAdapter = ::JettyClientAdapter val serverAdapter: () -> HelidonServerAdapter = ::HelidonServerAdapter @@ -24,34 +18,12 @@ internal class AdapterBooksTest : BooksTest(clientAdapter, serverAdapter) internal class AdapterErrorsTest : ErrorsTest(clientAdapter, serverAdapter) internal class AdapterFiltersTest : FiltersTest(clientAdapter, serverAdapter) internal class AdapterClientTest : ClientTest(clientAdapter, serverAdapter, formats) { + // TODO Fix this case @Test @Disabled override fun `Form parameters are sent correctly`() {} } @DisabledOnOs(WINDOWS) // TODO Make this work on GitHub runners internal class AdapterHttpsTest : HttpsTest(clientAdapter, serverAdapter) -internal class AdapterZipTest : ZipTest(clientAdapter, serverAdapter) { - @Test override fun `Use ZIP encoding without enabling the feature example`() { - - val server = HttpServer(serverAdapter(), serverSettings.copy(bindPort = 0)) { - get("/hello") { - ok("Hello World!") - } - } - server.start() - - val settings = HttpClientSettings(urlOf("http://localhost:${server.runtimePort}")) - val client = HttpClient(clientAdapter(), settings) - client.start() - - client.get("/hello").apply { - assertEquals(body, "Hello World!") - assertNull(headers["content-encoding"]) - assertNull(headers["Content-Encoding"]) - } - - client.stop() - server.stop() - } -} +internal class AdapterZipTest : ZipTest(clientAdapter, serverAdapter) internal class AdapterCookiesTest : CookiesTest(clientAdapter, serverAdapter) internal class AdapterFilesTest : FilesTest(clientAdapter, serverAdapter) internal class AdapterCorsTest : CorsTest(clientAdapter, serverAdapter) diff --git a/http/http_server_netty/src/main/kotlin/com/hexagonkt/http/server/netty/NettyServerHandler.kt b/http/http_server_netty/src/main/kotlin/com/hexagonkt/http/server/netty/NettyServerHandler.kt index fe989e5cdb..34d83e46c8 100644 --- a/http/http_server_netty/src/main/kotlin/com/hexagonkt/http/server/netty/NettyServerHandler.kt +++ b/http/http_server_netty/src/main/kotlin/com/hexagonkt/http/server/netty/NettyServerHandler.kt @@ -1,7 +1,7 @@ package com.hexagonkt.http.server.netty import com.hexagonkt.handlers.Context -import com.hexagonkt.http.bodyToBytes +import com.hexagonkt.http.handlers.bodyToBytes import com.hexagonkt.http.model.* import com.hexagonkt.http.model.Cookie import com.hexagonkt.http.handlers.HttpHandler 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 12842e00d3..91df8bbe9f 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 @@ -3,7 +3,7 @@ package com.hexagonkt.http.server.servlet import com.hexagonkt.core.logging.Logger import com.hexagonkt.core.media.TEXT_PLAIN import com.hexagonkt.core.toText -import com.hexagonkt.http.bodyToBytes +import com.hexagonkt.http.handlers.bodyToBytes import com.hexagonkt.http.handlers.HttpHandler import com.hexagonkt.http.model.HttpResponse import com.hexagonkt.http.model.HttpResponsePort 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 f5c78d9995..7bfe82171b 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 @@ -1,9 +1,9 @@ package com.hexagonkt.http.server.servlet -import com.hexagonkt.core.text.Ansi.BLUE -import com.hexagonkt.core.text.Ansi.BOLD -import com.hexagonkt.core.text.Ansi.CYAN -import com.hexagonkt.core.text.Ansi.MAGENTA +import com.hexagonkt.core.text.AnsiColor.BLUE +import com.hexagonkt.core.text.AnsiEffect.BOLD +import com.hexagonkt.core.text.AnsiColor.CYAN +import com.hexagonkt.core.text.AnsiColor.MAGENTA import com.hexagonkt.core.text.Ansi.RESET import com.hexagonkt.core.Jvm import com.hexagonkt.core.text.prependIndent diff --git a/http/rest_tools/src/main/kotlin/com/hexagonkt/rest/tools/openapi/VerifySpecCallback.kt b/http/rest_tools/src/main/kotlin/com/hexagonkt/rest/tools/openapi/VerifySpecCallback.kt new file mode 100644 index 0000000000..8b2c8f7401 --- /dev/null +++ b/http/rest_tools/src/main/kotlin/com/hexagonkt/rest/tools/openapi/VerifySpecCallback.kt @@ -0,0 +1,110 @@ +package com.hexagonkt.rest.tools.openapi + +import com.atlassian.oai.validator.OpenApiInteractionValidator +import com.atlassian.oai.validator.OpenApiInteractionValidator.createForInlineApiSpecification +import com.atlassian.oai.validator.model.Request +import com.atlassian.oai.validator.model.Response +import com.atlassian.oai.validator.model.Request.Method +import com.atlassian.oai.validator.model.SimpleRequest +import com.atlassian.oai.validator.model.SimpleResponse +import com.atlassian.oai.validator.report.ValidationReport +import com.hexagonkt.http.handlers.HttpContext +import com.hexagonkt.http.model.ContentType +import com.hexagonkt.http.model.HttpMethod +import com.hexagonkt.http.model.HttpMethod.* +import java.net.URL +import kotlin.jvm.optionals.getOrNull + +/** + * Callback that verifies server calls comply with a given OpenAPI spec. + */ +class VerifySpecCallback(spec: URL) : (HttpContext) -> HttpContext { + + private val messagePrefix: String = "\n- " + private val validator: OpenApiInteractionValidator = + createForInlineApiSpecification(spec.readText()).build() + + override fun invoke(context: HttpContext): HttpContext { + val requestReport = validator.validateRequest(request(context)) + + val result = context.next() + + val resultMethod = method(result.method) + val responseReport = validator.validateResponse(result.path, resultMethod, response(result)) + + responseReport.merge(requestReport) + + return if (responseReport.hasErrors()) result.badRequest(message(responseReport)) + else result + } + + private fun message(report: ValidationReport): String { + return report.messages.joinToString(messagePrefix, "Invalid request:$messagePrefix") { + val level = it.level + val key = it.key + val context = it.context + .map { c -> + val op = c.apiOperation + .getOrNull() + ?.let { ao -> + val method = ao.method + val apiPath = ao.apiPath + "$method ${apiPath.normalised()}" + } + ?: "" + + val loc = c.location.getOrNull()?.name ?: "" + + "$op $loc" + } + .orElse("") + val message = it.message + val additionalInfo = it.additionalInfo + val nestedMessages = it.nestedMessages + + "$level: $key [$context] $message $additionalInfo $nestedMessages" + } + } + + private fun request(context: HttpContext): Request { + val request = context.request + val builder = SimpleRequest.Builder(method(context.method), context.path, true) + + if (request.bodyString().isNotEmpty()) + builder.withBody(request.bodyString()) + + request.contentType?.text?.let(builder::withContentType) + request.headers.httpFields.values.forEach { builder.withHeader(it.name, it.strings()) } + request.accept.map(ContentType::text).forEach(builder::withAccept) + request.authorization?.text?.let(builder::withAuthorization) + request.queryParameters.httpFields.values.forEach { + builder.withQueryParam(it.name, it.strings()) + } + + return builder.build() + } + + private fun response(context: HttpContext): Response { + val response = context.response + val builder = SimpleResponse.Builder(context.status.code) + + builder.withBody(response.bodyString()) + + response.contentType?.text?.let(builder::withContentType) + response.headers.httpFields.values.forEach { builder.withHeader(it.name, it.strings()) } + + return builder.build() + } + + private fun method(method: HttpMethod): Method = + when (method) { + GET -> Method.GET + HEAD -> Method.HEAD + POST -> Method.POST + PUT -> Method.PUT + DELETE -> Method.DELETE + TRACE -> Method.TRACE + OPTIONS -> Method.OPTIONS + PATCH -> Method.PATCH + } +} diff --git a/http/rest_tools/src/main/resources/META-INF/native-image/com.hexagonkt/rest_tools/reflect-config.json b/http/rest_tools/src/main/resources/META-INF/native-image/com.hexagonkt/rest_tools/reflect-config.json new file mode 100644 index 0000000000..5e5b8b12a9 --- /dev/null +++ b/http/rest_tools/src/main/resources/META-INF/native-image/com.hexagonkt/rest_tools/reflect-config.json @@ -0,0 +1,234 @@ +[ + { + "name": "com.atlassian.oai.validator.schema.SwaggerV20Library$SyntaxBundle", + "allDeclaredConstructors": true + }, + { + "name": "com.atlassian.oai.validator.schema.SwaggerV20Library$ValidationBundle", + "allDeclaredConstructors": true + }, + { + "name": "com.atlassian.oai.validator.schema.keyword.DiscriminatorKeywordValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.atlassian.oai.validator.schema.keyword.Nullable$NullableKeywordValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jackson.jsonpointer.JsonPointerMessages", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.core.messages.JsonSchemaCoreMessageBundle", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.core.messages.JsonSchemaSyntaxMessageBundle", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.AdditionalItemsValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.AdditionalPropertiesValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.DependenciesValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.EnumValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.MaxItemsValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.MaxLengthValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.MaximumValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.MinItemsValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.MinLengthValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.MinimumValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.PatternValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.common.UniqueItemsValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv3.DisallowKeywordValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv3.DivisibleByValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv3.DraftV3TypeValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv3.ExtendsValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv3.PropertiesValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.AllOfValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.AnyOfValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.DraftV4TypeValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.MaxPropertiesValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.MinPropertiesValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.MultipleOfValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.NotValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.OneOfValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.keyword.validator.draftv4.RequiredKeywordValidator", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.messages.JsonSchemaConfigurationBundle", + "allDeclaredConstructors": true + }, + { + "name": "com.github.fge.jsonschema.messages.JsonSchemaValidationBundle", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.AESCipher$General", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.ARCFOURCipher", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.DESCipher", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.DESedeCipher", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.DHParameters", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "allDeclaredConstructors": true + }, + { + "name": "com.sun.crypto.provider.TlsMasterSecretGenerator", + "allDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.core.jackson.mixin.ExtensionsMixin", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.core.jackson.mixin.SchemaMixin", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.ArraySchema", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.BooleanSchema", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.DateTimeSchema", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.IntegerSchema", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.ObjectSchema", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.Schema", + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.swagger.v3.oas.models.media.StringSchema", + "queryAllDeclaredMethods": true, + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true + }, + { + "name": "io.swagger.v3.oas.models.media.XML", + "allDeclaredFields": true, + "queryAllDeclaredConstructors": true, + "allDeclaredMethods": true + } +] diff --git a/http/rest_tools/src/test/kotlin/com/hexagonkt/rest/tools/openapi/VerifySpecCallbackTest.kt b/http/rest_tools/src/test/kotlin/com/hexagonkt/rest/tools/openapi/VerifySpecCallbackTest.kt new file mode 100644 index 0000000000..7e41a874e5 --- /dev/null +++ b/http/rest_tools/src/test/kotlin/com/hexagonkt/rest/tools/openapi/VerifySpecCallbackTest.kt @@ -0,0 +1,232 @@ +package com.hexagonkt.rest.tools.openapi + +import com.hexagonkt.core.logging.info +import com.hexagonkt.core.media.APPLICATION_JSON +import com.hexagonkt.core.urlOf +import com.hexagonkt.http.handlers.HttpContext +import com.hexagonkt.http.model.* +import com.hexagonkt.http.model.HttpMethod.* +import com.hexagonkt.serialization.SerializationManager +import com.hexagonkt.serialization.jackson.json.Json +import com.hexagonkt.serialization.jackson.yaml.Yaml +import com.hexagonkt.serialization.serialize +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +internal class VerifySpecCallbackTest { + + private val verifySpecCallback = VerifySpecCallback(urlOf("classpath:petstore_openapi.json")) + + init { + SerializationManager.formats = setOf(Json, Yaml) + } + + // TODO Check commented code (it should throw validation errors) + @Test fun `Requests not complying with spec return an error`() { + verify(errors = listOf("ERROR: validation.request.path.missing [ ] No API path found that matches request ''. [] []")) +// verify( +// HttpRequest( +// path = "/pet/findByStatus", +// queryParameters = QueryParameters(QueryParameter("status", "invalid")) +// ), +// HttpResponse( +// status = OK_200, +// contentType = ContentType(APPLICATION_JSON), +// body = listOf( +// mapOf( +// "name" to "Keka", +// "photoUrls" to listOf("http://example.com") +// ) +// ), +// ), +// listOf("1") +// ) + verify( + HttpRequest( + method = HEAD, + path = "/pet/1", + accept = listOf(ContentType(APPLICATION_JSON)), + ), + HttpResponse( + status = OK_200, + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ), + ), + listOf("ERROR: validation.request.operation.notAllowed [ ] HEAD operation not allowed on path '/pet/1'. [] []") + ) + verify( + HttpRequest( + method = POST, + path = "/pet", + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ) + ), + HttpResponse( + status = OK_200, + contentType = ContentType(APPLICATION_JSON), + body = listOf( + mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ) + ), + ), + listOf("ERROR: validation.response.body.schema.type [POST /pet RESPONSE] Instance type (array) does not match any allowed primitive type (allowed: [\"object\"]) [] []") + ) +// verify( +// HttpRequest( +// method = POST, +// path = "/pet", +// ), +// HttpResponse( +// status = OK_200, +// contentType = ContentType(APPLICATION_JSON), +// body = mapOf( +// "name" to "Keka", +// "photoUrls" to listOf("http://example.com") +// ), +// ), +// listOf("1") +// ) + verify( + HttpRequest(method = DELETE, path = "/pet/1"), + HttpResponse(status = OK_200), + listOf("ERROR: validation.response.status.unknown [DELETE /pet/{petId} RESPONSE] Response status 200 not defined for path '/pet/{petId}'. [] []") + ) + } + + @Test fun `Requests complying with spec return the proper result`() { + verify( + HttpRequest( + path = "/pet/1", + accept = listOf(ContentType(APPLICATION_JSON)), + ), + HttpResponse( + status = OK_200, + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ), + ), + ) + verify( + HttpRequest(method = HEAD, path = "/pet/findByTags"), + HttpResponse(status = OK_200), + ) + verify( + HttpRequest(method = TRACE, path = "/pet/findByTags"), + HttpResponse(status = OK_200), + ) + verify( + HttpRequest(method = OPTIONS, path = "/pet/findByTags"), + HttpResponse(status = OK_200), + ) + verify( + HttpRequest(method = PATCH, path = "/pet/findByTags"), + HttpResponse(status = OK_200), + ) + verify( + HttpRequest( + path = "/pet/findByStatus", + queryParameters = QueryParameters(QueryParameter("status", "sold")) + ), + HttpResponse( + status = OK_200, + contentType = ContentType(APPLICATION_JSON), + body = listOf( + mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ) + ), + ), + ) + verify( + HttpRequest( + method = POST, + path = "/pet", + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ) + ), + HttpResponse( + status = OK_200, + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ), + ), + ) + verify( + HttpRequest( + method = PUT, + path = "/pet", + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ) + ), + HttpResponse( + status = OK_200, + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ), + ), + ) + verify( + HttpRequest( + method = PUT, + path = "/pet", + contentType = ContentType(APPLICATION_JSON), + body = mapOf( + "name" to "Keka", + "photoUrls" to listOf("http://example.com") + ) + ), + HttpResponse(status = NOT_FOUND_404), + ) + verify( + HttpRequest(method = DELETE, path = "/pet/1"), + HttpResponse(status = BAD_REQUEST_400), + ) + } + + private fun verify( + request: HttpRequestPort = HttpRequest(), + response: HttpResponsePort = HttpResponse(status = OK_200), + errors: List = emptyList(), + ) { + val serializedResponse = response.contentType + ?.let { response.with(body = response.body.serialize(it.mediaType)) } + ?: response + + val serializedRequest = request.contentType + ?.let { request.with(body = request.body.serialize(it.mediaType)) } + ?: request + + val result = verifySpecCallback(HttpContext(serializedRequest, serializedResponse)) + + val bodyString = result.response.bodyString() + val actualErrors = + if (errors.isEmpty()) emptyList() + else bodyString.info().lines().drop(1).map { it.removePrefix("- ") } + + val expectedStatus = if (errors.isEmpty()) response.status else BAD_REQUEST_400 + + assertEquals(expectedStatus, result.status) + assertEquals(errors, actualErrors) + } +} diff --git a/http/rest_tools/src/test/resources/petstore_openapi.json b/http/rest_tools/src/test/resources/petstore_openapi.json index 8654f6301e..d396fb6de5 100644 --- a/http/rest_tools/src/test/resources/petstore_openapi.json +++ b/http/rest_tools/src/test/resources/petstore_openapi.json @@ -230,6 +230,50 @@ } }, "/pet/findByTags": { + "patch": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "responses": { + "200": { + "description": "successful operation" + } + } + }, + "options": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "responses": { + "200": { + "description": "successful operation" + } + } + }, + "trace": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "responses": { + "200": { + "description": "successful operation" + } + } + }, + "head": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "responses": { + "200": { + "description": "successful operation" + } + } + }, "get": { "tags": [ "pet" @@ -577,7 +621,7 @@ "store" ], "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions", "operationId": "getOrderById", "parameters": [ { @@ -620,7 +664,7 @@ "store" ], "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors", "operationId": "deleteOrder", "parameters": [ { @@ -650,7 +694,7 @@ "user" ], "summary": "Create user", - "description": "This can only be done by the logged in user.", + "description": "This can only be done by the logged-in user.", "operationId": "createUser", "requestBody": { "description": "Created user object", @@ -745,7 +789,7 @@ { "name": "username", "in": "query", - "description": "The user name for login", + "description": "The username for login", "required": false, "schema": { "type": "string" @@ -773,7 +817,7 @@ } }, "X-Expires-After": { - "description": "date in UTC when toekn expires", + "description": "date in UTC when token expires", "schema": { "type": "string", "format": "date-time" @@ -804,7 +848,7 @@ "tags": [ "user" ], - "summary": "Logs out current logged in user session", + "summary": "Logs out current logged-in user session", "description": "", "operationId": "logoutUser", "parameters": [ ], @@ -820,7 +864,7 @@ "tags": [ "user" ], - "summary": "Get user by user name", + "summary": "Get user by username", "description": "", "operationId": "getUserByName", "parameters": [ @@ -863,7 +907,7 @@ "user" ], "summary": "Update user", - "description": "This can only be done by the logged in user.", + "description": "This can only be done by the logged-in user.", "operationId": "updateUser", "parameters": [ { @@ -907,7 +951,7 @@ "user" ], "summary": "Delete user", - "description": "This can only be done by the logged in user.", + "description": "This can only be done by the logged-in user.", "operationId": "deleteUser", "parameters": [ { @@ -983,7 +1027,7 @@ }, "username": { "type": "string", - "example": "fehguy" + "example": "guy" }, "address": { "type": "array", diff --git a/logging/logging_jul/src/main/kotlin/com/hexagonkt/logging/jul/PatternFormat.kt b/logging/logging_jul/src/main/kotlin/com/hexagonkt/logging/jul/PatternFormat.kt index 6d1f12ea72..7f156a79df 100644 --- a/logging/logging_jul/src/main/kotlin/com/hexagonkt/logging/jul/PatternFormat.kt +++ b/logging/logging_jul/src/main/kotlin/com/hexagonkt/logging/jul/PatternFormat.kt @@ -1,11 +1,13 @@ package com.hexagonkt.logging.jul -import com.hexagonkt.core.text.Ansi -import com.hexagonkt.core.text.Ansi.BLUE -import com.hexagonkt.core.text.Ansi.BRIGHT_BLACK -import com.hexagonkt.core.text.Ansi.CYAN -import com.hexagonkt.core.text.Ansi.MAGENTA -import com.hexagonkt.core.text.Ansi.RED +import com.hexagonkt.core.text.AnsiColor.DEFAULT +import com.hexagonkt.core.text.AnsiColor.YELLOW +import com.hexagonkt.core.text.AnsiColor.BLUE +import com.hexagonkt.core.text.AnsiColor.BRIGHT_BLACK +import com.hexagonkt.core.text.AnsiColor.CYAN +import com.hexagonkt.core.text.AnsiColor.MAGENTA +import com.hexagonkt.core.text.AnsiColor.RED +import com.hexagonkt.core.text.AnsiEffect.BOLD import com.hexagonkt.core.text.Ansi.RESET import com.hexagonkt.core.text.eol import com.hexagonkt.core.fail @@ -41,11 +43,11 @@ class PatternFormat( private val pattern: String = if (useColor) COLOR_PATTERN else PATTERN private val levelColors: Map = mapOf( - Level.FINER to Ansi.DEFAULT, - Level.FINE to Ansi.DEFAULT, + Level.FINER to DEFAULT, + Level.FINE to DEFAULT, Level.INFO to BLUE, - Level.WARNING to Ansi.YELLOW, - Level.SEVERE to RED + Ansi.BOLD + Level.WARNING to YELLOW, + Level.SEVERE to RED + BOLD ) private val levelNames: Map = mapOf( diff --git a/logging/logging_jul/src/test/kotlin/com/hexagonkt/logging/jul/PatternFormatTest.kt b/logging/logging_jul/src/test/kotlin/com/hexagonkt/logging/jul/PatternFormatTest.kt index 4f9aabfa92..b3b4d38900 100644 --- a/logging/logging_jul/src/test/kotlin/com/hexagonkt/logging/jul/PatternFormatTest.kt +++ b/logging/logging_jul/src/test/kotlin/com/hexagonkt/logging/jul/PatternFormatTest.kt @@ -1,7 +1,7 @@ package com.hexagonkt.logging.jul -import com.hexagonkt.core.text.Ansi import com.hexagonkt.core.fail +import com.hexagonkt.core.text.AnsiColor import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.lang.RuntimeException @@ -17,12 +17,12 @@ internal class PatternFormatTest { val colorFormat = PatternFormat(true) val colorMessage = colorFormat.format(LogRecord(INFO, message)) Assertions.assertTrue(colorMessage.contains(message)) - Assertions.assertTrue(colorMessage.contains(Ansi.BLUE)) + Assertions.assertTrue(colorMessage.contains(AnsiColor.BLUE)) val plainFormat = PatternFormat(false) val plainMessage = plainFormat.format(LogRecord(INFO, message)) Assertions.assertTrue(plainMessage.contains(message)) - Assertions.assertFalse(plainMessage.contains(Ansi.BLUE)) + Assertions.assertFalse(plainMessage.contains(AnsiColor.BLUE)) } @Test fun `Formatting error messages render stack traces`() { @@ -32,14 +32,14 @@ internal class PatternFormatTest { val colorMessage = PatternFormat(true).format(record) Assertions.assertTrue(colorMessage.contains(message)) - Assertions.assertTrue(colorMessage.contains(Ansi.RED)) + Assertions.assertTrue(colorMessage.contains(AnsiColor.RED)) Assertions.assertTrue(colorMessage.contains("Tested failure")) Assertions.assertTrue(colorMessage.contains(RuntimeException::class.qualifiedName ?: fail)) Assertions.assertTrue(colorMessage.contains(this::class.qualifiedName ?: fail)) val plainMessage = PatternFormat(false).format(record) Assertions.assertTrue(plainMessage.contains(message)) - Assertions.assertFalse(plainMessage.contains(Ansi.RED)) + Assertions.assertFalse(plainMessage.contains(AnsiColor.RED)) Assertions.assertTrue(plainMessage.contains("Tested failure")) Assertions.assertTrue(plainMessage.contains(RuntimeException::class.qualifiedName ?: fail)) Assertions.assertTrue(plainMessage.contains(this::class.qualifiedName ?: fail)) @@ -52,12 +52,12 @@ internal class PatternFormatTest { val colorMessage = colorFormat.format(LogRecord(INFO, message)) Assertions.assertTrue(colorMessage.contains(message)) Assertions.assertFalse(colorMessage.contains("INFO")) - Assertions.assertFalse(colorMessage.contains(Ansi.BLUE)) + Assertions.assertFalse(colorMessage.contains(AnsiColor.BLUE)) val plainFormat = PatternFormat(useColor = false, messageOnly = true) val plainMessage = plainFormat.format(LogRecord(INFO, message)) Assertions.assertTrue(plainMessage.contains(message)) Assertions.assertFalse(colorMessage.contains("INFO")) - Assertions.assertFalse(plainMessage.contains(Ansi.BLUE)) + Assertions.assertFalse(plainMessage.contains(AnsiColor.BLUE)) } }