diff --git a/.editorconfig b/.editorconfig index 5cbb26d5..b0b0b09e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,8 @@ [*.{kt,kts}] indent_size=2 insert_final_newline=true -max_line_length=100 -disabled_rules=no-consecutive-blank-lines,no-blank-line-before-rbrace +max_line_length=120 +kotlin_code_style=ktlint_official +ktlint_standard_no-consecutive-blank-lines=disabled +ktlint_standard_no-empty-first-line-in-class-body=disabled +ktlint_standard_no-blank-line-before-rbrace=disabled diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65855181..b1d564c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,23 +13,23 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - java-version: '11' - distribution: adopt + java-version: '21' + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Build & Test - uses: burrunan/gradle-cache-action@v1 + run: ./gradlew check sonar env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - job-id: jdk11-build-test - arguments: check sonar - name: Report Tests uses: dorny/test-reporter@v1 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 7f44e9ef..932e95a0 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -17,34 +17,34 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - java-version: '11' - distribution: adopt + java-version: '21' + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Build Artifacts & Documentation - uses: burrunan/gradle-cache-action@v1 - with: - job-id: jdk11-build-test - arguments: build dokkaHtmlMultiModule -x test - properties: | - releaseVersion=${{ github.ref_name }} + run: >- + ./gradlew + -PreleaseVersion=${{ github.ref_name }} + build dokkaHtmlMultiModule + -x test - name: Publish Maven Release - uses: burrunan/gradle-cache-action@v1 + run: >- + ./gradlew + -PreleaseVersion=${{ github.ref_name }} + -PsonatypeUsername=${{ secrets.OSSRH_USER }} -PsonatypePassword=${{ secrets.OSSRH_PASS }} + publishToSonatype closeAndReleaseSonatypeStagingRepository + -x test env: ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} - with: - job-id: jdk11-build-test - arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository -x test - properties: | - releaseVersion=${{ github.ref_name }} - sonatypeUsername=${{ secrets.OSSRH_USER }} - sonatypePassword=${{ secrets.OSSRH_PASS }} - name: Publish Documentation uses: JamesIves/github-pages-deploy-action@v4 @@ -54,14 +54,9 @@ jobs: folder: build/dokka - name: Publish GitHub Release - uses: burrunan/gradle-cache-action@v1 - env: - ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.OSSRH_GPG_SECRET_KEY_ID }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} - with: - job-id: jdk11-build-test - arguments: githubRelease -x test - properties: | - releaseVersion=${{ github.ref_name }} - github.token=${{ secrets.GITHUB_TOKEN }} + run: >- + ./gradlew + -PreleaseVersion=${{ github.ref_name }} + -Pgithub.token=${{ secrets.GITHUB_TOKEN }} + githubRelease + -x test diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 88e5d707..7301e355 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -17,27 +17,25 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - java-version: '11' - distribution: adopt + java-version: '21' + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 - name: Build Artifacts & Documentation - uses: burrunan/gradle-cache-action@v1 - with: - job-id: jdk11-build-test - arguments: build dokkaHtmlMultiModule -x test + run: ./gradlew build dokkaHtmlMultiModule -x test - name: Publish Maven Artifacts (Snapshot) - uses: burrunan/gradle-cache-action@v1 - with: - job-id: jdk11-build-test - arguments: publishToSonatype - properties: | - sonatypeUsername=${{ secrets.OSSRH_USER }} - sonatypePassword=${{ secrets.OSSRH_PASS }} + run: >- + ./gradlew + -PsonatypeUsername=${{ secrets.OSSRH_USER }} + -PsonatypePassword=${{ secrets.OSSRH_PASS }} + publishToSonatype - name: Publish Documentation uses: JamesIves/github-pages-deploy-action@v4 diff --git a/build.gradle.kts b/build.gradle.kts index d3de5b37..c2a65539 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,8 @@ import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.cadixdev.gradle.licenser.LicenseExtension import org.jetbrains.dokka.gradle.DokkaTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension plugins { @@ -10,7 +11,7 @@ plugins { id("com.github.breadmoirai.github-release") id("org.sonarqube") id("io.github.gradle-nexus.publish-plugin") - + id("org.jetbrains.kotlinx.kover") kotlin("jvm") apply (false) id("org.cadixdev.licenser") apply (false) @@ -46,11 +47,11 @@ allprojects { configure(moduleNames.map { project(":sunday-$it") }) { apply(plugin = "java-library") - apply(plugin = "jacoco") apply(plugin = "maven-publish") apply(plugin = "signing") apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlinx.kover") apply(plugin = "org.jetbrains.dokka") apply(plugin = "org.cadixdev.licenser") apply(plugin = "org.jmailen.kotlinter") @@ -88,13 +89,14 @@ configure(moduleNames.map { project(":sunday-$it") }) { withJavadocJar() } - tasks.withType().configureEach { - kotlinOptions { - kotlinOptions { - languageVersion = kotlinVersion - apiVersion = kotlinVersion - } - jvmTarget = javaVersion + configure { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(javaVersion)) + } + compilerOptions { + jvmTarget.set(JvmTarget.valueOf("JVM_$javaVersion")) + javaParameters.set(true) + freeCompilerArgs.add("-Xjvm-default=all") } } @@ -103,10 +105,6 @@ configure(moduleNames.map { project(":sunday-$it") }) { // TEST // - configure { - toolVersion = "0.8.7" - } - tasks.named("test").configure { useJUnitPlatform() @@ -118,8 +116,6 @@ configure(moduleNames.map { project(":sunday-$it") }) { } reports.junitXml.required.set(true) - - finalizedBy("jacocoTestReport") } @@ -133,9 +129,9 @@ configure(moduleNames.map { project(":sunday-$it") }) { } configure { - source = files("src/main/kotlin") + source.from("src/main/kotlin") - config = files("${rootProject.layout.projectDirectory}/src/main/detekt/detekt.yml") + config.from("${rootProject.layout.projectDirectory}/src/main/detekt/detekt.yml") buildUponDefaultConfig = true baseline = file("src/main/detekt/detekt-baseline.xml") } @@ -152,7 +148,7 @@ configure(moduleNames.map { project(":sunday-$it") }) { tasks.named("dokkaHtml") { failOnWarning.set(true) suppressObviousFunctions.set(false) - outputDirectory.set(file("$buildDir/dokka/${project.version}")) + outputDirectory.set(file("${layout.buildDirectory.get()}/dokka/${project.version}")) } tasks.named("dokkaJavadoc") { @@ -271,7 +267,7 @@ configure(moduleNames.map { project(":sunday-$it") }) { // ANALYSIS // - sonarqube { + sonar { properties { property("sonar.sources", "src/main") property("sonar.tests", "src/test") @@ -282,7 +278,7 @@ configure(moduleNames.map { project(":sunday-$it") }) { property("sonar.jacoco.reportPaths", "") property( "sonar.coverage.jacoco.xmlReportPaths", - "$rootDir/code-coverage/build/reports/jacoco/testCoverageReport/testCoverageReport.xml", + "$rootDir/code-coverage/build/reports/kover/report.xml", ) } } @@ -294,7 +290,7 @@ configure(moduleNames.map { project(":sunday-$it") }) { // ANALYSIS // -sonarqube { +sonar { properties { property("sonar.projectName", "sunday-kt") property("sonar.projectKey", "outfoxx_sunday-kt") @@ -309,7 +305,7 @@ sonarqube { // tasks.dokkaHtmlMultiModule.configure { - outputDirectory.set(buildDir.resolve("dokka/${releaseVersion}")) + outputDirectory.set(layout.buildDirectory.dir("dokka/${releaseVersion}")) } @@ -318,25 +314,23 @@ tasks.dokkaHtmlMultiModule.configure { // githubRelease { - owner("outfoxx") - repo("sunday-kt") - tagName(releaseVersion) - targetCommitish("main") - releaseName("🚀 v${releaseVersion}") - generateReleaseNotes(true) - draft(false) - prerelease(!releaseVersion.matches("""^\d+\.\d+\.\d+$""".toRegex())) - releaseAssets( + owner = "outfoxx" + repo = "sunday-kt" + tagName = releaseVersion + targetCommitish = "main" + releaseName = "🚀 v${releaseVersion}" + generateReleaseNotes = true + draft = false + prerelease = !releaseVersion.matches("""^\d+\.\d+\.\d+$""".toRegex()) + releaseAssets.from( moduleNames.flatMap { moduleName -> listOf("", "-javadoc", "-sources").map { suffix -> file("$rootDir/$moduleName/build/libs/sunday-$moduleName-$releaseVersion$suffix.jar") } } ) - overwrite(true) - authorization( - "Token " + (project.findProperty("github.token") as String? ?: System.getenv("GITHUB_TOKEN")) - ) + overwrite = true + authorization = "Token " + (project.findProperty("github.token") as String? ?: System.getenv("GITHUB_TOKEN")) } nexusPublishing { diff --git a/code-coverage/build.gradle.kts b/code-coverage/build.gradle.kts index caf38f3a..26567c61 100644 --- a/code-coverage/build.gradle.kts +++ b/code-coverage/build.gradle.kts @@ -1,7 +1,7 @@ plugins { base - id("jacoco-report-aggregation") + id("org.jetbrains.kotlinx.kover") } repositories { @@ -9,26 +9,15 @@ repositories { } dependencies { - jacocoAggregation(project(":sunday-core")) - jacocoAggregation(project(":sunday-jdk")) - jacocoAggregation(project(":sunday-okhttp")) -} - -reporting { - reports { - create("testCoverageReport") { - testType.set(TestSuiteType.UNIT_TEST) - reportTask { - reports.xml.required.set(true) - } - } - } + kover(project(":sunday-core")) + kover(project(":sunday-jdk")) + kover(project(":sunday-okhttp")) } tasks { check { - finalizedBy(named("testCoverageReport")) + finalizedBy(named("koverXmlReport"), named("koverHtmlReport")) } } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/EventParser.kt b/core/src/main/kotlin/io/outfoxx/sunday/EventParser.kt index aa2634ef..7d1b724b 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/EventParser.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/EventParser.kt @@ -42,7 +42,7 @@ class EventParser { /** * SSE data field. */ - var data: String? = null + var data: String? = null, ) { fun toEvent(origin: String) = EventSource.Event(event, id, data, origin) @@ -61,8 +61,10 @@ class EventParser { * @param data Latest data to process * @param dispatcher Handler for parsed events. */ - fun process(data: Buffer, dispatcher: (EventInfo) -> Unit) { - + fun process( + data: Buffer, + dispatcher: (EventInfo) -> Unit, + ) { unprocessedData.write(data, data.size) val eventStrings = extractEventStrings() @@ -75,11 +77,9 @@ class EventParser { @Suppress("LoopWithTooManyJumpStatements") private fun extractEventStrings(): List { - val eventStrings = mutableListOf() while (!unprocessedData.exhausted()) { - val eventSeparatorRange = findEventSeparator(unprocessedData) ?: break @@ -112,9 +112,7 @@ class EventParser { private fun findEventSeparator(data: Buffer): Pair? { for (idx in 0 until data.size) { - when (data[idx]) { - // line-feed LF -> { // if next char is same, @@ -152,16 +150,18 @@ class EventParser { private fun parseAndDispatchEvents( eventStrings: List, - dispatcher: (EventInfo) -> Unit + dispatcher: (EventInfo) -> Unit, ) { - for (eventString in eventStrings) { parseAndDispatchEvent(eventString, dispatcher) } } - private fun parseAndDispatchEvent(eventString: String, dispatcher: (EventInfo) -> Unit) { + private fun parseAndDispatchEvent( + eventString: String, + dispatcher: (EventInfo) -> Unit, + ) { if (eventString.isEmpty()) { return } @@ -175,11 +175,9 @@ class EventParser { @Suppress("LoopWithTooManyJumpStatements") private fun parseEvent(string: String): EventInfo { - val info = EventInfo() for (line in string.split(linSeparators)) { - val keyValueSeparatorIdx = line.indexOf(':') val (key, value) = if (keyValueSeparatorIdx != -1) { @@ -190,7 +188,6 @@ class EventParser { } when (key) { - "retry" -> info.retry = trimEventField(string = value) "event" -> info.event = trimEventField(string = value) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/EventSource.kt b/core/src/main/kotlin/io/outfoxx/sunday/EventSource.kt index 0a787e95..380ec320 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/EventSource.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/EventSource.kt @@ -67,7 +67,7 @@ class EventSource( retryTime: Duration = retryTimeDefault, private val eventTimeout: Duration? = eventTimeoutDefault, private val eventTimeoutCheckInterval: Duration = eventTimeoutCheckIntervalDefault, - private val logger: Logger = LoggerFactory.getLogger(EventSource::class.java) + private val logger: Logger = LoggerFactory.getLogger(EventSource::class.java), ) : Closeable { /** @@ -89,7 +89,7 @@ class EventSource( /** * SSE origin. */ - val origin: String + val origin: String, ) companion object { @@ -137,7 +137,7 @@ class EventSource( private fun createRequestEventScope(): CoroutineScope = CoroutineScope(CoroutineName("EventSource - Request Processor")) - private const val MaxRetryTimeMultiple = 30.0 + private const val MAX_RETRY_TIME_MULTIPLE = 30.0 } /** @@ -158,7 +158,7 @@ class EventSource( /** * [EventSource] is closed. */ - Closed + Closed, } private val stateLock = ReentrantReadWriteLock() @@ -197,7 +197,7 @@ class EventSource( private val eventParser = EventParser() - /** + /* * Event Listening */ @@ -239,19 +239,25 @@ class EventSource( /** * Add an event listener for a specific event type. */ - fun addEventListener(event: String, handler: (Event) -> Unit) { + fun addEventListener( + event: String, + handler: (Event) -> Unit, + ) { stateLock.write { eventListenersInternal[event] = handler } } /** * Removed a previously added event listener. */ - fun removeEventListener(event: String, handler: (Event) -> Unit) { + fun removeEventListener( + event: String, + handler: (Event) -> Unit, + ) { stateLock.write { eventListenersInternal.remove(event, handler) } } - /** + /* * Connection Management */ @@ -264,7 +270,6 @@ class EventSource( * [close] is called. */ fun connect() { - if (readyStateValue.isNotClosed) { return } @@ -277,7 +282,6 @@ class EventSource( } private suspend fun internalConnect() { - if (readyStateValue.isClosed) { logger.debug("Skipping connect due to close") return @@ -287,9 +291,10 @@ class EventSource( // Build default headers for passing to request builder - var headers = listOf( - Accept to EventStream.value, - ) + var headers = + listOf( + Accept to EventStream.value, + ) // Add last-event-id if we are reconnecting @@ -301,25 +306,25 @@ class EventSource( val request = requestSupplier(headers) - currentRequest = createRequestEventScope().launch { - try { - - request.start() - .onCompletion { - if (it != null) { - receivedError(it) - } else { - receivedComplete() - } - } - .collect(::dispatchEvent) - - } catch (ignored: CancellationException) { - // do nothing - } catch (error: Throwable) { - receivedError(error) + currentRequest = + createRequestEventScope().launch { + try { + request + .start() + .onCompletion { + if (it != null) { + receivedError(it) + } else { + receivedComplete() + } + }.collect(::dispatchEvent) + + } catch (ignored: CancellationException) { + // do nothing + } catch (error: Throwable) { + receivedError(error) + } } - } } private fun dispatchEvent(event: Request.Event) { @@ -348,7 +353,6 @@ class EventSource( * Close and disconnect the [EventSource]. */ override fun close() { - if (readyStateValue.isClosed) { return } @@ -392,7 +396,6 @@ class EventSource( } private fun stopEventTimeoutCheck() { - eventTimeoutTimerTask?.cancel() eventTimeoutTimerTask = null } @@ -425,7 +428,6 @@ class EventSource( private fun receivedResponse(response: Response) { - if (!readyStateValue.updateIfNotClosed(Open)) { logger.warn("Invalid state for receiving headers: {}", readyStateValue.current) @@ -448,7 +450,6 @@ class EventSource( } private fun receivedData(data: Buffer) { - if (readyStateValue.current != Open) { logger.warn("Invalid state for receiving headers: {}", readyStateValue.current) @@ -461,7 +462,6 @@ class EventSource( logger.debug("Received: data") try { - eventParser.process(data, ::dispatchParsedEvent) } catch (x: SocketException) { @@ -474,7 +474,6 @@ class EventSource( } private fun receivedError(t: Throwable?) { - if (readyStateValue.isClosed) { return } @@ -489,7 +488,6 @@ class EventSource( } private fun receivedComplete() { - if (readyStateValue.isClosed) { return } @@ -506,7 +504,6 @@ class EventSource( private fun scheduleReconnect() { - if (readyStateValue.isClosed) { return } @@ -547,9 +544,8 @@ class EventSource( private fun calculateRetryDelay( retryAttempt: Int, retryTime: Duration, - lastConnectTime: Duration + lastConnectTime: Duration, ): Duration { - val retryTimeMs = retryTime.toMillis() // calculate total delay @@ -557,13 +553,12 @@ class EventSource( var retryDelayMs = min( retryTimeMs + backOffDelayMs, - retryTimeMs * MaxRetryTimeMultiple + retryTimeMs * MAX_RETRY_TIME_MULTIPLE, ) // Adjust delay by amount of time last connect // cycle took, except on the first attempt if (retryAttempt > 0) { - retryDelayMs -= lastConnectTime.toMillis() // Ensure delay is at least as large as @@ -581,16 +576,13 @@ class EventSource( private fun dispatchParsedEvent(info: EventParser.EventInfo) { - lastEventReceivedTime = Instant.now() // Update retry time if it's a valid integer val retry = info.retry if (retry != null) { - val retryTime = retry.trim().toLongOrNull(radix = 10) if (retryTime != null) { - logger.debug("update retry timeout: retryTime=$retryTime") this.retryTimeValue = Duration.ofMillis(retryTime) @@ -608,10 +600,8 @@ class EventSource( // Save event id, if it does not contain null val eventId = info.id if (eventId != null) { - // Check for NULL as it is not allowed if (!eventId.contains(0.toChar())) { - lastEventId = eventId } else { logger.warn("event id contains null, unable to use for last-event-id") diff --git a/core/src/main/kotlin/io/outfoxx/sunday/EventSourceError.kt b/core/src/main/kotlin/io/outfoxx/sunday/EventSourceError.kt index 9371f179..bbc19524 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/EventSourceError.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/EventSourceError.kt @@ -20,9 +20,11 @@ class EventSourceError( val reason: Reason, ) : Exception(reason.message) { - enum class Reason(val message: String) { + enum class Reason( + val message: String, + ) { EventTimeout("Event Timeout Reached"), - InvalidState("Invalid State") + InvalidState("Invalid State"), } } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/MediaType.kt b/core/src/main/kotlin/io/outfoxx/sunday/MediaType.kt index 385db55d..03c905d5 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/MediaType.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/MediaType.kt @@ -27,7 +27,7 @@ class MediaType( val tree: Tree = Tree.Standard, subtype: String = "*", val suffix: Suffix? = null, - parameters: Map = emptyMap() + parameters: Map = emptyMap(), ) { constructor( @@ -35,20 +35,24 @@ class MediaType( tree: Tree = Tree.Standard, subtype: String = "*", suffix: Suffix? = null, - vararg parameters: Pair + vararg parameters: Pair, ) : this(type, tree, subtype, suffix, mapOf(*parameters)) /** * Standard parameter names. */ - enum class StandardParameterName(val code: String) { - CharSet("charset") + enum class StandardParameterName( + val code: String, + ) { + CharSet("charset"), } /** * Allowed types of media type. */ - enum class Type(val code: String) { + enum class Type( + val code: String, + ) { Application("application"), Audio("audio"), Example("example"), @@ -59,7 +63,8 @@ class MediaType( Multipart("multipart"), Text("text"), Video("video"), - Any("*"); + Any("*"), + ; companion object { @@ -76,13 +81,16 @@ class MediaType( /** * Allowed trees of media type. */ - enum class Tree(val code: String) { + enum class Tree( + val code: String, + ) { Standard(""), Vendor("vnd."), Personal("prs."), Unregistered("x."), Obsolete("x-"), - Any("*"); + Any("*"), + ; companion object { @@ -99,7 +107,9 @@ class MediaType( /** * Allowed suffixes of media type. */ - enum class Suffix(val code: String) { + enum class Suffix( + val code: String, + ) { XML("xml"), JSON("json"), BER("ber"), @@ -107,7 +117,8 @@ class MediaType( FastInfoSet("fastinfoset"), WBXML("wbxml"), Zip("zip"), - CBOR("cbor"); + CBOR("cbor"), + ; companion object { @@ -140,9 +151,7 @@ class MediaType( * @param name Standard parameter name to lookup. * @return Value of parameter if it exists or null. */ - fun parameter(name: StandardParameterName): String? { - return parameters[name.code] - } + fun parameter(name: StandardParameterName): String? = parameters[name.code] /** * Looks up a parameter. @@ -150,9 +159,7 @@ class MediaType( * @param name Parameter name to lookup. * @return Value of parameter if it exists or null. */ - fun parameter(name: String): String? { - return parameters[name] - } + fun parameter(name: String): String? = parameters[name] /** * Builds a new [MediaType] overriding one or more of the properties. @@ -167,15 +174,14 @@ class MediaType( type: Type? = null, tree: Tree? = null, subtype: String? = null, - parameters: Map? = null - ) = - MediaType( - type ?: this.type, - tree ?: this.tree, - subtype ?: this.subtype, - suffix, - parameters ?: this.parameters - ) + parameters: Map? = null, + ) = MediaType( + type ?: this.type, + tree ?: this.tree, + subtype ?: this.subtype, + suffix, + parameters ?: this.parameters, + ) /** * Builds a new [MediaType] overriding a parameter value. @@ -184,8 +190,10 @@ class MediaType( * @param value Overridden parameter value. * @return [MediaType] instance with the overridden parameter. */ - fun with(parameter: StandardParameterName, value: String) = - with(parameters = parameters + mapOf(parameter.code to value)) + fun with( + parameter: StandardParameterName, + value: String, + ) = with(parameters = parameters + mapOf(parameter.code to value)) /** * Builds a new [MediaType] overriding a parameter value. @@ -194,8 +202,10 @@ class MediaType( * @param value Overridden parameter value. * @return [MediaType] instance with the overridden parameter. */ - fun with(parameter: String, value: String) = - with(parameters = parameters + mapOf(parameter to value)) + fun with( + parameter: String, + value: String, + ) = with(parameters = parameters + mapOf(parameter to value)) /** * Encoded media type. @@ -205,7 +215,10 @@ class MediaType( val type = this.type.code val tree = this.tree.code val suffix = this.suffix?.let { "+${it.name.lowercase(ENGLISH)}" } ?: "" - val parameters = this.parameters.keys.sorted().joinToString("") { ";$it=${parameters[it]}" } + val parameters = + this.parameters.keys + .sorted() + .joinToString("") { ";$it=${parameters[it]}" } return "$type/$tree$subtype$suffix$parameters" } @@ -215,17 +228,17 @@ class MediaType( * @see other [MediaType] to check for compatibility. * @return `true` if [other] is compatible with this instance. */ - fun compatible(other: MediaType): Boolean { - return when { + fun compatible(other: MediaType): Boolean = + when { this.type != Type.Any && other.type != Type.Any && this.type != other.type -> false this.tree != Tree.Any && other.tree != Tree.Any && this.tree != other.tree -> false this.subtype != "*" && other.subtype != "*" && this.subtype != other.subtype -> false this.suffix != other.suffix -> false else -> - this.parameters.keys.intersect(other.parameters.keys) + this.parameters.keys + .intersect(other.parameters.keys) .all { this.parameters[it] == other.parameters[it] } } - } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -261,9 +274,8 @@ class MediaType( * @param acceptHeaders Accept headers to parse into media types. * @return List of [media types][MediaType] parsed from the given Accept headers. */ - fun from(acceptHeaders: List): List { - return acceptHeaders.flatMap { header -> header.split(",") }.map { from(it.trim()) } - } + fun from(acceptHeaders: List): List = + acceptHeaders.flatMap { header -> header.split(",") }.map { from(it.trim()) } /** * Parse a string into a [media type][MediaType]. @@ -271,8 +283,10 @@ class MediaType( * @param string String media type to parse. * @return [MediaType] parsed from the given string. */ - fun from(string: String, default: MediaType? = null): MediaType { - + fun from( + string: String, + default: MediaType? = null, + ): MediaType { fun default(): MediaType { if (default == null) { throw SundayError(SundayError.Reason.InvalidContentType, string) @@ -284,31 +298,44 @@ class MediaType( val match = fullRegex.matchEntire(string) ?: return default() val type = - match.groupValues.getOrNull(1)?.lowercase(ENGLISH)?.let { Type.fromCode(it) } + match.groupValues + .getOrNull(1) + ?.lowercase(ENGLISH) + ?.let { Type.fromCode(it) } ?: return default() val tree = - match.groupValues.getOrNull(2)?.lowercase(ENGLISH)?.let { Tree.fromCode(it) } + match.groupValues + .getOrNull(2) + ?.lowercase(ENGLISH) + ?.let { Tree.fromCode(it) } ?: Tree.Standard val subType = match.groupValues.getOrNull(3)?.lowercase(ENGLISH) ?: return default() - val suffix = match.groupValues.getOrNull(4)?.lowercase(ENGLISH)?.let { Suffix.fromCode(it) } + val suffix = + match.groupValues + .getOrNull(4) + ?.lowercase(ENGLISH) + ?.let { Suffix.fromCode(it) } val parameters = match.groupValues.getOrNull(5)?.let { params -> - paramRegex.findAll(params).mapNotNull { param -> - val key = param.groupValues.getOrNull(1) ?: return@mapNotNull null - val value = param.groupValues.getOrNull(2) ?: return@mapNotNull null - key.lowercase(ENGLISH) to value.lowercase(ENGLISH) - }.toMap() + paramRegex + .findAll(params) + .mapNotNull { param -> + val key = param.groupValues.getOrNull(1) ?: return@mapNotNull null + val value = param.groupValues.getOrNull(2) ?: return@mapNotNull null + key.lowercase(ENGLISH) to value.lowercase(ENGLISH) + }.toMap() } ?: emptyMap() return MediaType(type, tree, subType, suffix, parameters) } private val fullRegex = - """^([a-z]+|\*)/(x(?:-|\\.)|(?:vnd|prs|x)\.|\*)?([a-z0-9\-.]+|\*)(?:\+([a-z]+))?( *(?:; *[\w.-]+ *= *[\w.-]+ *)*)$""" // ktlint-disable max-line-length + @Suppress("ktlint:standard:max-line-length") + """^([a-z]+|\*)/(x(?:-|\\.)|(?:vnd|prs|x)\.|\*)?([a-z0-9\-.]+|\*)(?:\+([a-z]+))?( *(?:; *[\w.-]+ *= *[\w.-]+ *)*)$""" .toRegex(option = IGNORE_CASE) private val paramRegex = """ *; *([\w.-]+) *= *([\w.-]+)""".toRegex(option = IGNORE_CASE) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/PathEncoders.kt b/core/src/main/kotlin/io/outfoxx/sunday/PathEncoders.kt index 25e441ad..d0e69d83 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/PathEncoders.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/PathEncoders.kt @@ -25,9 +25,10 @@ typealias PathEncoderMap = Map, PathEncoder> object PathEncoders { val default: PathEncoderMap - get() = mapOf( - Enum::class to { encodeEnum(it as Enum<*>) } - ) + get() = + mapOf( + Enum::class to { encodeEnum(it as Enum<*>) }, + ) fun > encodeEnum(value: Enum) = value.javaClass @@ -40,13 +41,8 @@ object PathEncoders { inline fun PathEncoderMap.add( type: KClass, - crossinline encoder: (T) -> String -): PathEncoderMap { - return this + mapOf(type to { encoder.invoke(it as T) }) -} + crossinline encoder: (T) -> String, +): PathEncoderMap = this + mapOf(type to { encoder.invoke(it as T) }) -inline fun PathEncoderMap.add( - crossinline encoder: (T) -> String -): PathEncoderMap { - return add(T::class, encoder) -} +inline fun PathEncoderMap.add(crossinline encoder: (T) -> String): PathEncoderMap = + add(T::class, encoder) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt b/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt index a533e306..73e51767 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt @@ -81,7 +81,7 @@ abstract class RequestFactory : Closeable { /** * Request will be used for Server-Sent Events connections. */ - Events + Events, } /** @@ -94,7 +94,10 @@ abstract class RequestFactory : Closeable { * @param typeId Problem type id to register. * @param problemType [Problem] subclass to map to [typeId]. */ - abstract fun registerProblem(typeId: String, problemType: KClass) + abstract fun registerProblem( + typeId: String, + problemType: KClass, + ) abstract val registeredProblemTypes: Map> abstract val mediaTypeEncoders: MediaTypeEncoders @@ -111,7 +114,7 @@ abstract class RequestFactory : Closeable { queryParameters: Parameters? = null, contentTypes: List? = null, acceptTypes: List? = null, - headers: Parameters? = null + headers: Parameters? = null, ) = request( method, pathTemplate, @@ -120,7 +123,7 @@ abstract class RequestFactory : Closeable { null as Unit?, contentTypes, acceptTypes, - headers + headers, ) /** @@ -135,7 +138,7 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - purpose: RequestPurpose = RequestPurpose.Normal + purpose: RequestPurpose = RequestPurpose.Normal, ): Request /** @@ -159,7 +162,7 @@ abstract class RequestFactory : Closeable { queryParameters: Parameters? = null, contentTypes: List? = null, acceptTypes: List? = null, - headers: Parameters? = null + headers: Parameters? = null, ) = response( method, pathTemplate, @@ -168,7 +171,7 @@ abstract class RequestFactory : Closeable { null as Unit?, contentTypes, acceptTypes, - headers + headers, ) /** @@ -185,7 +188,7 @@ abstract class RequestFactory : Closeable { body: B? = null, contentTypes: List? = null, acceptTypes: List? = null, - headers: Parameters? = null + headers: Parameters? = null, ): Response { val request = request( @@ -196,7 +199,7 @@ abstract class RequestFactory : Closeable { body, contentTypes, acceptTypes, - headers + headers, ) return response(request) @@ -217,17 +220,18 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - ): R = result( - method, - pathTemplate, - pathParameters, - queryParameters, - body, - contentTypes, - acceptTypes, - headers, - typeOf() - ) + ): R = + result( + method, + pathTemplate, + pathParameters, + queryParameters, + body, + contentTypes, + acceptTypes, + headers, + typeOf(), + ) /** * Create and execute a [request][Request] created from the given request @@ -243,17 +247,18 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - ): R = result( - method, - pathTemplate, - pathParameters, - queryParameters, - null as Unit?, - contentTypes, - acceptTypes, - headers, - typeOf() - ) + ): R = + result( + method, + pathTemplate, + pathParameters, + queryParameters, + null as Unit?, + contentTypes, + acceptTypes, + headers, + typeOf(), + ) /** * Create and execute a [request][Request] created from the given request @@ -270,18 +275,19 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - resultType: KType - ): R = resultResponse( - method, - pathTemplate, - pathParameters, - queryParameters, - body, - contentTypes, - acceptTypes, - headers, - resultType, - ).result + resultType: KType, + ): R = + resultResponse( + method, + pathTemplate, + pathParameters, + queryParameters, + body, + contentTypes, + acceptTypes, + headers, + resultType, + ).result /** * Create and execute a [request][Request] created from the given request @@ -299,17 +305,18 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - ): ResultResponse = resultResponse( - method, - pathTemplate, - pathParameters, - queryParameters, - body, - contentTypes, - acceptTypes, - headers, - typeOf() - ) + ): ResultResponse = + resultResponse( + method, + pathTemplate, + pathParameters, + queryParameters, + body, + contentTypes, + acceptTypes, + headers, + typeOf(), + ) /** * Create and execute a [request][Request] created from the given request @@ -326,17 +333,18 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - ): ResultResponse = resultResponse( - method, - pathTemplate, - pathParameters, - queryParameters, - null as Unit?, - contentTypes, - acceptTypes, - headers, - typeOf() - ) + ): ResultResponse = + resultResponse( + method, + pathTemplate, + pathParameters, + queryParameters, + null as Unit?, + contentTypes, + acceptTypes, + headers, + typeOf(), + ) /** * Create and execute a [request][Request] created from the given request @@ -354,9 +362,8 @@ abstract class RequestFactory : Closeable { contentTypes: List? = null, acceptTypes: List? = null, headers: Parameters? = null, - resultType: KType + resultType: KType, ): ResultResponse { - val response = response( method, @@ -366,7 +373,7 @@ abstract class RequestFactory : Closeable { body, contentTypes, acceptTypes, - headers + headers, ) if (isFailureResponse(response)) { @@ -382,7 +389,7 @@ abstract class RequestFactory : Closeable { response, resultType, ), - response + response, ) } @@ -400,19 +407,20 @@ abstract class RequestFactory : Closeable { body: B? = null, contentTypes: List? = null, acceptTypes: List? = null, - headers: Parameters? = null - ): EventSource = eventSource { - request( - method, - pathTemplate, - pathParameters, - queryParameters, - body, - contentTypes, - acceptTypes, - headers - ) - } + headers: Parameters? = null, + ): EventSource = + eventSource { + request( + method, + pathTemplate, + pathParameters, + queryParameters, + body, + contentTypes, + acceptTypes, + headers, + ) + } /** * Creates an [EventSource] that uses the provided request parameters to supply @@ -427,21 +435,22 @@ abstract class RequestFactory : Closeable { queryParameters: Parameters? = null, contentTypes: List? = null, acceptTypes: List? = null, - headers: Parameters? = null - ): EventSource = eventSource { eventSourceHeaders -> - request( - method, - pathTemplate, - pathParameters, - queryParameters, - null as Unit?, - contentTypes, - acceptTypes, - eventSourceHeaders.let { esHeaders -> - (headers?.let { esHeaders + HeaderParameters.encode(headers) } ?: esHeaders).toMap() - } - ) - } + headers: Parameters? = null, + ): EventSource = + eventSource { eventSourceHeaders -> + request( + method, + pathTemplate, + pathParameters, + queryParameters, + null as Unit?, + contentTypes, + acceptTypes, + eventSourceHeaders.let { esHeaders -> + (headers?.let { esHeaders + HeaderParameters.encode(headers) } ?: esHeaders).toMap() + }, + ) + } /** * Creates an [EventSource] that uses the provided [requestSupplier] to supply the [Request] @@ -468,18 +477,19 @@ abstract class RequestFactory : Closeable { acceptTypes: List? = null, headers: Parameters? = null, decoder: (TextMediaTypeDecoder, String?, String?, String, Logger) -> D?, - ): Flow = eventStream(decoder) { - request( - method, - pathTemplate, - pathParameters, - queryParameters, - body, - contentTypes, - acceptTypes, - headers - ) - } + ): Flow = + eventStream(decoder) { + request( + method, + pathTemplate, + pathParameters, + queryParameters, + body, + contentTypes, + acceptTypes, + headers, + ) + } /** * Creates an [Flow] of events that uses the provided request parameters to supply @@ -497,18 +507,19 @@ abstract class RequestFactory : Closeable { acceptTypes: List? = null, headers: Parameters? = null, decoder: (TextMediaTypeDecoder, String?, String?, String, Logger) -> D?, - ): Flow = eventStream(decoder) { - request( - method, - pathTemplate, - pathParameters, - queryParameters, - null as Unit?, - contentTypes, - acceptTypes, - headers - ) - } + ): Flow = + eventStream(decoder) { + request( + method, + pathTemplate, + pathParameters, + queryParameters, + null as Unit?, + contentTypes, + acceptTypes, + headers, + ) + } /** * Creates a [Flow] of events that uses the provided [requestSupplier] to supply the [Request] @@ -520,59 +531,57 @@ abstract class RequestFactory : Closeable { */ fun eventStream( decoder: (TextMediaTypeDecoder, String?, String?, String, Logger) -> D?, - requestSupplier: suspend (Headers) -> Request - ): Flow = callbackFlow { - - val jsonDecoder = mediaTypeDecoders.find(JSON) ?: throw SundayError(NoDecoder, JSON.value) - jsonDecoder as TextMediaTypeDecoder - - val eventSource = eventSource(requestSupplier) - - eventSource.onMessage = { event -> - - val data = event.data - if (data != null) { - - try { - - val decodedEvent = decoder(jsonDecoder, event.event, event.id, data, logger) - if (decodedEvent != null) { - - trySendBlocking(decodedEvent) - .onFailure { - cancel("Event send failed", it) - } - + requestSupplier: suspend (Headers) -> Request, + ): Flow = + callbackFlow { + val jsonDecoder = mediaTypeDecoders.find(JSON) ?: throw SundayError(NoDecoder, JSON.value) + jsonDecoder as TextMediaTypeDecoder + + val eventSource = eventSource(requestSupplier) + + eventSource.onMessage = { event -> + + val data = event.data + if (data != null) { + try { + val decodedEvent = decoder(jsonDecoder, event.event, event.id, data, logger) + if (decodedEvent != null) { + trySendBlocking(decodedEvent) + .onFailure { + cancel("Event send failed", it) + } + + } + + } catch (x: Throwable) { + cancel("Event decoding failed", SundayError(EventDecodingFailed, cause = x)) } - } catch (x: Throwable) { - cancel("Event decoding failed", SundayError(EventDecodingFailed, cause = x)) } } - } - - eventSource.onError = { error -> - logger.warn("EventSource error encountered", error) - } + eventSource.onError = { error -> + logger.warn("EventSource error encountered", error) + } - eventSource.connect() + eventSource.connect() - awaitClose { - logger.debug("Stream closed or canceled") + awaitClose { + logger.debug("Stream closed or canceled") - eventSource.close() + eventSource.close() + } } - } abstract fun close(cancelOutstandingRequests: Boolean) - private fun isFailureResponse(response: Response) = - failureStatusCodes.contains(response.statusCode) - - private fun parseSuccess(response: Response, resultType: KType): T { + private fun isFailureResponse(response: Response) = failureStatusCodes.contains(response.statusCode) + private fun parseSuccess( + response: Response, + resultType: KType, + ): T { val body = response.body if (emptyDataStatusCodes.contains(response.statusCode)) { if (resultType != typeOf()) { @@ -590,14 +599,14 @@ abstract class RequestFactory : Closeable { response.contentType?.let { MediaType.from(it.toString()) } ?: throw SundayError( SundayError.Reason.InvalidContentType, - response.contentType?.value ?: "" + response.contentType?.value ?: "", ) - val contentTypeDecoder = mediaTypeDecoders.find(contentType) - ?: throw SundayError(NoDecoder, contentType.value) + val contentTypeDecoder = + mediaTypeDecoders.find(contentType) + ?: throw SundayError(NoDecoder, contentType.value) try { - return contentTypeDecoder.decode(body, resultType) } catch (x: Throwable) { throw SundayError(SundayError.Reason.ResponseDecodingFailed, cause = x) @@ -605,7 +614,6 @@ abstract class RequestFactory : Closeable { } private fun parseFailure(response: Response): ThrowableProblem { - val status = try { Status.valueOf(response.statusCode) @@ -616,12 +624,13 @@ abstract class RequestFactory : Closeable { return parseFailureResponseBody(response, status) } - private fun parseFailureResponseBody(response: Response, status: StatusType): ThrowableProblem { - + private fun parseFailureResponseBody( + response: Response, + status: StatusType, + ): ThrowableProblem { val body = response.body return if (body != null && response.contentLength != 0L) { - val contentType = response.contentType?.let { MediaType.from(it.toString()) } ?: MediaType.OctetStream @@ -644,7 +653,7 @@ abstract class RequestFactory : Closeable { problemDecoder as? StructuredMediaTypeDecoder ?: throw SundayError( NoDecoder, - "'${MediaType.Problem}' decoder must support structured decoding" + "'${MediaType.Problem}' decoder must support structured decoding", ) val decoded: Map = problemDecoder.decode(body) @@ -658,15 +667,17 @@ abstract class RequestFactory : Closeable { private fun parseUnknownFailureResponseBody( contentType: MediaType, body: BufferedSource, - status: StatusType + status: StatusType, ): ThrowableProblem { val (responseText, responseData) = - if (contentType.compatible(MediaType.AnyText)) + if (contentType.compatible(MediaType.AnyText)) { body.readString(Charsets.from(contentType)) to null - else + } else { null to body.readByteArray() + } - return Problem.builder() + return Problem + .builder() .withStatus(status) .withTitle(status.reasonPhrase) .apply { @@ -676,8 +687,7 @@ abstract class RequestFactory : Closeable { if (responseData != null) { with("responseData", responseData) } - } - .build() + }.build() } } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt b/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt index 328bd4a6..dfc26021 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt @@ -16,11 +16,16 @@ package io.outfoxx.sunday -class SundayError(message: String, val reason: Reason? = null, cause: Throwable? = null) : - RuntimeException(message, cause) { +class SundayError( + message: String, + val reason: Reason? = null, + cause: Throwable? = null, +) : RuntimeException(message, cause) { - /* ktlint-disable max-line-length */ - enum class Reason(val message: String) { + @Suppress("ktlint:standard:max-line-length") + enum class Reason( + val message: String, + ) { UnexpectedEmptyResponse("Unexpected empty reason"), NoData("No data in response when method requires it"), InvalidContentType("Invalid Content-Type"), @@ -29,11 +34,10 @@ class SundayError(message: String, val reason: Reason? = null, cause: Throwable? ResponseDecodingFailed("Response decoding failed"), EventDecodingFailed("Event decoding failed"), InvalidBaseUri("Base URL is invalid after expanding template"), - NoSupportedContentTypes("None of the provided Content-Types for the request has a registered decoder"), + NoSupportedContentTypes("None of the provided Content-Types for the request has a registered encoder"), NoSupportedAcceptTypes("None of the provided Accept types for the request has a registered decoder"), - InvalidHeaderValue("The encoded header value contains one or more invalid characters") + InvalidHeaderValue("The encoded header value contains one or more invalid characters"), } - /* ktlint-enable max-line-length */ constructor(reason: Reason, extraMessage: String? = null, cause: Throwable? = null) : this("${reason.message}${extraMessage?.let { " $it" } ?: ""}", reason, cause) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/URITemplate.kt b/core/src/main/kotlin/io/outfoxx/sunday/URITemplate.kt index 042c3408..1962935c 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/URITemplate.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/URITemplate.kt @@ -50,16 +50,16 @@ class URITemplate( fun resolve( relative: String? = null, parameters: Parameters? = null, - encoders: Map, PathEncoder> = mapOf() + encoders: Map, PathEncoder> = PathEncoders.default, ): URIBuilder { - val template = URITemplate(join(template, relative)) val allParameters = - if (parameters != null) + if (parameters != null) { this.parameters + parameters - else + } else { this.parameters + } val allStringParameters = allParameters @@ -68,24 +68,27 @@ class URITemplate( value!! encoders.entries .firstOrNull { it.key.isInstance(value) } - ?.value?.invoke(value) + ?.value + ?.invoke(value) ?: value.toString() } return template.expand(allStringParameters).toBuilder() } - private fun join(base: String, relative: String?) = - if (relative != null) { - if (base.endsWith("/") && relative.startsWith("/")) { - base + relative.removePrefix("/") - } else if (base.endsWith("/") || relative.startsWith("/")) { - base + relative - } else { - "$base/$relative" - } + private fun join( + base: String, + relative: String?, + ) = if (relative != null) { + if (base.endsWith("/") && relative.startsWith("/")) { + base + relative.removePrefix("/") + } else if (base.endsWith("/") || relative.startsWith("/")) { + base + relative } else { - base + "$base/$relative" } + } else { + base + } } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderNames.kt b/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderNames.kt index 9b69416f..98d788ed 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderNames.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderNames.kt @@ -19,8 +19,8 @@ package io.outfoxx.sunday.http /** * Commonly used HTTP header names. */ +@Suppress("ktlint") object HeaderNames { - const val Accept = "accept" const val Authorization = "authorization" const val Connection = "connection" diff --git a/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderParameters.kt b/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderParameters.kt index 0917ac9c..93d9726b 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderParameters.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/http/HeaderParameters.kt @@ -38,7 +38,7 @@ object HeaderParameters { private fun encodeParameter( headerName: String, - headerParameter: Any? + headerParameter: Any?, ): List> = when (headerParameter) { null -> emptyList() @@ -63,7 +63,10 @@ object HeaderParameters { * * Checks that each character is ASCII. Also disallowing NULL, CR, and LF. */ - private fun validate(headerName: String, headerValue: String): String { + private fun validate( + headerName: String, + headerValue: String, + ): String { for (char in headerValue) { if (!asciiEncoder.canEncode(char) || isDisallowedChar(char)) { throw SundayError(InvalidHeaderValue, ": header=$headerName, value=$headerValue") @@ -72,7 +75,6 @@ object HeaderParameters { return headerValue } - private fun isDisallowedChar(char: Char): Boolean = - char == 0.toChar() || char == '\r' || char == '\n' + private fun isDisallowedChar(char: Char): Boolean = char == 0.toChar() || char == '\r' || char == '\n' } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/http/Headers.kt b/core/src/main/kotlin/io/outfoxx/sunday/http/Headers.kt index c5f73e8f..3a397023 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/http/Headers.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/http/Headers.kt @@ -27,16 +27,14 @@ typealias Headers = Iterable> * @param name Name of the header to find; names are compared case insensitively. * @throws NoSuchElementException Thrown when no header with the given name was found. */ -fun Headers.getFirst(name: String): String = - first { it.first.equals(name, ignoreCase = true) }.second +fun Headers.getFirst(name: String): String = first { it.first.equals(name, ignoreCase = true) }.second /** * Retrieves the first header matching the given name or returns null. * * @param name Name of the header to find; names are compared case insensitively. */ -fun Headers.getFirstOrNull(name: String): String? = - firstOrNull { it.first.equals(name, ignoreCase = true) }?.second +fun Headers.getFirstOrNull(name: String): String? = firstOrNull { it.first.equals(name, ignoreCase = true) }?.second /** * Retrieves all the headers matching the given name. @@ -51,5 +49,4 @@ fun Headers.getAll(name: String): Iterable = /** * Converts the header list into an equivalent multi-map. */ -fun Headers.toMultiMap(): Map> = - groupBy({ it.first.lowercase() }, { it.second }) +fun Headers.toMultiMap(): Map> = groupBy({ it.first.lowercase() }, { it.second }) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/http/Method.kt b/core/src/main/kotlin/io/outfoxx/sunday/http/Method.kt index bca75d69..16a2cf82 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/http/Method.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/http/Method.kt @@ -25,7 +25,10 @@ package io.outfoxx.sunday.http * Constants are provided for the common methods * [Options], [Get], [Head], [Post], [Put], [Patch], [Delete], [Trace], and [Connect]. */ -data class Method(val name: String, val requiresBody: Boolean = false) { +data class Method( + val name: String, + val requiresBody: Boolean = false, +) { companion object { diff --git a/core/src/main/kotlin/io/outfoxx/sunday/http/Request.kt b/core/src/main/kotlin/io/outfoxx/sunday/http/Request.kt index 2b428046..4ab28e40 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/http/Request.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/http/Request.kt @@ -66,19 +66,25 @@ interface Request { * and [Response.trailers] will not be available. The response body data, * if any, will be provided in [Data] events that follow. */ - data class Start(val value: Response) : Event + data class Start( + val value: Response, + ) : Event /** * HTTP response body data has been received. */ - data class Data(val value: Buffer) : Event + data class Data( + val value: Buffer, + ) : Event /** * HTTP response has completed. * * Response trailers are provided, if any were delivered. */ - data class End(val trailers: Headers) : Event + data class End( + val trailers: Headers, + ) : Event } /** diff --git a/core/src/main/kotlin/io/outfoxx/sunday/json/patch/Patching.kt b/core/src/main/kotlin/io/outfoxx/sunday/json/patch/Patching.kt index 3c9a1c6c..6f8632b7 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/json/patch/Patching.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/json/patch/Patching.kt @@ -45,7 +45,10 @@ sealed class PatchOp { /** * JSON Merge Patch `set` operation that sets/replaces the current value. */ - data class Set(val value: T) : PatchOp(), UpdateOp { + data class Set( + val value: T, + ) : PatchOp(), + UpdateOp { override fun toString() = "set($value)" } @@ -66,15 +69,17 @@ sealed class PatchOp { /** * JSON Merge Patch `none` operation that leaves the current value untouched. */ - class None private constructor() : PatchOp(), UpdateOp { + class None private constructor() : + PatchOp(), + UpdateOp { - companion object { + companion object { - val instance = None() - } + val instance = None() + } - override fun toString() = "none" - } + override fun toString() = "none" + } @Suppress("UNCHECKED_CAST") companion object { @@ -126,15 +131,20 @@ sealed class PatchOp { */ class Serializer : JsonSerializer>() { - override fun isEmpty(provider: SerializerProvider?, value: PatchOp<*>?): Boolean = - value is PatchOp.None - - override fun serialize(value: PatchOp<*>, gen: JsonGenerator, serializers: SerializerProvider) = - when (value) { - is Set<*> -> serializers.defaultSerializeValue(value.value, gen) - is Delete<*> -> gen.writeNull() - is PatchOp.None -> error("isEmpty should handle this state") - } + override fun isEmpty( + provider: SerializerProvider?, + value: PatchOp<*>?, + ): Boolean = value is PatchOp.None + + override fun serialize( + value: PatchOp<*>, + gen: JsonGenerator, + serializers: SerializerProvider, + ) = when (value) { + is Set<*> -> serializers.defaultSerializeValue(value.value, gen) + is Delete<*> -> gen.writeNull() + is PatchOp.None -> error("isEmpty should handle this state") + } } @@ -143,24 +153,28 @@ sealed class PatchOp { */ object Deserializer : JsonDeserializer>(), ContextualDeserializer { - class TypedDeserializer(private val type: JavaType) : JsonDeserializer>() { + class TypedDeserializer( + private val type: JavaType, + ) : JsonDeserializer>() { override fun getNullValue(ctxt: DeserializationContext): PatchOp = delete() - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PatchOp = - Set(ctxt.readValue(p, type)) + override fun deserialize( + p: JsonParser, + ctxt: DeserializationContext, + ): PatchOp = Set(ctxt.readValue(p, type)) } override fun createContextual( ctxt: DeserializationContext, - property: BeanProperty - ): JsonDeserializer<*> { - return TypedDeserializer(property.type.containedType(0)) - } + property: BeanProperty, + ): JsonDeserializer<*> = TypedDeserializer(property.type.containedType(0)) - override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): PatchOp = - error("Should not be called") + override fun deserialize( + p: JsonParser?, + ctxt: DeserializationContext?, + ): PatchOp = error("Should not be called") } } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/BinaryDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/BinaryDecoder.kt index acd29cb1..5186bfd3 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/BinaryDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/BinaryDecoder.kt @@ -40,7 +40,10 @@ class BinaryDecoder : MediaTypeDecoder { } - override fun decode(data: Source, type: KType): T = + override fun decode( + data: Source, + type: KType, + ): T = @Suppress("UNCHECKED_CAST") when (type.classifier) { ByteArray::class -> data.buffer().readByteArray() as T diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBORDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBORDecoder.kt index af75e28a..925a7eef 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBORDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBORDecoder.kt @@ -27,7 +27,9 @@ import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper * * @see [ObjectMapperDecoder] */ -class CBORDecoder(cborMapper: CBORMapper) : ObjectMapperDecoder(cborMapper) { +class CBORDecoder( + cborMapper: CBORMapper, +) : ObjectMapperDecoder(cborMapper) { companion object { @@ -41,10 +43,9 @@ class CBORDecoder(cborMapper: CBORMapper) : ObjectMapperDecoder(cborMapper) { .setBase64Variant( Base64Variants.MIME_NO_LINEFEEDS .withReadPadding(Base64Variant.PaddingReadBehaviour.PADDING_ALLOWED) - .withWritePadding(false) - ) - .enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) - .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as CBORMapper + .withWritePadding(false), + ).enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as CBORMapper, ) } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBOREncoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBOREncoder.kt index ca3d3cf4..251e6102 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBOREncoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/CBOREncoder.kt @@ -25,7 +25,9 @@ import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper * * @see [ObjectMapperEncoder] */ -class CBOREncoder(cborMapper: CBORMapper) : ObjectMapperEncoder(cborMapper) { +class CBOREncoder( + cborMapper: CBORMapper, +) : ObjectMapperEncoder(cborMapper) { companion object { @@ -37,7 +39,7 @@ class CBOREncoder(cborMapper: CBORMapper) : ObjectMapperEncoder(cborMapper) { CBORMapper() .findAndRegisterModules() .enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) - .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as CBORMapper + .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as CBORMapper, ) } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONDecoder.kt index 41e11cfb..3b950830 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONDecoder.kt @@ -30,7 +30,10 @@ import kotlin.reflect.jvm.javaType * * @see [ObjectMapperDecoder] */ -class JSONDecoder(jsonMapper: JsonMapper) : ObjectMapperDecoder(jsonMapper), TextMediaTypeDecoder { +class JSONDecoder( + jsonMapper: JsonMapper, +) : ObjectMapperDecoder(jsonMapper), + TextMediaTypeDecoder { companion object { @@ -44,14 +47,15 @@ class JSONDecoder(jsonMapper: JsonMapper) : ObjectMapperDecoder(jsonMapper), Tex .setBase64Variant( Base64Variants.MIME_NO_LINEFEEDS .withReadPadding(Base64Variant.PaddingReadBehaviour.PADDING_ALLOWED) - .withWritePadding(false) - ) - .enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) - .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as JsonMapper + .withWritePadding(false), + ).enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) + .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as JsonMapper, ) } - override fun decode(data: String, type: KType): T = - objectMapper.readValue(data, TypeFactory.defaultInstance().constructType(type.javaType)) + override fun decode( + data: String, + type: KType, + ): T = objectMapper.readValue(data, TypeFactory.defaultInstance().constructType(type.javaType)) } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONEncoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONEncoder.kt index 66f5ddbe..dedf72f9 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONEncoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/JSONEncoder.kt @@ -25,7 +25,9 @@ import com.fasterxml.jackson.databind.json.JsonMapper * * @see [ObjectMapperEncoder] */ -class JSONEncoder(jsonMapper: JsonMapper) : ObjectMapperEncoder(jsonMapper) { +class JSONEncoder( + jsonMapper: JsonMapper, +) : ObjectMapperEncoder(jsonMapper) { companion object { @@ -37,7 +39,7 @@ class JSONEncoder(jsonMapper: JsonMapper) : ObjectMapperEncoder(jsonMapper) { JsonMapper() .findAndRegisterModules() .enable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS) - .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as JsonMapper + .enable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) as JsonMapper, ) } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoder.kt index 797169fd..5e9790ef 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoder.kt @@ -35,7 +35,10 @@ interface MediaTypeDecoder { * @param type Target Java/Kotlin type. * @return Instance of [T]. */ - fun decode(data: Source, type: KType): T + fun decode( + data: Source, + type: KType, + ): T } /** diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoders.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoders.kt index 4a5ef021..2e30072b 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoders.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeDecoders.kt @@ -32,15 +32,16 @@ import io.outfoxx.sunday.MediaType.Companion.X509UserCert * Container for [MediaTypeDecoder]s that allows registering and * locating decoders for specific [media types][MediaType]. */ -class MediaTypeDecoders(private val registered: Map) { +class MediaTypeDecoders( + private val registered: Map, +) { /** * Check if the given [media type][MediaType] has a decoder registered. * * @return `true` if a decoder is available. */ - fun supports(mediaType: MediaType) = - registered.keys.any { mediaType.compatible(it) } + fun supports(mediaType: MediaType) = registered.keys.any { mediaType.compatible(it) } /** * Locates a compatible decoder for the given [media type][MediaType]. @@ -48,13 +49,14 @@ class MediaTypeDecoders(private val registered: Map * @param mediaType Media type to locate decoder for. * @return Compatible [MediaTypeDecoder] or null if none found. */ - fun find(mediaType: MediaType) = - registered.entries.firstOrNull { it.key.compatible(mediaType) }?.value + fun find(mediaType: MediaType) = registered.entries.firstOrNull { it.key.compatible(mediaType) }?.value /** * Builder for [MediaTypeDecoders]. */ - class Builder(private val registered: Map = mapOf()) { + class Builder( + private val registered: Map = mapOf(), + ) { /** * Registers all the default decoders. @@ -75,16 +77,14 @@ class MediaTypeDecoders(private val registered: Map * * @return Fluent builder. */ - fun registerData() = - register(BinaryDecoder(), OctetStream) + fun registerData() = register(BinaryDecoder(), OctetStream) /** * Registers the default JSON decoder. * * @return Fluent builder. */ - fun registerJSON() = - register(JSONDecoder.default, JSON, JSONStructured) + fun registerJSON() = register(JSONDecoder.default, JSON, JSONStructured) /** * Registers a custom JSON decoder. @@ -92,16 +92,14 @@ class MediaTypeDecoders(private val registered: Map * @param mapper Jackson mapper to use for decoding. * @return Fluent builder. */ - fun registerJSON(mapper: JsonMapper) = - register(JSONDecoder(mapper), JSON, JSONStructured) + fun registerJSON(mapper: JsonMapper) = register(JSONDecoder(mapper), JSON, JSONStructured) /** * Registers the default JSON decoder. * * @return Fluent builder. */ - fun registerCBOR() = - register(CBORDecoder.default, CBOR) + fun registerCBOR() = register(CBORDecoder.default, CBOR) /** * Registers a custom CBOR encoder. @@ -109,16 +107,14 @@ class MediaTypeDecoders(private val registered: Map * @param mapper Jackson mapper to use for decoding. * @return Fluent builder. */ - fun registerCBOR(mapper: CBORMapper) = - register(CBORDecoder(mapper), CBOR) + fun registerCBOR(mapper: CBORMapper) = register(CBORDecoder(mapper), CBOR) /** * Registers the default UTF-8 text decoder. * * @return Fluent builder. */ - fun registerText() = - register(TextDecoder.default, AnyText) + fun registerText() = register(TextDecoder.default, AnyText) /** * Registers a dummy binary decoder for Server-Sent Events streams. @@ -128,16 +124,14 @@ class MediaTypeDecoders(private val registered: Map * * @return Fluent builder. */ - fun registerEventStream() = - register(BinaryDecoder.default, EventStream) + fun registerEventStream() = register(BinaryDecoder.default, EventStream) /** * Registers a binary decoder for X509 types. * * @return Fluent builder. */ - fun registerX509() = - register(BinaryDecoder.default, X509CACert, X509UserCert) + fun registerX509() = register(BinaryDecoder.default, X509CACert, X509UserCert) /** * Registers a decoder with specific media types. @@ -146,8 +140,10 @@ class MediaTypeDecoders(private val registered: Map * @param types Media types to associate with [decoder]. * @return Fluent builder. */ - fun register(decoder: MediaTypeDecoder, vararg types: MediaType) = - Builder(registered.plus(types.map { it to decoder })) + fun register( + decoder: MediaTypeDecoder, + vararg types: MediaType, + ) = Builder(registered.plus(types.map { it to decoder })) /** * Builds the immutable [MediaTypeDecoders] instance. diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeEncoders.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeEncoders.kt index fc0fc81c..5df9d880 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeEncoders.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/MediaTypeEncoders.kt @@ -33,15 +33,16 @@ import io.outfoxx.sunday.MediaType.Companion.X509UserCert * Container for [MediaTypeEncoder]s that allows registering and * locating encoders for specific [media types][MediaType]. */ -class MediaTypeEncoders(private val registered: Map) { +class MediaTypeEncoders( + private val registered: Map, +) { /** * Check if the given [media type][MediaType] has an encoder registered. * * @return `true` if an encoder is available. */ - fun supports(mediaType: MediaType) = - registered.keys.any { mediaType.compatible(it) } + fun supports(mediaType: MediaType) = registered.keys.any { mediaType.compatible(it) } /** * Locates a compatible encoder for the given [media type][MediaType]. @@ -49,13 +50,14 @@ class MediaTypeEncoders(private val registered: Map * @param mediaType Media type to locate encoder for. * @return Compatible [MediaTypeEncoder] or null if none found. */ - fun find(mediaType: MediaType) = - registered.entries.firstOrNull { it.key.compatible(mediaType) }?.value + fun find(mediaType: MediaType) = registered.entries.firstOrNull { it.key.compatible(mediaType) }?.value /** * Builder for [MediaTypeEncoders]. */ - class Builder(val registered: Map = mapOf()) { + class Builder( + val registered: Map = mapOf(), + ) { /** * Registers all the default encoders. @@ -87,11 +89,11 @@ class MediaTypeEncoders(private val registered: Map WWWFormURLEncoder.BoolEncoding.Literal, dateEncoding: WWWFormURLEncoder.DateEncoding = WWWFormURLEncoder.DateEncoding.FractionalSecondsSinceEpoch, - mapper: ObjectMapper = ObjectMapper().findAndRegisterModules() + mapper: ObjectMapper = ObjectMapper().findAndRegisterModules(), ): Builder = register( WWWFormURLEncoder(arrayEncoding, boolEncoding, dateEncoding, mapper), - WWWFormUrlEncoded + WWWFormUrlEncoded, ) /** @@ -99,16 +101,14 @@ class MediaTypeEncoders(private val registered: Map * * @return Fluent builder. */ - fun registerData() = - register(BinaryEncoder.default, OctetStream) + fun registerData() = register(BinaryEncoder.default, OctetStream) /** * Registers the default JSON encoder. * * @return Fluent builder. */ - fun registerJSON() = - register(JSONEncoder.default, JSON, JSONStructured) + fun registerJSON() = register(JSONEncoder.default, JSON, JSONStructured) /** * Registers a custom JSON encoder. @@ -116,16 +116,14 @@ class MediaTypeEncoders(private val registered: Map * @param mapper Jackson mapper to use for encoding. * @return Fluent builder. */ - fun registerJSON(mapper: JsonMapper) = - register(JSONEncoder(mapper), JSON, JSONStructured) + fun registerJSON(mapper: JsonMapper) = register(JSONEncoder(mapper), JSON, JSONStructured) /** * Registers the default CBOR encoder. * * @return Fluent builder. */ - fun registerCBOR() = - register(CBOREncoder.default, CBOR) + fun registerCBOR() = register(CBOREncoder.default, CBOR) /** * Registers a custom CBOR encoder. @@ -133,24 +131,21 @@ class MediaTypeEncoders(private val registered: Map * @param mapper Jackson mapper to use for encoding. * @return Fluent builder. */ - fun registerCBOR(mapper: CBORMapper) = - register(CBOREncoder(mapper), CBOR) + fun registerCBOR(mapper: CBORMapper) = register(CBOREncoder(mapper), CBOR) /** * Registers the default UTF-8 text encoder. * * @return Fluent builder. */ - fun registerText() = - register(TextEncoder.default, AnyText) + fun registerText() = register(TextEncoder.default, AnyText) /** * Registers a binary encoder for X509 types. * * @return Fluent builder. */ - fun registerX509() = - register(BinaryEncoder.default, X509CACert, X509UserCert) + fun registerX509() = register(BinaryEncoder.default, X509CACert, X509UserCert) /** * Registers an encoder with specific media types. @@ -159,8 +154,10 @@ class MediaTypeEncoders(private val registered: Map * @param types Media types to associate with [encoder]. * @return Fluent builder. */ - fun register(encoder: MediaTypeEncoder, vararg types: MediaType) = - Builder(registered.plus(types.map { it to encoder })) + fun register( + encoder: MediaTypeEncoder, + vararg types: MediaType, + ) = Builder(registered.plus(types.map { it to encoder })) /** * Builds the immutable [MediaTypeEncoders] instance. diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperDecoder.kt index 6b14cbda..c1604b96 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperDecoder.kt @@ -31,8 +31,10 @@ import kotlin.reflect.jvm.javaType * Common Jackson [ObjectMapper] decoder that supports decoding * from binary data and structured [map][Map] data. */ -open class ObjectMapperDecoder(objectMapper: ObjectMapper) : - MediaTypeDecoder, StructuredMediaTypeDecoder { +open class ObjectMapperDecoder( + objectMapper: ObjectMapper, +) : MediaTypeDecoder, + StructuredMediaTypeDecoder { class CustomDeserializationProblemHandler : DeserializationProblemHandler() { @@ -41,7 +43,7 @@ open class ObjectMapperDecoder(objectMapper: ObjectMapper) : baseType: JavaType?, subTypeId: String?, idResolver: TypeIdResolver?, - failureMsg: String? + failureMsg: String?, ): JavaType? { // Ensure deserialization of Problem subclasses can be done explicitly without // registration @@ -53,15 +55,21 @@ open class ObjectMapperDecoder(objectMapper: ObjectMapper) : } val objectMapper: ObjectMapper = - objectMapper.copy() + objectMapper + .copy() .addHandler(CustomDeserializationProblemHandler()) - override fun decode(data: Source, type: KType): T = + override fun decode( + data: Source, + type: KType, + ): T = objectMapper.readValue( data.buffer().inputStream(), - objectMapper.typeFactory.constructType(type.javaType) + objectMapper.typeFactory.constructType(type.javaType), ) - override fun decode(data: Map, type: KType): T = - objectMapper.convertValue(data, objectMapper.typeFactory.constructType(type.javaType)) + override fun decode( + data: Map, + type: KType, + ): T = objectMapper.convertValue(data, objectMapper.typeFactory.constructType(type.javaType)) } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperEncoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperEncoder.kt index b63a55bd..3fb4e081 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperEncoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/ObjectMapperEncoder.kt @@ -24,8 +24,9 @@ import okio.Source * Common Jackson [ObjectMapper] encoder that supports encoding * Java/Kotlin values into binary data. */ -open class ObjectMapperEncoder(private val objectMapper: ObjectMapper) : MediaTypeEncoder { +open class ObjectMapperEncoder( + private val objectMapper: ObjectMapper, +) : MediaTypeEncoder { - override fun encode(value: B): Source = - Buffer().write(objectMapper.writeValueAsBytes(value)) + override fun encode(value: B): Source = Buffer().write(objectMapper.writeValueAsBytes(value)) } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/StructuredMediaTypeDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/StructuredMediaTypeDecoder.kt index 2995057b..d36e2bcf 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/StructuredMediaTypeDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/StructuredMediaTypeDecoder.kt @@ -31,7 +31,10 @@ interface StructuredMediaTypeDecoder : MediaTypeDecoder { * @param data Structured map to decode. * @param type Target Java/Kotlin type. */ - fun decode(data: Map, type: KType): T + fun decode( + data: Map, + type: KType, + ): T } /** @@ -41,5 +44,4 @@ interface StructuredMediaTypeDecoder : MediaTypeDecoder { * * @param data Structured map to decode. */ -inline fun StructuredMediaTypeDecoder.decode(data: Map): T = - decode(data, typeOf()) +inline fun StructuredMediaTypeDecoder.decode(data: Map): T = decode(data, typeOf()) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextDecoder.kt index 6c7b0bb8..aa026753 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextDecoder.kt @@ -26,7 +26,9 @@ import kotlin.reflect.KType * * Decoding to [String], and [CharSequence] is supported. */ -class TextDecoder(private val charset: Charset) : TextMediaTypeDecoder { +class TextDecoder( + private val charset: Charset, +) : TextMediaTypeDecoder { companion object { @@ -37,14 +39,20 @@ class TextDecoder(private val charset: Charset) : TextMediaTypeDecoder { } - override fun decode(data: Source, type: KType): T = + override fun decode( + data: Source, + type: KType, + ): T = @Suppress("UNCHECKED_CAST") when (type.classifier) { String::class, CharSequence::class -> data.buffer().readString(charset) as T else -> throw IllegalArgumentException("Unsupported type for text decode") } - override fun decode(data: String, type: KType): T = + override fun decode( + data: String, + type: KType, + ): T = @Suppress("UNCHECKED_CAST") when (type.classifier) { String::class, CharSequence::class -> data as T diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextEncoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextEncoder.kt index 670c2ff2..4c05df29 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextEncoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextEncoder.kt @@ -25,7 +25,9 @@ import java.nio.charset.Charset * * Encoding from [String], and [CharSequence] is supported. */ -class TextEncoder(private val charset: Charset) : MediaTypeEncoder { +class TextEncoder( + private val charset: Charset, +) : MediaTypeEncoder { companion object { diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextMediaTypeDecoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextMediaTypeDecoder.kt index 67514a7c..929d5276 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextMediaTypeDecoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/TextMediaTypeDecoder.kt @@ -31,7 +31,10 @@ interface TextMediaTypeDecoder : MediaTypeDecoder { * @param data Text data. * @param type Target Java/Kotlin type. */ - fun decode(data: String, type: KType): T + fun decode( + data: String, + type: KType, + ): T } /** @@ -40,5 +43,4 @@ interface TextMediaTypeDecoder : MediaTypeDecoder { * @param data Text data. * @param type Target Java/Kotlin type. */ -inline fun TextMediaTypeDecoder.decode(data: String): T = - decode(data, typeOf()) +inline fun TextMediaTypeDecoder.decode(data: String): T = decode(data, typeOf()) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/WWWFormURLEncoder.kt b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/WWWFormURLEncoder.kt index 74ea5b16..453560c0 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/WWWFormURLEncoder.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/mediatypes/codecs/WWWFormURLEncoder.kt @@ -37,7 +37,7 @@ class WWWFormURLEncoder( private val arrayEncoding: ArrayEncoding, private val boolEncoding: BoolEncoding, private val dateEncoding: DateEncoding, - private val mapper: ObjectMapper = ObjectMapper().findAndRegisterModules() + private val mapper: ObjectMapper = ObjectMapper().findAndRegisterModules(), ) : URLQueryParamsEncoder { companion object { @@ -49,13 +49,12 @@ class WWWFormURLEncoder( WWWFormURLEncoder( ArrayEncoding.Bracketed, BoolEncoding.Numeric, - DateEncoding.ISO8601 + DateEncoding.ISO8601, ) private val URI_ENCODE_COMPONENT_FIXES_REGEX = """\+|%21|%27|%28|%29|%7E""".toRegex(IGNORE_CASE) private fun encodeURIComponent(value: Any): String { - val result = URLEncoder.encode("$value", UTF_8) return URI_ENCODE_COMPONENT_FIXES_REGEX.replace(result) { matchResult -> @@ -73,62 +72,74 @@ class WWWFormURLEncoder( } - enum class ArrayEncoding(val encode: (String) -> String) { + enum class ArrayEncoding( + val encode: (String) -> String, + ) { Bracketed({ "$it[]" }), - Unbracketed({ it }) + Unbracketed({ it }), } - enum class BoolEncoding(val encode: (Boolean) -> String) { + enum class BoolEncoding( + val encode: (Boolean) -> String, + ) { Numeric({ if (it) "1" else "0" }), - Literal({ if (it) "true" else "false" }) + Literal({ if (it) "true" else "false" }), } - enum class DateEncoding(val encode: (Instant) -> String) { + enum class DateEncoding( + val encode: (Instant) -> String, + ) { FractionalSecondsSinceEpoch( { (it.epochSecond + (it.nano / TimeUnit.SECONDS.toNanos(1).toDouble())) .toBigDecimal() .toPlainString() - } + }, ), MillisecondsSinceEpoch({ it.toEpochMilli().toString() }), - ISO8601({ ISO_INSTANT.format(it) }) + ISO8601({ ISO_INSTANT.format(it) }), } override fun encode(value: T): Source { - val parameters = mapper.convertValue>(value as Any) return Buffer().write(encodeQueryString(parameters).toByteArray(US_ASCII)) } override fun encodeQueryString(parameters: Parameters): String = - parameters.toSortedMap(compareBy { it }) + parameters + .toSortedMap(compareBy { it }) .flatMap { (key, value) -> encodeQueryComponent(key, value) - } - .joinToString("&") + }.joinToString("&") - private fun encodeQueryComponent(key: String, value: Any?): List = + private fun encodeQueryComponent( + key: String, + value: Any?, + ): List = when (value) { null -> listOf(encodeURIComponent(key)) - is Map<*, *> -> value.toSortedMap(compareBy { it.toString() }) - .flatMap { (nestedKey, value) -> encodeQueryComponent("$key[$nestedKey]", value as Any) } - - is Iterable<*> -> value.flatMap { element -> - encodeQueryComponent( - arrayEncoding.encode(key), - element as Any - ) - } + is Map<*, *> -> + value + .toSortedMap(compareBy { it.toString() }) + .flatMap { (nestedKey, value) -> encodeQueryComponent("$key[$nestedKey]", value as Any) } + + is Iterable<*> -> + value.flatMap { element -> + encodeQueryComponent( + arrayEncoding.encode(key), + element as Any, + ) + } - is Array<*> -> value.flatMap { element -> - encodeQueryComponent( - arrayEncoding.encode(key), - element as Any - ) - } + is Array<*> -> + value.flatMap { element -> + encodeQueryComponent( + arrayEncoding.encode(key), + element as Any, + ) + } is Instant -> listOf(encodeURIComponent(key) + "=" + encodeURIComponent(dateEncoding.encode(value))) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/problems/NonStandardStatus.kt b/core/src/main/kotlin/io/outfoxx/sunday/problems/NonStandardStatus.kt index ad191707..beac1232 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/problems/NonStandardStatus.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/problems/NonStandardStatus.kt @@ -31,5 +31,6 @@ data class NonStandardStatus( this(response.statusCode, response.reasonPhrase) override fun getStatusCode() = statusCode + override fun getReasonPhrase() = reasonPhrase } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/utils/CharsetsExt.kt b/core/src/main/kotlin/io/outfoxx/sunday/utils/CharsetsExt.kt index 5bc70227..b0c59717 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/utils/CharsetsExt.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/utils/CharsetsExt.kt @@ -27,8 +27,10 @@ import java.nio.charset.Charset * @param default If [mediaType] doesn't have a `charset` parameter, this value is returned. * @return Charset from the [mediaType] or the [default]. */ -fun Charsets.from(mediaType: MediaType, default: Charset = UTF_8): Charset { - +fun Charsets.from( + mediaType: MediaType, + default: Charset = UTF_8, +): Charset { val encoding = mediaType.parameter(CharSet) ?: return default return Charset.forName(encoding) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/utils/Problems.kt b/core/src/main/kotlin/io/outfoxx/sunday/utils/Problems.kt index 5f8ebb40..b203c773 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/utils/Problems.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/utils/Problems.kt @@ -24,10 +24,12 @@ import org.zalando.problem.ThrowableProblem internal object Problems { - fun forResponse(response: Response): ThrowableProblem = - forStatus(response.statusCode, response.reasonPhrase) + fun forResponse(response: Response): ThrowableProblem = forStatus(response.statusCode, response.reasonPhrase) - fun forStatus(statusCode: Int, reasonPhrase: String?): ThrowableProblem { + fun forStatus( + statusCode: Int, + reasonPhrase: String?, + ): ThrowableProblem { val status = try { Status.valueOf(statusCode) diff --git a/core/src/main/kotlin/io/outfoxx/sunday/utils/Strings.kt b/core/src/main/kotlin/io/outfoxx/sunday/utils/Strings.kt index bfbc667d..8fc601ae 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/utils/Strings.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/utils/Strings.kt @@ -19,5 +19,4 @@ package io.outfoxx.sunday.utils import okio.Buffer import java.nio.charset.Charset -internal fun String.buffer(charset: Charset = Charsets.UTF_8): Buffer = - Buffer().writeString(this, charset) +internal fun String.buffer(charset: Charset = Charsets.UTF_8): Buffer = Buffer().writeString(this, charset) diff --git a/core/src/test/kotlin/EventParserTest.kt b/core/src/test/kotlin/EventParserTest.kt index b8ee11e1..cef30166 100644 --- a/core/src/test/kotlin/EventParserTest.kt +++ b/core/src/test/kotlin/EventParserTest.kt @@ -87,7 +87,7 @@ class EventParserTest { |data: Hello World! | | - """.trimMargin() + """.trimMargin(), ) val events = run(EventParser(), eventBuffer) @@ -99,7 +99,7 @@ class EventParserTest { EventInfo(null, "hello", "12345", "Hello World!"), EventInfo(null, "hello", "67890", "Hello World!"), EventInfo(null, "hello", "abcde", "Hello World!"), - ) + ), ) } @@ -186,7 +186,7 @@ class EventParserTest { | | | - """.trimMargin() + """.trimMargin() val eventBuffers = mutableListOf() while (eventStream.isNotEmpty()) { @@ -211,13 +211,16 @@ class EventParserTest { EventInfo(null, "hello", "3-12345", "Hello World!"), EventInfo(null, "hello", "4-12345", "Hello World!"), EventInfo(null, "hello", "5-12345", "Hello World!"), - ) + ), ) } private fun source(data: String): Buffer = data.buffer() - private fun run(parser: EventParser, source: Buffer): List { + private fun run( + parser: EventParser, + source: Buffer, + ): List { val events = mutableListOf() parser.process(source) { events.add(it) } return events diff --git a/core/src/test/kotlin/HeaderExtensionsTest.kt b/core/src/test/kotlin/HeaderExtensionsTest.kt new file mode 100644 index 00000000..86399e61 --- /dev/null +++ b/core/src/test/kotlin/HeaderExtensionsTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.outfoxx.sunday.MediaType +import io.outfoxx.sunday.http.HeaderParameters +import io.outfoxx.sunday.http.getAll +import io.outfoxx.sunday.http.getFirst +import io.outfoxx.sunday.http.toMultiMap +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.oneOf +import org.junit.jupiter.api.Test + +class HeaderExtensionsTest { + + @Test + fun `test getFirst`() { + val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) + + assertThat( + headers.getFirst("test"), + `is`(oneOf(MediaType.JSON.value, MediaType.CBOR.value)), + ) + } + + @Test + fun `test getAll`() { + val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) + + assertThat( + headers.getAll("test"), + containsInAnyOrder(MediaType.JSON.value, MediaType.CBOR.value), + ) + } + + @Test + fun `test toMultiMap`() { + val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) + + assertThat( + headers.toMultiMap(), + equalTo(mapOf("test" to listOf(MediaType.JSON.value, MediaType.CBOR.value))), + ) + } + +} diff --git a/core/src/test/kotlin/HeaderParametersTest.kt b/core/src/test/kotlin/HeaderParametersTest.kt index b6d9f029..618542fa 100644 --- a/core/src/test/kotlin/HeaderParametersTest.kt +++ b/core/src/test/kotlin/HeaderParametersTest.kt @@ -29,29 +29,26 @@ class HeaderParametersTest { @Test fun `test encodes array values as repeated headers`() { - val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) assertThat( headers, - containsInAnyOrder("test" to "application/json", "test" to "application/cbor") + containsInAnyOrder("test" to "application/json", "test" to "application/cbor"), ) } @Test fun `test encodes iterables as repeated headers`() { - val headers = HeaderParameters.encode(mapOf("test" to listOf(MediaType.JSON, MediaType.CBOR))) assertThat( headers, - containsInAnyOrder("test" to "application/json", "test" to "application/cbor") + containsInAnyOrder("test" to "application/json", "test" to "application/cbor"), ) } @Test fun `test string encoding`() { - val headers = HeaderParameters.encode(mapOf("test" to "header")) assertThat(headers, containsInAnyOrder("test" to "header")) @@ -59,7 +56,6 @@ class HeaderParametersTest { @Test fun `test integer encoding`() { - val headers = HeaderParameters.encode(mapOf("test" to 123456789)) assertThat(headers, containsInAnyOrder("test" to "123456789")) @@ -67,7 +63,6 @@ class HeaderParametersTest { @Test fun `test decimal encoding`() { - val headers = HeaderParameters.encode(mapOf("test" to 12345.6789)) assertThat(headers, containsInAnyOrder("test" to "12345.6789")) @@ -75,7 +70,6 @@ class HeaderParametersTest { @Test fun `test null values are ignored`() { - val headers = HeaderParameters.encode(mapOf("test" to null)) assertThat(headers, emptyIterable()) @@ -83,20 +77,19 @@ class HeaderParametersTest { @Test fun `test nested null values are ignored`() { - - val headers = HeaderParameters.encode( - mapOf( - "test1" to listOf(null), - "test2" to arrayOf(null), + val headers = + HeaderParameters.encode( + mapOf( + "test1" to listOf(null), + "test2" to arrayOf(null), + ), ) - ) assertThat(headers, emptyIterable()) } @Test fun `test fails on invalid header values`() { - val nullError = assertThrows { HeaderParameters.encode(mapOf("test" to "a${0.toChar()}b")) diff --git a/core/src/test/kotlin/MediaTypeCodecTest.kt b/core/src/test/kotlin/MediaTypeCodecTest.kt index c5429d4d..c41744f9 100644 --- a/core/src/test/kotlin/MediaTypeCodecTest.kt +++ b/core/src/test/kotlin/MediaTypeCodecTest.kt @@ -14,8 +14,13 @@ * limitations under the License. */ +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper +import io.outfoxx.sunday.MediaType import io.outfoxx.sunday.mediatypes.codecs.BinaryDecoder import io.outfoxx.sunday.mediatypes.codecs.BinaryEncoder +import io.outfoxx.sunday.mediatypes.codecs.MediaTypeDecoders +import io.outfoxx.sunday.mediatypes.codecs.MediaTypeEncoders import io.outfoxx.sunday.mediatypes.codecs.TextDecoder import io.outfoxx.sunday.mediatypes.codecs.TextEncoder import io.outfoxx.sunday.mediatypes.codecs.decode @@ -27,6 +32,9 @@ import okio.Source import okio.buffer import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.nullValue import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -43,7 +51,6 @@ class MediaTypeCodecTest { @Test fun `test decoder decodes text`() { - val decoder = TextDecoder.default assertThat(decoder.decode("testing".buffer()), equalTo("testing")) @@ -52,7 +59,6 @@ class MediaTypeCodecTest { @Test fun `test decoder fails to decode non text`() { - val decoder = TextDecoder.default assertThrows { @@ -62,7 +68,6 @@ class MediaTypeCodecTest { @Test fun `test encoder encodes text`() { - val encoder = TextEncoder.default assertThat(encoder.encode("testing"), equalTo("testing".buffer())) @@ -71,7 +76,6 @@ class MediaTypeCodecTest { @Test fun `test encoder fails to encode non text values`() { - val encoder = TextEncoder.default assertThrows { @@ -86,7 +90,6 @@ class MediaTypeCodecTest { @Test fun `test decoder decodes binary values`() { - val decoder = BinaryDecoder() val buffer = "testing".buffer() @@ -94,21 +97,20 @@ class MediaTypeCodecTest { assertThat(decoder.decode(buffer.copy()), equalTo(buffer.copy().readByteString())) assertThat( decoder.decode(buffer.copy()).readAllBytes(), - equalTo(buffer.copy().readByteArray()) + equalTo(buffer.copy().readByteArray()), ) assertThat( decoder.decode(buffer.copy()).buffer().readByteArray(), - equalTo(buffer.copy().readByteArray()) + equalTo(buffer.copy().readByteArray()), ) assertThat( decoder.decode(buffer.copy()).readByteArray(), - equalTo(buffer.copy().readByteArray()) + equalTo(buffer.copy().readByteArray()), ) } @Test fun `test decoder fails to decode non binary`() { - val decoder = BinaryDecoder() assertThrows { @@ -118,30 +120,28 @@ class MediaTypeCodecTest { @Test fun `test encoder encodes binary values`() { - val encoder = BinaryEncoder() assertThat( encoder.encode(byteArrayOf(1, 2, 3)), - equalTo(Buffer().write(byteArrayOf(1, 2, 3))) + equalTo(Buffer().write(byteArrayOf(1, 2, 3))), ) assertThat( encoder.encode(ByteString.of(1, 2, 3)), - equalTo(Buffer().write(byteArrayOf(1, 2, 3))) + equalTo(Buffer().write(byteArrayOf(1, 2, 3))), ) assertThat( encoder.encode(ByteArrayInputStream(byteArrayOf(1, 2, 3))), - equalTo(Buffer().write(byteArrayOf(1, 2, 3))) + equalTo(Buffer().write(byteArrayOf(1, 2, 3))), ) assertThat( encoder.encode(Buffer().write(byteArrayOf(1, 2, 3))), - equalTo(Buffer().write(byteArrayOf(1, 2, 3))) + equalTo(Buffer().write(byteArrayOf(1, 2, 3))), ) } @Test fun `test encoder fails to encode non binary values`() { - val encoder = BinaryEncoder() assertThrows { @@ -150,4 +150,29 @@ class MediaTypeCodecTest { } } + @Test + fun `test encoders builder registers specific codecs`() { + val encoders = + MediaTypeEncoders + .Builder() + .registerJSON(JsonMapper()) + .registerCBOR(CBORMapper()) + .build() + + assertThat(encoders.find(MediaType.JSON), `is`(not(nullValue()))) + assertThat(encoders.find(MediaType.CBOR), `is`(not(nullValue()))) + } + + @Test + fun `test decoders builder registers specific codecs`() { + val decoders = + MediaTypeDecoders + .Builder() + .registerJSON(JsonMapper()) + .registerCBOR(CBORMapper()) + .build() + + assertThat(decoders.find(MediaType.JSON), `is`(not(nullValue()))) + assertThat(decoders.find(MediaType.CBOR), `is`(not(nullValue()))) + } } diff --git a/core/src/test/kotlin/MediaTypeTest.kt b/core/src/test/kotlin/MediaTypeTest.kt index 1c1afc9d..df01f776 100644 --- a/core/src/test/kotlin/MediaTypeTest.kt +++ b/core/src/test/kotlin/MediaTypeTest.kt @@ -54,20 +54,19 @@ class MediaTypeTest { @Test fun `extracts from headers`() { - - val mediaTypes = MediaType.from( - listOf( - "${MediaType.JSON.value} , ${MediaType.CBOR.value}", - MediaType.HTML.value + val mediaTypes = + MediaType.from( + listOf( + "${MediaType.JSON.value} , ${MediaType.CBOR.value}", + MediaType.HTML.value, + ), ) - ) assertThat(mediaTypes, containsInAnyOrder(MediaType.JSON, MediaType.CBOR, MediaType.HTML)) } @Test fun `test equality`() { - val mediaType = MediaType.HTML.with(CharSet, "utf-8") assertEquals(mediaType, mediaType) @@ -104,6 +103,7 @@ class MediaTypeTest { } @Test + @Suppress("LongMethod") fun `test compatibility`() { assertTrue( MediaType(Text, Vendor, "plain", JSON, mapOf("a" to "b")).compatible( @@ -112,9 +112,9 @@ class MediaTypeTest { Vendor, "plain", JSON, - mapOf("a" to "b") - ) - ) + mapOf("a" to "b"), + ), + ), ) { "Test compatibility" } assertFalse( @@ -124,9 +124,9 @@ class MediaTypeTest { Vendor, "plain", JSON, - mapOf("a" to "b") - ) - ) + mapOf("a" to "b"), + ), + ), ) { "Test incompatibility in types" } assertFalse( @@ -136,9 +136,9 @@ class MediaTypeTest { Personal, "plain", JSON, - mapOf("a" to "b") - ) - ) + mapOf("a" to "b"), + ), + ), ) { "Test incompatibility in trees" } assertFalse( @@ -148,9 +148,9 @@ class MediaTypeTest { Vendor, "html", JSON, - mapOf("a" to "b") - ) - ) + mapOf("a" to "b"), + ), + ), ) { "Test incompatibility in subtypes" } assertFalse( @@ -160,9 +160,9 @@ class MediaTypeTest { Vendor, "plain", XML, - mapOf("a" to "b") - ) - ) + mapOf("a" to "b"), + ), + ), ) { "Test incompatibility in suffixes" } assertFalse( @@ -172,9 +172,9 @@ class MediaTypeTest { Vendor, "plain", JSON, - mapOf("a" to "c") - ) - ) + mapOf("a" to "c"), + ), + ), ) { "Test incompatibility in parameter values" } assertFalse( @@ -183,9 +183,9 @@ class MediaTypeTest { Text, Vendor, "plain", - parameters = mapOf("a" to "c") - ) - ) + parameters = mapOf("a" to "c"), + ), + ), ) { "Test incompatibility in parameter values missing suffix" } assertTrue( @@ -193,11 +193,12 @@ class MediaTypeTest { MediaType( Text, subtype = "html", - parameters = mapOf( - "charset" to "utf-8" - ) - ) - ) + parameters = + mapOf( + "charset" to "utf-8", + ), + ), + ), ) { "Test compatibility with different parameters" } assertTrue( @@ -205,11 +206,12 @@ class MediaTypeTest { MediaType( Text, subtype = "html", - parameters = mapOf( - "CHARSET" to "UTF-8" - ) - ) - ) + parameters = + mapOf( + "CHARSET" to "UTF-8", + ), + ), + ), ) { "Test compatibility with different parameter cases" } assertTrue( @@ -217,11 +219,12 @@ class MediaTypeTest { MediaType( Text, subtype = "html", - parameters = mapOf( - "test" to "it" - ) - ) - ) + parameters = + mapOf( + "test" to "it", + ), + ), + ), ) { "Test compatibility with different parameters" } assertFalse( @@ -229,59 +232,61 @@ class MediaTypeTest { MediaType( Text, subtype = "html", - parameters = mapOf( - "charset" to "utf-16" - ) - ) - ) + parameters = + mapOf( + "charset" to "utf-16", + ), + ), + ), ) { "Test compatibility with different parameter values" } assertTrue( MediaType(Text, subtype = "html").compatible( MediaType( Any, - subtype = "*" - ) - ) + subtype = "*", + ), + ), ) { "Test compatibility with wildcard type & subtype" } assertTrue( MediaType(Text, subtype = "html").compatible( MediaType( Any, - subtype = "html" - ) - ) + subtype = "html", + ), + ), ) { "Test compatibility with wildcard type" } assertTrue( MediaType(Text, subtype = "html").compatible( MediaType( Text, - subtype = "*" - ) - ) + subtype = "*", + ), + ), ) { "Test compatibility with wildcard subtype" } } @Test + @Suppress("LongMethod") fun `test parse`() { assertThat( "Test parsing", MediaType(Application, Standard, "problem", JSON, mapOf("charset" to "utf-8")), - equalTo(MediaType.from("application/problem+json;charset=utf-8")) + equalTo(MediaType.from("application/problem+json;charset=utf-8")), ) assertThat( "Test parsing with non-standard tree", MediaType(Application, Obsolete, "www-form-urlencoded"), - equalTo(MediaType.from("application/x-www-form-urlencoded")) + equalTo(MediaType.from("application/x-www-form-urlencoded")), ) assertThat( "Test parsing with non-standard tree and complexs subtype", MediaType(Application, Obsolete, "x509-ca-cert"), - equalTo(MediaType.from("application/x-x509-ca-cert")) + equalTo(MediaType.from("application/x-x509-ca-cert")), ) assertThat( @@ -290,9 +295,9 @@ class MediaTypeTest { Application, Vendor, "yaml", - parameters = mapOf("charset" to "utf-8", "something" to "else") + parameters = mapOf("charset" to "utf-8", "something" to "else"), ), - equalTo(MediaType.from("application/vnd.yaml;charset=utf-8;something=else")) + equalTo(MediaType.from("application/vnd.yaml;charset=utf-8;something=else")), ) assertThat( @@ -301,9 +306,9 @@ class MediaTypeTest { Application, Vendor, "yaml", - parameters = mapOf("charset" to "utf-8", "something" to "else") + parameters = mapOf("charset" to "utf-8", "something" to "else"), ), - equalTo(MediaType.from("APPLICATION/VND.YAML;CHARSET=UTF-8;SOMETHING=ELSE")) + equalTo(MediaType.from("APPLICATION/VND.YAML;CHARSET=UTF-8;SOMETHING=ELSE")), ) assertThat( @@ -312,9 +317,9 @@ class MediaTypeTest { Application, Vendor, "yaml", - parameters = mapOf("charset" to "utf-8", "something" to "else") + parameters = mapOf("charset" to "utf-8", "something" to "else"), ), - equalTo(MediaType.from("APPLICATION/VND.YAML ; CHARSET=UTF-8 ; SOMETHING=ELSE ")) + equalTo(MediaType.from("APPLICATION/VND.YAML ; CHARSET=UTF-8 ; SOMETHING=ELSE ")), ) assertThat(MediaType.from("application/*").type, equalTo(Application)) @@ -354,13 +359,12 @@ class MediaTypeTest { "yaml", parameters = mapOf("charset" to "utf-8", "something" to "else"), ).value, - "application/vnd.yaml;charset=utf-8;something=else" + "application/vnd.yaml;charset=utf-8;something=else", ) } @Test fun `test parameter access`() { - val mediaType = MediaType.HTML.with(CharSet, "utf-8").with("test", "123") @@ -371,20 +375,19 @@ class MediaTypeTest { @Test fun `test parameter override`() { - assertEquals( MediaType.HTML .with("test", "123") .with("test", "456") .parameter("test"), - "456" + "456", ) assertEquals( MediaType.HTML .with("test", "456") .with("test", "123") .parameter("test"), - "123" + "123", ) } @@ -404,4 +407,11 @@ class MediaTypeTest { assertFalse(htmlWithCharset.compatible(MediaType.JSONStructured)) assertTrue(htmlWithCharset.compatible(MediaType.Any)) } + + @Test + fun `test constructor`() { + val mediaType = MediaType(Application, Vendor, "test", Zip, "charset" to "utf-8") + + assertEquals(mediaType.value, "application/vnd.test+zip;charset=utf-8") + } } diff --git a/core/src/test/kotlin/PatchingTest.kt b/core/src/test/kotlin/PatchingTest.kt index 19e46ac7..e8da73c8 100644 --- a/core/src/test/kotlin/PatchingTest.kt +++ b/core/src/test/kotlin/PatchingTest.kt @@ -74,27 +74,28 @@ class PatchingTest { companion object { - private val mapper = jacksonObjectMapper() - .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + private val mapper = + jacksonObjectMapper() + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) } @Test fun simple() { - val patch = DeviceUpdate.patch { name = set("test") - security = Security.merge { - type = set(17) - enc = set(Base64.getUrlEncoder().encodeToString(byteArrayOf(1, 2, 3))) - sig = delete() - } + security = + Security.merge { + type = set(17) + enc = set(Base64.getUrlEncoder().encodeToString(byteArrayOf(1, 2, 3))) + sig = delete() + } } val json = mapper.writeValueAsString(patch) assertThat( json, - equalTo("""{"name":"test","security":{"type":17,"enc":"AQID","sig":null}}""") + equalTo("""{"name":"test","security":{"type":17,"enc":"AQID","sig":null}}"""), ) val encodedJSON = mapper.writeValueAsString(patch) diff --git a/core/src/test/kotlin/PathEncodersTest.kt b/core/src/test/kotlin/PathEncodersTest.kt index 0568a8b0..de8fd2ff 100644 --- a/core/src/test/kotlin/PathEncodersTest.kt +++ b/core/src/test/kotlin/PathEncodersTest.kt @@ -25,14 +25,12 @@ class PathEncodersTest { @Test fun `adding implicitly typed encoders`() { - val encoders = PathEncoders.default.add(UUID::toString) assertThat(encoders, Matchers.aMapWithSize(2)) } @Test fun `adding explicitly typed encoders`() { - val encoders = PathEncoders.default.add(UUID::class, UUID::toString) assertThat(encoders, Matchers.aMapWithSize(2)) } diff --git a/core/src/test/kotlin/ProblemsTest.kt b/core/src/test/kotlin/ProblemsTest.kt new file mode 100644 index 00000000..3c4b4ac3 --- /dev/null +++ b/core/src/test/kotlin/ProblemsTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.outfoxx.sunday.http.Headers +import io.outfoxx.sunday.http.Request +import io.outfoxx.sunday.http.Response +import io.outfoxx.sunday.utils.Problems +import okio.BufferedSource +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.zalando.problem.Status + +class ProblemsTest { + + @Test + fun `test forResponse`() { + val problem = Problems.forResponse(TestResponse(400, "Bad Request", listOf(), null)) + + assertThat(problem.status, equalTo(Status.BAD_REQUEST)) + } + + @Test + fun `test forStatus`() { + val problem = Problems.forStatus(400, "Bad Request") + + assertThat(problem.status, equalTo(Status.BAD_REQUEST)) + } + + @Test + fun `test forStatus supports non-standard values`() { + val problem = Problems.forStatus(195, "AI Thinking") + + assertThat(problem.status?.statusCode, equalTo(195)) + assertThat(problem.status?.reasonPhrase, equalTo("AI Thinking")) + } + + data class TestResponse( + override val statusCode: Int, + override val reasonPhrase: String?, + override val headers: Headers, + override val body: BufferedSource?, + ) : Response { + + override val trailers: Headers? + get() = null + override val request: Request + get() = TODO("Not yet implemented") + } + +} diff --git a/core/src/test/kotlin/URITemplateTest.kt b/core/src/test/kotlin/URITemplateTest.kt index 46a5261b..ca0a2eb5 100644 --- a/core/src/test/kotlin/URITemplateTest.kt +++ b/core/src/test/kotlin/URITemplateTest.kt @@ -28,12 +28,22 @@ class URITemplateTest { enum class TestEnum { @JsonProperty("test-value") - TestValue + TestValue, } @Test - fun `test enum encoding`() { + fun `test encoding`() { + val path = + URITemplate("http://example.com/{enum}") + .resolve(parameters = mapOf("enum" to TestEnum.TestValue)) + .toURI() + .toString() + + assertThat(path, equalTo("http://example.com/test-value")) + } + @Test + fun `test enum encoding`() { val path = URITemplate("http://example.com/{enum}", mapOf("enum" to TestEnum.TestValue)) .resolve(encoders = PathEncoders.default) @@ -45,10 +55,9 @@ class URITemplateTest { @Test fun `test custom encoding`() { - val encoders: Map, PathEncoder> = mapOf( - UUID::class to { (it as UUID).toString().replace("-", "") } + UUID::class to { (it as UUID).toString().replace("-", "") }, ) val id = UUID.randomUUID() @@ -60,7 +69,7 @@ class URITemplateTest { assertThat( path, - equalTo("http://example.com/objects/${id.toString().replace("-", "")}") + equalTo("http://example.com/objects/${id.toString().replace("-", "")}"), ) } diff --git a/core/src/test/kotlin/WWWFormURLEncoderTest.kt b/core/src/test/kotlin/WWWFormURLEncoderTest.kt index c40d26c8..ad53a5b4 100644 --- a/core/src/test/kotlin/WWWFormURLEncoderTest.kt +++ b/core/src/test/kotlin/WWWFormURLEncoderTest.kt @@ -26,29 +26,31 @@ class WWWFormURLEncoderTest { @Test fun `percent encodes keys`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test/data" to listOf(1, 2, 3))), - equalTo("test%2Fdata=1&test%2Fdata=2&test%2Fdata=3") + equalTo("test%2Fdata=1&test%2Fdata=2&test%2Fdata=3"), ) } @Test fun `percent encodes values`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf("1/1", "1/2", "1/3", " !'()~"))), - equalTo("test=1%2F1&test=1%2F2&test=1%2F3&test=%20!'()~") + equalTo("test=1%2F1&test=1%2F2&test=1%2F3&test=%20!'()~"), ) } @@ -58,119 +60,127 @@ class WWWFormURLEncoderTest { assertThat( encoder.encodeQueryString(mapOf("test" to mapOf("a" to 1, "b" to 2), "c" to "3")), - equalTo("c=3&test%5Ba%5D=1&test%5Bb%5D=2") + equalTo("c=3&test%5Ba%5D=1&test%5Bb%5D=2"), ) } @Test fun `encodes list values in bracketed form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Bracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Bracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(1, 2, 3))), - equalTo("test%5B%5D=1&test%5B%5D=2&test%5B%5D=3") + equalTo("test%5B%5D=1&test%5B%5D=2&test%5B%5D=3"), ) } @Test fun `encodes list values in unbracketed form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(1, 2, 3))), - equalTo("test=1&test=2&test=3") + equalTo("test=1&test=2&test=3"), ) } @Test fun `encodes set values in bracketed form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Bracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Bracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to setOf(1, 2, 3))), - equalTo("test%5B%5D=1&test%5B%5D=2&test%5B%5D=3") + equalTo("test%5B%5D=1&test%5B%5D=2&test%5B%5D=3"), ) } @Test fun `encodes set values in unbracketed form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to setOf(1, 2, 3))), - equalTo("test=1&test=2&test=3") + equalTo("test=1&test=2&test=3"), ) } @Test fun `encodes array values in bracketed form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Bracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Bracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to arrayOf(1, 2, 3))), - equalTo("test%5B%5D=1&test%5B%5D=2&test%5B%5D=3") + equalTo("test%5B%5D=1&test%5B%5D=2&test%5B%5D=3"), ) } @Test fun `encodes array values in unbracketed form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to arrayOf(1, 2, 3))), - equalTo("test=1&test=2&test=3") + equalTo("test=1&test=2&test=3"), ) } @Test fun `encodes bool values in numeric form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(true, false))), - equalTo("test=1&test=0") + equalTo("test=1&test=0"), ) } @Test fun `encodes bool values in literal form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Literal, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Literal, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(true, false))), - equalTo("test=true&test=false") + equalTo("test=true&test=false"), ) } @@ -180,43 +190,46 @@ class WWWFormURLEncoderTest { @Test fun `encodes date values in ISO form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.ISO8601 - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.ISO8601, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(date1, date2))), - equalTo("test=2017-05-15T08%3A30%3A00.123456789Z&test=2018-06-16T02%3A40%3A10.123456789Z") + equalTo("test=2017-05-15T08%3A30%3A00.123456789Z&test=2018-06-16T02%3A40%3A10.123456789Z"), ) } @Test fun `encodes date values in seconds since epoch form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.FractionalSecondsSinceEpoch - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.FractionalSecondsSinceEpoch, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(date1, date2))), - equalTo("test=1494837000.1234567&test=1529116810.1234567") + equalTo("test=1494837000.1234567&test=1529116810.1234567"), ) } @Test fun `encodes date values in milliseconds since epoch form`() { - val encoder = WWWFormURLEncoder( - WWWFormURLEncoder.ArrayEncoding.Unbracketed, - WWWFormURLEncoder.BoolEncoding.Numeric, - WWWFormURLEncoder.DateEncoding.MillisecondsSinceEpoch - ) + val encoder = + WWWFormURLEncoder( + WWWFormURLEncoder.ArrayEncoding.Unbracketed, + WWWFormURLEncoder.BoolEncoding.Numeric, + WWWFormURLEncoder.DateEncoding.MillisecondsSinceEpoch, + ) assertThat( encoder.encodeQueryString(mapOf("test" to listOf(date1, date2))), - equalTo("test=1494837000123&test=1529116810123") + equalTo("test=1494837000123&test=1529116810123"), ) } @@ -226,7 +239,7 @@ class WWWFormURLEncoderTest { assertThat( encoder.encodeQueryString(mapOf("flagged" to null)), - equalTo("flagged") + equalTo("flagged"), ) } diff --git a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/EventSourceTest.kt b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/EventSourceTest.kt index a3f1580f..b9685ae6 100644 --- a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/EventSourceTest.kt +++ b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/EventSourceTest.kt @@ -50,7 +50,7 @@ abstract class EventSourceTest { url: String, headers: Headers, onStart: () -> Unit = {}, - onCancel: () -> Unit = {} + onCancel: () -> Unit = {}, ): Request @Test @@ -66,13 +66,11 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) - .setBodyDelay(500, MILLISECONDS) + 5, + ).setBodyDelay(500, MILLISECONDS), ) server.start() server.use { - val eventSource = EventSource({ headers -> createRequest(server.url("/test").toString(), headers) }) @@ -87,7 +85,6 @@ abstract class EventSourceTest { } eventSource.use { - eventSource.connect() eventSource.connect() @@ -111,12 +108,11 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.start() server.use { - val eventSource = EventSource({ headers -> createRequest(server.url("/test").toString(), headers) }) @@ -128,7 +124,6 @@ abstract class EventSourceTest { } eventSource.use { - eventSource.connect() assertTrue(completed.await(300, SECONDS)) @@ -157,12 +152,11 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.start() server.use { - val eventSource = EventSource({ headers -> createRequest(server.url("/test").toString(), headers) }) @@ -174,7 +168,6 @@ abstract class EventSourceTest { } eventSource.use { - eventSource.connect() assertTrue(completed.await(3, SECONDS)) @@ -208,20 +201,19 @@ abstract class EventSourceTest { | | """.trimMargin(), - ) + ), ) server.enqueue( MockResponse() .setResponseCode(400) - .setHeader(ContentType, Problem) + .setHeader(ContentType, Problem), ) server.start() server.use { - val eventSource = EventSource( { headers -> createRequest(server.url("/test").toString(), headers) }, - retryTime = Duration.ofMillis(100) + retryTime = Duration.ofMillis(100), ) val opened = CountDownLatch(1) @@ -244,7 +236,6 @@ abstract class EventSourceTest { assertThat(eventSource.onError, not(nullValue())) eventSource.use { - eventSource.connect() assertTrue(opened.await(3, SECONDS), "Did not received open callback") @@ -256,7 +247,6 @@ abstract class EventSourceTest { @Test fun `test listener add & remove`() { - val eventSource = EventSource({ headers -> createRequest("http://example.com", headers) }) val handler: (EventSource.Event) -> Unit = { } @@ -280,12 +270,11 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.start() server.use { - val eventSource = EventSource({ headers -> createRequest(server.url("/test").toString(), headers) }) @@ -296,7 +285,6 @@ abstract class EventSourceTest { } eventSource.use { - eventSource.connect() assertTrue(completed.await(3, SECONDS)) @@ -319,12 +307,11 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.start() server.use { - val eventSource = EventSource({ headers -> createRequest(server.url("/test").toString(), headers) }) @@ -335,7 +322,6 @@ abstract class EventSourceTest { } eventSource.use { - eventSource.connect() assertTrue(completed.await(3, SECONDS)) @@ -358,21 +344,19 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.enqueue( MockResponse() - .setResponseCode(503) + .setResponseCode(503), ) server.start() server.use { - val eventSource = EventSource({ headers -> createRequest(server.url("/test").toString(), headers) }) eventSource.use { - eventSource.connect() server.takeRequest(3, SECONDS) @@ -398,8 +382,8 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.enqueue( MockResponse() @@ -412,24 +396,22 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) + 5, + ), ) server.enqueue( MockResponse() - .setResponseCode(503) + .setResponseCode(503), ) server.start() server.use { - val eventSource = EventSource( { headers -> createRequest(server.url("/test").toString(), headers) }, - retryTime = Duration.ofMillis(100) + retryTime = Duration.ofMillis(100), ) eventSource.use { - eventSource.connect() server.takeRequest(3, SECONDS) @@ -458,18 +440,16 @@ abstract class EventSourceTest { | | """.trimMargin(), - 5 - ) - .throttleBody(20, 2, SECONDS) + 5, + ).throttleBody(20, 2, SECONDS), ) server.start() server.use { - val eventSource = EventSource( { headers -> createRequest(server.url("/test").toString(), headers) }, eventTimeout = Duration.ofMillis(500), - eventTimeoutCheckInterval = Duration.ofMillis(100) + eventTimeoutCheckInterval = Duration.ofMillis(100), ) val completed = CountDownLatch(1) @@ -515,12 +495,11 @@ abstract class EventSourceTest { | | """.trimMargin(), - 3 - ) + 3, + ), ) server.start() server.use { - val connected = CountDownLatch(1) val eventSource = diff --git a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/GeneratedAPITests.kt b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/GeneratedAPITests.kt index 5bea4081..d5631b4c 100644 --- a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/GeneratedAPITests.kt +++ b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/GeneratedAPITests.kt @@ -49,9 +49,14 @@ abstract class GeneratedAPITests { decoders: MediaTypeDecoders = MediaTypeDecoders.default, ): RequestFactory - class API(private val requestFactory: RequestFactory) { + class API( + private val requestFactory: RequestFactory, + ) { - data class TestResult(val message: String, val count: Int) + data class TestResult( + val message: String, + val count: Int, + ) suspend fun testResult(): TestResult = requestFactory.result( @@ -77,7 +82,6 @@ abstract class GeneratedAPITests { @Test fun `generated style API result method`() { - val testResult = API.TestResult("Test", 10) val server = MockWebServer() @@ -85,11 +89,10 @@ abstract class GeneratedAPITests { MockResponse() .setResponseCode(200) .addHeader(ContentType, JSON) - .setBody(objectMapper.writeValueAsString(testResult)) + .setBody(objectMapper.writeValueAsString(testResult)), ) server.start() server.use { - val api = API(createRequestFactory(URITemplate(server.url("/").toString()))) val result = runBlocking { api.testResult() } @@ -100,7 +103,6 @@ abstract class GeneratedAPITests { @Test fun `generated style API result response method`() { - val testResult = API.TestResult("Test", 10) val server = MockWebServer() @@ -108,11 +110,10 @@ abstract class GeneratedAPITests { MockResponse() .setResponseCode(200) .addHeader(ContentType, JSON) - .setBody(objectMapper.writeValueAsString(testResult)) + .setBody(objectMapper.writeValueAsString(testResult)), ) server.start() server.use { - val api = API(createRequestFactory(URITemplate(server.url("/").toString()))) val resultResponse = runBlocking { api.testResultResponse() } @@ -120,23 +121,21 @@ abstract class GeneratedAPITests { assertThat(resultResponse.result, equalTo(testResult)) assertThat( resultResponse.headers.map { it.first.lowercase() to it.second.lowercase() }, - hasItems(ContentType to JSON.value, ContentLength to "29") + hasItems(ContentType to JSON.value, ContentLength to "29"), ) } } @Test fun `generated style API unit result response method`() { - val server = MockWebServer() server.enqueue( MockResponse() .addHeader(ContentLength, "0") - .setResponseCode(204) + .setResponseCode(204), ) server.start() server.use { - val api = API(createRequestFactory(URITemplate(server.url("/").toString()))) val responseResult = runBlocking { api.testVoidResultResponse() } @@ -144,7 +143,7 @@ abstract class GeneratedAPITests { assertThat(responseResult.result, equalTo(Unit)) assertThat( responseResult.headers.map { it.first.lowercase() to it.second.lowercase() }, - hasItems(ContentLength to "0") + hasItems(ContentLength to "0"), ) } } diff --git a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/Implementation.kt b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/Implementation.kt index e3755a2f..63f7c051 100644 --- a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/Implementation.kt +++ b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/Implementation.kt @@ -18,5 +18,5 @@ package io.outfoxx.sunday.test enum class Implementation { JDK, - OkHttp + OkHttp, } diff --git a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt index 7c1e0e2f..2001783d 100644 --- a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt +++ b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt @@ -23,6 +23,7 @@ import io.outfoxx.sunday.MediaType.Companion.CBOR import io.outfoxx.sunday.MediaType.Companion.EventStream import io.outfoxx.sunday.MediaType.Companion.HTML import io.outfoxx.sunday.MediaType.Companion.JSON +import io.outfoxx.sunday.MediaType.Companion.Plain import io.outfoxx.sunday.MediaType.Companion.Problem import io.outfoxx.sunday.MediaType.Companion.WWWFormUrlEncoded import io.outfoxx.sunday.RequestFactory @@ -88,7 +89,6 @@ abstract class RequestFactoryTest { @Test fun `allows overriding defaults constructor`() { - val specialEncoders = MediaTypeEncoders.Builder().build() val specialDecoders = MediaTypeDecoders.Builder().build() @@ -107,7 +107,6 @@ abstract class RequestFactoryTest { @Test fun `encodes path parameters`() { - createRequestFactory(URITemplate("http://example.com/{id}")) .use { requestFactory -> @@ -117,23 +116,18 @@ abstract class RequestFactoryTest { Method.Get, "/encoded-params", pathParameters = mapOf("id" to 123), - body = null, - contentTypes = null, - acceptTypes = null, - headers = null ) } assertThat( request.uri, - equalTo(URI("http://example.com/123/encoded-params")) + equalTo(URI("http://example.com/123/encoded-params")), ) } } @Test fun `encodes query parameters`() { - createRequestFactory(URITemplate("http://example.com")) .use { requestFactory -> @@ -142,52 +136,49 @@ abstract class RequestFactoryTest { requestFactory.request( Method.Get, "/encode-query-params", - pathParameters = null, queryParameters = mapOf("limit" to 5, "search" to "1 & 2"), - body = null, - contentTypes = null, - acceptTypes = null, - headers = null ) } assertThat( request.uri, - equalTo(URI("http://example.com/encode-query-params?limit=5&search=1%20%26%202")) + equalTo(URI("http://example.com/encode-query-params?limit=5&search=1%20%26%202")), ) } } @Test fun `fails when no query parameter encoder is registered and query params are provided`() { - createRequestFactory( URITemplate("http://example.com"), - encoders = MediaTypeEncoders.Builder().registerData().registerJSON().build() - ) - .use { requestFactory -> + encoders = + MediaTypeEncoders + .Builder() + .registerData() + .registerJSON() + .build(), + ).use { requestFactory -> - val error = - assertThrows { - runBlocking { - requestFactory.request( - Method.Get, - "/encode-query-params", - queryParameters = mapOf("limit" to 5, "search" to "1 & 2") - ) - } + val error = + assertThrows { + runBlocking { + requestFactory.request( + Method.Get, + "/encode-query-params", + queryParameters = mapOf("limit" to 5, "search" to "1 & 2"), + ) } + } - assertThat(error.reason, equalTo(SundayError.Reason.NoDecoder)) - } + assertThat(error.reason, equalTo(SundayError.Reason.NoDecoder)) + } } @Test fun `fails url query parameter encoder is not a URLQueryParamsEncoder`() { - createRequestFactory( URITemplate("http://example.com"), - encoders = MediaTypeEncoders.Builder().register(BinaryEncoder(), WWWFormUrlEncoded).build() + encoders = MediaTypeEncoders.Builder().register(BinaryEncoder(), WWWFormUrlEncoded).build(), ).use { requestFactory -> val error = @@ -196,7 +187,7 @@ abstract class RequestFactoryTest { requestFactory.request( Method.Get, "/encode-query-params", - queryParameters = mapOf("limit" to 5, "search" to "1 & 2") + queryParameters = mapOf("limit" to 5, "search" to "1 & 2"), ) } } @@ -208,7 +199,6 @@ abstract class RequestFactoryTest { @Test fun `adds custom headers`() { - createRequestFactory(URITemplate("http://example.com")) .use { requestFactory -> @@ -217,7 +207,7 @@ abstract class RequestFactoryTest { requestFactory.request( Method.Get, "/add-custom-headers", - headers = mapOf(HeaderNames.Authorization to "Bearer 12345") + headers = mapOf(HeaderNames.Authorization to "Bearer 12345"), ) } @@ -227,7 +217,6 @@ abstract class RequestFactoryTest { @Test fun `adds accept headers`() { - createRequestFactory(URITemplate("http://example.com")) .use { requestFactory -> @@ -236,44 +225,41 @@ abstract class RequestFactoryTest { requestFactory.request( Method.Get, "/add-accept-headers", - acceptTypes = listOf(JSON, CBOR) + acceptTypes = listOf(JSON, CBOR), ) } assertThat( request.headers, - contains(HeaderNames.Accept to "application/json , application/cbor") + contains(HeaderNames.Accept to "application/json , application/cbor"), ) } } @Test fun `fails if none of the accept types has a decoder`() { - createRequestFactory( URITemplate("http://example.com"), - decoders = MediaTypeDecoders.Builder().build() - ) - .use { requestFactory -> + decoders = MediaTypeDecoders.Builder().build(), + ).use { requestFactory -> - val error = - assertThrows { - runBlocking { - requestFactory.request( - Method.Get, - "/add-accept-headers", - acceptTypes = listOf(JSON, CBOR) - ) - } + val error = + assertThrows { + runBlocking { + requestFactory.request( + Method.Get, + "/add-accept-headers", + acceptTypes = listOf(JSON, CBOR), + ) } + } - assertThat(error.reason, equalTo(NoSupportedAcceptTypes)) - } + assertThat(error.reason, equalTo(NoSupportedAcceptTypes)) + } } @Test fun `fails if none of the content types has an encoder for the body`() { - createRequestFactory(URITemplate("http://example.com")) .use { requestFactory -> @@ -284,7 +270,7 @@ abstract class RequestFactoryTest { Method.Post, "/add-accept-headers", body = "a body", - contentTypes = listOf(MediaType.from("application/x-unknown")) + contentTypes = listOf(MediaType.from("application/x-unknown")), ) } } @@ -295,7 +281,6 @@ abstract class RequestFactoryTest { @Test fun `attaches encoded body based on content-type`() { - createRequestFactory(URITemplate("http://example.com")) .use { requestFactory -> @@ -305,7 +290,7 @@ abstract class RequestFactoryTest { Method.Post, "/attach-body", body = mapOf("a" to 5), - contentTypes = listOf(JSON) + contentTypes = listOf(JSON), ) } @@ -316,7 +301,6 @@ abstract class RequestFactoryTest { @Test fun `set content-type when body is non-existent`() { - createRequestFactory(URITemplate("http://example.com")) .use { requestFactory -> @@ -325,7 +309,7 @@ abstract class RequestFactoryTest { requestFactory.request( Method.Post, "/attach-body", - contentTypes = listOf(JSON) + contentTypes = listOf(JSON), ) } @@ -339,10 +323,9 @@ abstract class RequestFactoryTest { @Test fun `fetches typed results`() { - data class Tester( val name: String, - val count: Int + val count: Int, ) val tester = Tester("Test", 10) @@ -352,25 +335,54 @@ abstract class RequestFactoryTest { MockResponse() .setResponseCode(200) .addHeader(ContentType, JSON) - .setBody(objectMapper.writeValueAsString(tester)) + .setBody(objectMapper.writeValueAsString(tester)), ) server.start() server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val result = + runBlocking { + requestFactory.resultResponse( + Method.Get, + "", + ) + } + + assertThat(result.headers, hasItem(ContentType to "application/json")) + assertThat(result.result, equalTo(tester)) + } + } + } + + @Test + fun `fetches typed results with body`() { + data class Tester( + val name: String, + val count: Int, + ) + val tester = Tester("Test", 10) + + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader(ContentType, JSON) + .setBody(objectMapper.writeValueAsString(tester)), + ) + server.start() + server.use { createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> val result = runBlocking { - requestFactory.resultResponse( + requestFactory.resultResponse( Method.Get, "", - pathParameters = null, - queryParameters = null, body = null, - contentTypes = null, - acceptTypes = null, - headers = null ) } @@ -382,15 +394,13 @@ abstract class RequestFactoryTest { @Test fun `executes requests with empty responses`() { - val server = MockWebServer() server.enqueue( MockResponse() - .setResponseCode(204) + .setResponseCode(204), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -405,17 +415,15 @@ abstract class RequestFactoryTest { @Test fun `executes manual requests for responses`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(200) .setHeader(ContentType, JSON) - .setBody("[]") + .setBody("[]"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -430,25 +438,47 @@ abstract class RequestFactoryTest { } @Test - fun `error responses with non standard status codes are handled`() { + fun `executes manual requests with body for responses`() { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .setHeader(ContentType, JSON) + .setBody("[]"), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val response = + runBlocking { + requestFactory.response(Method.Get, "", body = null, contentTypes = listOf(Plain)) + } + assertThat(response.body?.readByteArray(), equalTo("[]".encodeToByteArray())) + } + } + } + + @Test + fun `error responses with non standard status codes are handled`() { val server = MockWebServer() server.enqueue( MockResponse() .setStatus("HTTP/1.1 484 Special Status") .setHeader(ContentType, JSON) - .setBody("[]") + .setBody("[]"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> val problem = assertThrows { runBlocking { - requestFactory.result>(Method.Get, "") + requestFactory.result>(Method.Get, "", body = null) } } @@ -464,15 +494,13 @@ abstract class RequestFactoryTest { @Test fun `fails when no data and non empty result types`() { - val server = MockWebServer() server.enqueue( MockResponse() - .setResponseCode(204) + .setResponseCode(204), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -490,15 +518,38 @@ abstract class RequestFactoryTest { @Test fun `fails when a result is expected and no data is returned in response`() { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + val error = + assertThrows { + runBlocking { + requestFactory.result>(Method.Get, "") + } + } + + assertThat(error.reason, equalTo(SundayError.Reason.NoData)) + } + } + } + + @Test + fun `fails when response content-type is missing`() { val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(200) + .setBody("some stuff"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -509,24 +560,23 @@ abstract class RequestFactoryTest { } } - assertThat(error.reason, equalTo(SundayError.Reason.NoData)) + assertThat(error.reason, equalTo(SundayError.Reason.InvalidContentType)) + assertThat(error.message, containsString("")) } } } @Test fun `fails when response content-type is invalid`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(200) .setHeader(ContentType, "bad/x-unknown") - .setBody("some stuff") + .setBody("some stuff"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -538,23 +588,22 @@ abstract class RequestFactoryTest { } assertThat(error.reason, equalTo(SundayError.Reason.InvalidContentType)) + assertThat(error.message, containsString("bad/x-unknown")) } } } @Test fun `fails when response content-type is unsupported`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(200) .setHeader(ContentType, "application/x-unknown") - .setBody("some data") + .setBody("some data"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -572,17 +621,15 @@ abstract class RequestFactoryTest { @Test fun `test decoding fails when no decoder for content-type`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(200) .addHeader(ContentType, "application/x-unknown-type") - .setBody("Test") + .setBody("Test"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -598,17 +645,15 @@ abstract class RequestFactoryTest { @Test fun `test decoding errors are translated to SundayError`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(200) .addHeader(ContentType, JSON) - .setBody("Test") + .setBody("Test"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -628,7 +673,6 @@ abstract class RequestFactoryTest { @Test fun `test registered problems decode as typed problems`() { - val testProblem = TestProblem("Some Extra", URI.create("id:12345")) val server = MockWebServer() @@ -636,11 +680,10 @@ abstract class RequestFactoryTest { MockResponse() .setResponseCode(TestProblem.STATUS.statusCode) .addHeader(ContentType, Problem) - .setBody(objectMapper.writeValueAsString(testProblem)) + .setBody(objectMapper.writeValueAsString(testProblem)), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> requestFactory.registerProblem(TestProblem.TYPE, TestProblem::class) @@ -663,7 +706,6 @@ abstract class RequestFactoryTest { @Test fun `test unregistered problems decode as generic problems`() { - val testProblem = TestProblem("Some Extra", URI.create("id:12345")) val server = MockWebServer() @@ -671,11 +713,10 @@ abstract class RequestFactoryTest { MockResponse() .setResponseCode(TestProblem.STATUS.statusCode) .addHeader(ContentType, Problem) - .setBody(objectMapper.writeValueAsString(testProblem)) + .setBody(objectMapper.writeValueAsString(testProblem)), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -696,17 +737,15 @@ abstract class RequestFactoryTest { @Test fun `test non problem error responses are translated to predefined problems`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(400) .addHeader(ContentType, HTML) - .setBody("An Error Occurred") + .setBody("An Error Occurred"), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -727,16 +766,14 @@ abstract class RequestFactoryTest { @Test fun `test problem responses with empty bodies are translated to predefined problems`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(400) - .addHeader(ContentType, Problem) + .addHeader(ContentType, Problem), ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -757,58 +794,53 @@ abstract class RequestFactoryTest { @Test fun `test problem responses fail with SundayError when no JSON decoder`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(TestProblem.STATUS.statusCode) .addHeader(ContentType, Problem) - .setBody(objectMapper.writeValueAsString(TestProblem("test"))) + .setBody(objectMapper.writeValueAsString(TestProblem("test"))), ) server.start() server.use { - createRequestFactory( URITemplate(server.url("/").toString()), - decoders = MediaTypeDecoders.Builder().build() - ) - .use { requestFactory -> + decoders = MediaTypeDecoders.Builder().build(), + ).use { requestFactory -> - val error = - assertThrows { - runBlocking { requestFactory.result(Method.Get, "/problem") } - } + val error = + assertThrows { + runBlocking { requestFactory.result(Method.Get, "/problem") } + } - assertThat(error.reason, equalTo(SundayError.Reason.NoDecoder)) - } + assertThat(error.reason, equalTo(SundayError.Reason.NoDecoder)) + } } } @Test fun `test problem responses fail when registered JSON decoder is not a structured decoder`() { - val server = MockWebServer() server.enqueue( MockResponse() .setResponseCode(TestProblem.STATUS.statusCode) .addHeader(ContentType, Problem) - .setBody(objectMapper.writeValueAsString(TestProblem("test"))) + .setBody(objectMapper.writeValueAsString(TestProblem("test"))), ) server.start() server.use { createRequestFactory( URITemplate(server.url("/").toString()), - decoders = MediaTypeDecoders.Builder().register(TextDecoder.default, JSON).build() - ) - .use { requestFactory -> + decoders = MediaTypeDecoders.Builder().register(TextDecoder.default, JSON).build(), + ).use { requestFactory -> - val error = - assertThrows { - runBlocking { requestFactory.result(Method.Get, "/problem") } - } + val error = + assertThrows { + runBlocking { requestFactory.result(Method.Get, "/problem") } + } - assertThat(error.reason, equalTo(SundayError.Reason.NoDecoder)) - } + assertThat(error.reason, equalTo(SundayError.Reason.NoDecoder)) + } } } @@ -819,7 +851,6 @@ abstract class RequestFactoryTest { @Test fun `builds event sources`() { - val encodedEvent = "event: hello\nid: 12345\ndata: Hello World!\n\n" val server = MockWebServer() @@ -831,7 +862,6 @@ abstract class RequestFactoryTest { ) server.start() server.use { - createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> @@ -839,7 +869,40 @@ abstract class RequestFactoryTest { withTimeout(5000) { val eventSource = requestFactory.eventSource(Method.Get, "") eventSource.use { + suspendCancellableCoroutine { continuation -> + eventSource.onMessage = { _ -> + continuation.resume(Unit) + } + eventSource.connect() + } + + } + } + } + } + } + } + + @Test + fun `builds event sources with explicit body`() { + val encodedEvent = "event: hello\nid: 12345\ndata: Hello World!\n\n" + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader(ContentType, EventStream) + .setBody(encodedEvent), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + runBlocking { + withTimeout(5000) { + val eventSource = requestFactory.eventSource(Method.Get, "", body = null) + eventSource.use { suspendCancellableCoroutine { continuation -> eventSource.onMessage = { _ -> continuation.resume(Unit) @@ -856,7 +919,6 @@ abstract class RequestFactoryTest { @Test fun `builds event streams`() { - val encodedEvent = "event: hello\nid: 12345\ndata: {\"target\":\"world\"}\n\n" val server = MockWebServer() @@ -868,26 +930,70 @@ abstract class RequestFactoryTest { ) server.start() server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val result = + runBlocking { + withTimeout(50000) { + val eventStream = + requestFactory.eventStream( + Method.Get, + "", + decoder = { decoder, event, _, data, logger -> + when (event) { + "hello" -> decoder.decode>(data, typeOf>()) + else -> { + logger.error("unsupported event type") + null + } + } + }, + ) + + eventStream.first() + } + } + + assertThat(result, hasEntry("target", "world")) + } + } + } + + @Test + fun `builds event streams with explicit body`() { + val encodedEvent = "event: hello\nid: 12345\ndata: {\"target\":\"world\"}\n\n" + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader(ContentType, EventStream) + .setBody(encodedEvent), + ) + server.start() + server.use { createRequestFactory(URITemplate(server.url("/").toString())) .use { requestFactory -> val result = runBlocking { withTimeout(50000) { - val eventStream = requestFactory.eventStream( - Method.Get, - "", - decoder = { decoder, event, _, data, logger -> - when (event) { - "hello" -> decoder.decode>(data, typeOf>()) - else -> { - logger.error("unsupported event type") - null + val eventStream = + requestFactory.eventStream>( + Method.Get, + "", + body = null, + decoder = { decoder, event, _, data, logger -> + when (event) { + "hello" -> decoder.decode>(data, typeOf>()) + else -> { + logger.error("unsupported event type") + null + } } - } - } - ) + }, + ) eventStream.first() } @@ -900,14 +1006,14 @@ abstract class RequestFactoryTest { class TestProblem( @JsonProperty("extra") val extra: String, - instance: URI? = null + instance: URI? = null, ) : AbstractThrowableProblem( - URI.create(TYPE), - TITLE, - STATUS, - DETAIL, - instance, - ) { + URI.create(TYPE), + TITLE, + STATUS, + DETAIL, + instance, + ) { companion object { diff --git a/gradle.properties b/gradle.properties index 819b3dd9..4c9c913a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,27 +1,28 @@ -org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m +org.gradle.jvmargs=-XX:MaxMetaspaceSize=1g releaseVersion=1.0.0-SNAPSHOT -kotlinVersion=1.7 +kotlinVersion=1.9 javaVersion=11 -kotlinPluginVersion=1.7.21 -dokkaPluginVersion=1.7.20 +kotlinPluginVersion=1.9.20 +koverPluginVersion=0.8.3 +dokkaPluginVersion=1.9.20 licenserPluginVersion=0.6.1 -kotlinterPluginVersion=3.4.4 -detektPluginVersion=1.22.0 -githubReleasePluginVersion=2.4.1 -sonarqubePluginVersion=3.5.0.2730 -nexusPublishPluginVersion=1.1.0 +kotlinterPluginVersion=4.4.1 +detektPluginVersion=1.23.6 +githubReleasePluginVersion=2.5.2 +sonarqubePluginVersion=5.1.0.4882 +nexusPublishPluginVersion=2.0.0 slf4jVersion=2.0.4 -kotlinCoroutinesVersion=1.6.4 +kotlinCoroutinesVersion=1.8.1 uriTemplateVersion=1.3.0 -jacksonVersion=2.12.7 +jacksonVersion=2.17.2 -okioVersion=3.0.0 -okhttpVersion=4.10.0 +okioVersion=3.9.0 +okhttpVersion=4.12.0 zalandoProblemVersion=0.27.1 junitVersion=5.9.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index adb6acbd..4ea536e7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/HttpRequests.kt b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/HttpRequests.kt index 4527551f..1cdbd70c 100644 --- a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/HttpRequests.kt +++ b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/HttpRequests.kt @@ -19,12 +19,12 @@ package io.outfoxx.sunday.jdk import io.outfoxx.sunday.http.Headers import java.net.http.HttpRequest -internal fun HttpRequest.Builder.headers(headers: Headers) = apply { - headers.forEach { header(it.first, it.second) } -} +internal fun HttpRequest.Builder.headers(headers: Headers) = + apply { + headers.forEach { header(it.first, it.second) } + } fun HttpRequest.copyToBuilder(includeHeaders: Boolean = true): HttpRequest.Builder { - val builder = HttpRequest.newBuilder() builder.uri(uri()) builder.expectContinue(expectContinue()) @@ -45,7 +45,7 @@ fun HttpRequest.copyToBuilder(includeHeaders: Boolean = true): HttpRequest.Build "DELETE" -> builder.DELETE() else -> builder.method(method, HttpRequest.BodyPublishers.noBody()) } - } + }, ) return builder diff --git a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequest.kt b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequest.kt index 68cd15b9..2f9d258f 100644 --- a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequest.kt +++ b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequest.kt @@ -84,7 +84,8 @@ open class JdkRequest( val response = suspendCancellableCoroutine { continuation -> - httpClient.sendAsync(request, handler) + httpClient + .sendAsync(request, handler) .whenComplete { response, error -> if (error != null) { continuation.resumeWithException(error) @@ -99,9 +100,8 @@ open class JdkRequest( return JdkResponse(response, httpClient) } - override fun start(): Flow { - return callbackFlow { - + override fun start(): Flow = + callbackFlow { logger.debug("Starting") val handler = RequestEventBodyHandler(JdkRequest(request, httpClient), channel) @@ -115,7 +115,6 @@ open class JdkRequest( future.cancel(true) } } - } class BufferedSourceBodyHandler : BodyHandler { @@ -146,9 +145,7 @@ open class JdkRequest( bodyFuture.complete(buffer) } - override fun getBody(): CompletionStage { - return bodyFuture - } + override fun getBody(): CompletionStage = bodyFuture } @@ -205,19 +202,17 @@ open class JdkRequest( } override fun onComplete() { - val endEvent = Request.Event.End(emptyList()) - channel.trySend(endEvent) + channel + .trySend(endEvent) .onSuccess { logger.trace("Sent: end") } .onFailure { logger.error("Failed to send: end") } bodyFuture.complete(Unit) } - override fun getBody(): CompletionStage { - return bodyFuture - } + override fun getBody(): CompletionStage = bodyFuture } @@ -228,16 +223,16 @@ open class JdkRequest( } override fun apply(responseInfo: HttpResponse.ResponseInfo): BodySubscriber { - val startEvent = Request.Event.Start( JdkResponseInfo( responseInfo, originalRequest, - ) + ), ) - channel.trySend(startEvent) + channel + .trySend(startEvent) .onSuccess { logger.trace("Sent: start") } .onFailure { logger.error("Failed to send: start") } diff --git a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactory.kt b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactory.kt index 6d4c8c6b..d84fcc1a 100644 --- a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactory.kt +++ b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactory.kt @@ -62,18 +62,19 @@ class JdkRequestFactory( override val mediaTypeDecoders: MediaTypeDecoders = MediaTypeDecoders.default, override val pathEncoders: Map, PathEncoder> = PathEncoders.default, private val requestTimeout: Duration = requestTimeoutDefault, - private val eventRequestTimeout: Duration = EventSource.eventTimeoutDefault -) : RequestFactory(), Closeable { + private val eventRequestTimeout: Duration = EventSource.eventTimeoutDefault, +) : RequestFactory(), + Closeable { companion object { fun defaultHttpClient(authenticator: Authenticator? = null): HttpClient = - HttpClient.newBuilder() + HttpClient + .newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) .apply { authenticator?.let { authenticator(authenticator) } - } - .build() + }.build() val requestTimeoutDefault: Duration = Duration.ofSeconds(10) @@ -92,7 +93,10 @@ class JdkRequestFactory( get() = registeredProblemTypesStorage private val registeredProblemTypesStorage = mutableMapOf>() - override fun registerProblem(typeId: String, problemType: KClass) { + override fun registerProblem( + typeId: String, + problemType: KClass, + ) { registeredProblemTypesStorage[typeId] = problemType } @@ -105,34 +109,11 @@ class JdkRequestFactory( contentTypes: List?, acceptTypes: List?, headers: Parameters?, - purpose: RequestPurpose + purpose: RequestPurpose, ): Request { logger.trace("Building request") - var uri = - try { - baseURI.resolve(pathTemplate, pathParameters, pathEncoders).toURI() - } catch (x: Throwable) { - throw SundayError(InvalidBaseUri, cause = x) - } - - if (!queryParameters.isNullOrEmpty()) { - - // Encode & add query parameters to url - - val urlQueryEncoder = mediaTypeEncoders.find(WWWFormUrlEncoded) - ?: throw SundayError(NoDecoder, WWWFormUrlEncoded.value) - - urlQueryEncoder as? URLQueryParamsEncoder - ?: throw SundayError( - NoDecoder, - "'$WWWFormUrlEncoded' encoder must implement ${URLQueryParamsEncoder::class.simpleName}" - ) - - val uriQuery = urlQueryEncoder.encodeQueryString(queryParameters) - - uri = uri.replaceQuery(uriQuery) - } + val uri = uri(pathTemplate, pathParameters, queryParameters) val requestBuilder = HttpRequest.newBuilder(uri) @@ -157,16 +138,18 @@ class JdkRequestFactory( // Add `Content-Type` header (even if body is null, to match any expected server requirements) contentType?.let { requestBuilder.header(ContentType, contentType.toString()) } - var requestBodyPublisher = body?.let { - contentType ?: throw SundayError(NoSupportedContentTypes) + var requestBodyPublisher = + body?.let { + contentType ?: throw SundayError(NoSupportedContentTypes) - val mediaTypeEncoder = mediaTypeEncoders.find(contentType) - ?: error("Cannot find encoder that was reported as supported") + val mediaTypeEncoder = + mediaTypeEncoders.find(contentType) + ?: error("Cannot find encoder that was reported as supported") - val encodedBody = mediaTypeEncoder.encode(body) + val encodedBody = mediaTypeEncoder.encode(body) - BodyPublishers.ofInputStream { encodedBody.buffer().inputStream() } - } + BodyPublishers.ofInputStream { encodedBody.buffer().inputStream() } + } if (requestBodyPublisher == null && method.requiresBody) { requestBodyPublisher = BodyPublishers.ofByteArray(byteArrayOf()) @@ -179,28 +162,57 @@ class JdkRequestFactory( when (purpose) { RequestPurpose.Normal -> requestTimeout RequestPurpose.Events -> eventRequestTimeout - } - ) - .build() + }, + ).build() logger.debug("Built request: {}", request) return JdkRequest( adapter.invoke(request), - httpClient + httpClient, ) } + private fun uri( + pathTemplate: String, + pathParameters: Parameters?, + queryParameters: Parameters?, + ): URI? { + val uri = + try { + baseURI.resolve(pathTemplate, pathParameters, pathEncoders).toURI() + } catch (x: Throwable) { + throw SundayError(InvalidBaseUri, cause = x) + } + + if (!queryParameters.isNullOrEmpty()) { + // Encode & add query parameters to url + + val urlQueryEncoder = + mediaTypeEncoders.find(WWWFormUrlEncoded) + ?: throw SundayError(NoDecoder, WWWFormUrlEncoded.value) + + urlQueryEncoder as? URLQueryParamsEncoder + ?: throw SundayError( + NoDecoder, + "'$WWWFormUrlEncoded' encoder must implement ${URLQueryParamsEncoder::class.simpleName}", + ) + + val uriQuery = urlQueryEncoder.encodeQueryString(queryParameters) + + return uri.replaceQuery(uriQuery) + } else { + return uri + } + } + override suspend fun response(request: Request): Response { logger.debug("Initiating request") return request.execute() } - override fun eventSource(requestSupplier: suspend (Headers) -> Request): EventSource { - - return EventSource(requestSupplier) - } + override fun eventSource(requestSupplier: suspend (Headers) -> Request): EventSource = EventSource(requestSupplier) override fun close() { close(true) diff --git a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/ReasonPhrases.kt b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/ReasonPhrases.kt index 2d561caa..7938af07 100644 --- a/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/ReasonPhrases.kt +++ b/jdk/src/main/kotlin/io/outfoxx/sunday/jdk/ReasonPhrases.kt @@ -19,8 +19,8 @@ package io.outfoxx.sunday.jdk object ReasonPhrases { @Suppress("MagicNumber", "LongMethod") - fun lookup(statusCode: Int): String? { - return when (statusCode) { + fun lookup(statusCode: Int): String? = + when (statusCode) { 100 -> "Continue" 101 -> "Switching Protocols" 102 -> "Processing" @@ -91,5 +91,4 @@ object ReasonPhrases { else -> null } - } } diff --git a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkEventSourceTest.kt b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkEventSourceTest.kt index b1f760d4..e798cd6e 100644 --- a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkEventSourceTest.kt +++ b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkEventSourceTest.kt @@ -35,33 +35,33 @@ class JdkEventSourceTest : EventSourceTest() { private val onStart: () -> Unit, private val onCancel: () -> Unit, ) : JdkRequest( - request, - httpClient, - ) { + request, + httpClient, + ) { - override fun start(): Flow { - return super.start() + override fun start(): Flow = + super + .start() .onEach { if (it is Request.Event.Start) { onStart() } - } - .onCompletion { + }.onCompletion { if (it is CancellationException) { onCancel() } } - } } override fun createRequest( url: String, headers: Headers, onStart: () -> Unit, - onCancel: () -> Unit + onCancel: () -> Unit, ): Request = JdkTrackingRequest( - HttpRequest.newBuilder(URI(url)) + HttpRequest + .newBuilder(URI(url)) .headers(headers) .build(), HttpClient.newHttpClient(), diff --git a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkGeneratedAPITest.kt b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkGeneratedAPITest.kt index 13cde09b..d7eb3563 100644 --- a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkGeneratedAPITest.kt +++ b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkGeneratedAPITest.kt @@ -27,7 +27,7 @@ class JdkGeneratedAPITest : GeneratedAPITests() { override fun createRequestFactory( uriTemplate: URITemplate, encoders: MediaTypeEncoders, - decoders: MediaTypeDecoders + decoders: MediaTypeDecoders, ): RequestFactory = JdkRequestFactory( uriTemplate, diff --git a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactoryTest.kt b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactoryTest.kt index 92843b30..762a6d21 100644 --- a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactoryTest.kt +++ b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/JdkRequestFactoryTest.kt @@ -43,7 +43,7 @@ class JdkRequestFactoryTest : RequestFactoryTest() { override fun createRequestFactory( uriTemplate: URITemplate, encoders: MediaTypeEncoders, - decoders: MediaTypeDecoders + decoders: MediaTypeDecoders, ): RequestFactory = JdkRequestFactory( uriTemplate, @@ -52,168 +52,181 @@ class JdkRequestFactoryTest : RequestFactoryTest() { ) @Test - fun `adapt a an HTTP request`() = runBlocking { - - val factory = - JdkRequestFactory( - URITemplate("http://example.com"), - adapter = { request -> - request.copyToBuilder() - .header("Authorization", "Bearer 12345") - .build() - } - ) + fun `adapt a an HTTP request`() = + runBlocking { + val factory = + JdkRequestFactory( + URITemplate("http://example.com"), + adapter = { request -> + request + .copyToBuilder() + .header("Authorization", "Bearer 12345") + .build() + }, + ) - val request = factory.request(Method.Get, "test") + val request = factory.request(Method.Get, "test") - assertThat(request.headers, hasItem("Authorization" to "Bearer 12345")) - } + assertThat(request.headers, hasItem("Authorization" to "Bearer 12345")) + } @Test - fun `copying requests to builder`() = runBlocking { - - val headers = - HttpHeaders.of( - mapOf( - Authorization to listOf("Bearer 12345"), - ContentType to listOf("application/json", "application/cbor") - ) - ) { _, _ -> true } - - val get = - HttpRequest.newBuilder(URI("http://example.com")) - .GET() - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val getCopy = assertDoesNotThrow { get.copyToBuilder().build() } - assertThat(getCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(getCopy.method(), equalTo("GET")) - assertThat(getCopy.bodyPublisher().isPresent, equalTo(false)) - assertThat(getCopy.headers(), equalTo(headers)) - - val delete = - HttpRequest.newBuilder(URI("http://example.com")) - .DELETE() - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val deleteCopy = assertDoesNotThrow { delete.copyToBuilder().build() } - assertThat(deleteCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(deleteCopy.method(), equalTo("DELETE")) - assertThat(deleteCopy.bodyPublisher().isPresent, equalTo(false)) - assertThat(deleteCopy.headers(), equalTo(headers)) - - val post = - HttpRequest.newBuilder(URI("http://example.com")) - .POST(BodyPublishers.noBody()) - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val postCopy = assertDoesNotThrow { post.copyToBuilder().build() } - assertThat(postCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(postCopy.method(), equalTo("POST")) - assertThat(postCopy.bodyPublisher().isPresent, equalTo(true)) - assertThat(postCopy.headers(), equalTo(headers)) - - val put = - HttpRequest.newBuilder(URI("http://example.com")) - .PUT(BodyPublishers.ofString("test")) - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val putCopy = assertDoesNotThrow { put.copyToBuilder().build() } - assertThat(putCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(putCopy.method(), equalTo("PUT")) - assertThat(putCopy.bodyPublisher().isPresent, equalTo(true)) - assertThat(putCopy.headers(), equalTo(headers)) - - val custom = - HttpRequest.newBuilder(URI("http://example.com")) - .method("TEST", BodyPublishers.noBody()) - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val customCopy = assertDoesNotThrow { custom.copyToBuilder().build() } - assertThat(customCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(customCopy.method(), equalTo("TEST")) - assertThat(customCopy.bodyPublisher().isPresent, equalTo(true)) - assertThat(customCopy.headers(), equalTo(headers)) - } + @Suppress("LongMethod") + fun `copying requests to builder`() = + runBlocking { + val headers = + HttpHeaders.of( + mapOf( + Authorization to listOf("Bearer 12345"), + ContentType to listOf("application/json", "application/cbor"), + ), + ) { _, _ -> true } + + val get = + HttpRequest + .newBuilder(URI("http://example.com")) + .GET() + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val getCopy = assertDoesNotThrow { get.copyToBuilder().build() } + assertThat(getCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(getCopy.method(), equalTo("GET")) + assertThat(getCopy.bodyPublisher().isPresent, equalTo(false)) + assertThat(getCopy.headers(), equalTo(headers)) + + val delete = + HttpRequest + .newBuilder(URI("http://example.com")) + .DELETE() + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val deleteCopy = assertDoesNotThrow { delete.copyToBuilder().build() } + assertThat(deleteCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(deleteCopy.method(), equalTo("DELETE")) + assertThat(deleteCopy.bodyPublisher().isPresent, equalTo(false)) + assertThat(deleteCopy.headers(), equalTo(headers)) + + val post = + HttpRequest + .newBuilder(URI("http://example.com")) + .POST(BodyPublishers.noBody()) + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val postCopy = assertDoesNotThrow { post.copyToBuilder().build() } + assertThat(postCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(postCopy.method(), equalTo("POST")) + assertThat(postCopy.bodyPublisher().isPresent, equalTo(true)) + assertThat(postCopy.headers(), equalTo(headers)) + + val put = + HttpRequest + .newBuilder(URI("http://example.com")) + .PUT(BodyPublishers.ofString("test")) + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val putCopy = assertDoesNotThrow { put.copyToBuilder().build() } + assertThat(putCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(putCopy.method(), equalTo("PUT")) + assertThat(putCopy.bodyPublisher().isPresent, equalTo(true)) + assertThat(putCopy.headers(), equalTo(headers)) + + val custom = + HttpRequest + .newBuilder(URI("http://example.com")) + .method("TEST", BodyPublishers.noBody()) + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val customCopy = assertDoesNotThrow { custom.copyToBuilder().build() } + assertThat(customCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(customCopy.method(), equalTo("TEST")) + assertThat(customCopy.bodyPublisher().isPresent, equalTo(true)) + assertThat(customCopy.headers(), equalTo(headers)) + } @Test - fun `copying requests to builder without headers`() = runBlocking { - - val headers = HttpHeaders.of(mapOf()) { _, _ -> true } - - val get = - HttpRequest.newBuilder(URI("http://example.com")) - .GET() - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val getCopy = assertDoesNotThrow { get.copyToBuilder(includeHeaders = false).build() } - assertThat(getCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(getCopy.method(), equalTo("GET")) - assertThat(getCopy.bodyPublisher().isPresent, equalTo(false)) - assertThat(getCopy.headers(), equalTo(headers)) - - val delete = - HttpRequest.newBuilder(URI("http://example.com")) - .DELETE() - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val deleteCopy = assertDoesNotThrow { delete.copyToBuilder(includeHeaders = false).build() } - assertThat(deleteCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(deleteCopy.method(), equalTo("DELETE")) - assertThat(deleteCopy.bodyPublisher().isPresent, equalTo(false)) - assertThat(deleteCopy.headers(), equalTo(headers)) - - val post = - HttpRequest.newBuilder(URI("http://example.com")) - .POST(BodyPublishers.noBody()) - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val postCopy = assertDoesNotThrow { post.copyToBuilder(includeHeaders = false).build() } - assertThat(postCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(postCopy.method(), equalTo("POST")) - assertThat(postCopy.bodyPublisher().isPresent, equalTo(true)) - assertThat(postCopy.headers(), equalTo(headers)) - - val put = - HttpRequest.newBuilder(URI("http://example.com")) - .PUT(BodyPublishers.ofString("test")) - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val putCopy = assertDoesNotThrow { put.copyToBuilder(includeHeaders = false).build() } - assertThat(putCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(putCopy.method(), equalTo("PUT")) - assertThat(putCopy.bodyPublisher().isPresent, equalTo(true)) - assertThat(putCopy.headers(), equalTo(headers)) - - val custom = - HttpRequest.newBuilder(URI("http://example.com")) - .method("TEST", BodyPublishers.noBody()) - .header(Authorization, "Bearer 12345") - .header(ContentType, "application/json") - .header(ContentType, "application/cbor") - .build() - val customCopy = assertDoesNotThrow { custom.copyToBuilder(includeHeaders = false).build() } - assertThat(customCopy.uri(), equalTo(URI("http://example.com"))) - assertThat(customCopy.method(), equalTo("TEST")) - assertThat(customCopy.bodyPublisher().isPresent, equalTo(true)) - assertThat(customCopy.headers(), equalTo(headers)) - } + @Suppress("LongMethod") + fun `copying requests to builder without headers`() = + runBlocking { + val headers = HttpHeaders.of(mapOf()) { _, _ -> true } + + val get = + HttpRequest + .newBuilder(URI("http://example.com")) + .GET() + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val getCopy = assertDoesNotThrow { get.copyToBuilder(includeHeaders = false).build() } + assertThat(getCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(getCopy.method(), equalTo("GET")) + assertThat(getCopy.bodyPublisher().isPresent, equalTo(false)) + assertThat(getCopy.headers(), equalTo(headers)) + + val delete = + HttpRequest + .newBuilder(URI("http://example.com")) + .DELETE() + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val deleteCopy = assertDoesNotThrow { delete.copyToBuilder(includeHeaders = false).build() } + assertThat(deleteCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(deleteCopy.method(), equalTo("DELETE")) + assertThat(deleteCopy.bodyPublisher().isPresent, equalTo(false)) + assertThat(deleteCopy.headers(), equalTo(headers)) + + val post = + HttpRequest + .newBuilder(URI("http://example.com")) + .POST(BodyPublishers.noBody()) + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val postCopy = assertDoesNotThrow { post.copyToBuilder(includeHeaders = false).build() } + assertThat(postCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(postCopy.method(), equalTo("POST")) + assertThat(postCopy.bodyPublisher().isPresent, equalTo(true)) + assertThat(postCopy.headers(), equalTo(headers)) + + val put = + HttpRequest + .newBuilder(URI("http://example.com")) + .PUT(BodyPublishers.ofString("test")) + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val putCopy = assertDoesNotThrow { put.copyToBuilder(includeHeaders = false).build() } + assertThat(putCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(putCopy.method(), equalTo("PUT")) + assertThat(putCopy.bodyPublisher().isPresent, equalTo(true)) + assertThat(putCopy.headers(), equalTo(headers)) + + val custom = + HttpRequest + .newBuilder(URI("http://example.com")) + .method("TEST", BodyPublishers.noBody()) + .header(Authorization, "Bearer 12345") + .header(ContentType, "application/json") + .header(ContentType, "application/cbor") + .build() + val customCopy = assertDoesNotThrow { custom.copyToBuilder(includeHeaders = false).build() } + assertThat(customCopy.uri(), equalTo(URI("http://example.com"))) + assertThat(customCopy.method(), equalTo("TEST")) + assertThat(customCopy.bodyPublisher().isPresent, equalTo(true)) + assertThat(customCopy.headers(), equalTo(headers)) + } } diff --git a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/ReasonPhrasesTest.kt b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/ReasonPhrasesTest.kt new file mode 100644 index 00000000..8a0050e7 --- /dev/null +++ b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/ReasonPhrasesTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.outfoxx.sunday.jdk + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.Test + +class ReasonPhrasesTest { + + @Test + fun `test lookups`() { + assertThat(ReasonPhrases.lookup(99), `is`(nullValue())) + assertThat(ReasonPhrases.lookup(100), equalTo("Continue")) + assertThat(ReasonPhrases.lookup(200), equalTo("OK")) + assertThat(ReasonPhrases.lookup(300), equalTo("Multiple Choices")) + assertThat(ReasonPhrases.lookup(400), equalTo("Bad Request")) + assertThat(ReasonPhrases.lookup(500), equalTo("Server Error")) + assertThat(ReasonPhrases.lookup(600), `is`(nullValue())) + } + +} diff --git a/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequest.kt b/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequest.kt index 52bf52e7..c1070a9a 100644 --- a/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequest.kt +++ b/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequest.kt @@ -87,8 +87,10 @@ open class OkHttpRequest( } call.enqueue( object : Callback { - override fun onResponse(call: Call, response: okhttp3.Response) { - + override fun onResponse( + call: Call, + response: okhttp3.Response, + ) { logger.debug("Received response") // Don't bother with resuming the continuation if it is already cancelled. @@ -97,8 +99,10 @@ open class OkHttpRequest( continuation.resume(OkHttpResponse(response, httpClient)) } - override fun onFailure(call: Call, e: IOException) { - + override fun onFailure( + call: Call, + e: IOException, + ) { logger.debug("Received error") // Don't bother with resuming the continuation if it is already cancelled. @@ -106,14 +110,13 @@ open class OkHttpRequest( continuation.resumeWithException(e) } - } + }, ) } } - override fun start(): Flow { - return callbackFlow { - + override fun start(): Flow = + callbackFlow { logger.debug("Starting") val callback = RequestCallback(this, httpClient, requestDispatcher) @@ -126,7 +129,6 @@ open class OkHttpRequest( callback.cancel() } } - } class RequestCallback( private val scope: ProducerScope, @@ -140,55 +142,55 @@ open class OkHttpRequest( reader?.cancel() } - override fun onResponse(call: Call, response: okhttp3.Response) { - + override fun onResponse( + call: Call, + response: okhttp3.Response, + ) { logger.debug("Received response") - reader = scope.launch(dispatcher) { - - response.use { - - // Because they called `start`, this is a long-lived response, - // cancel full-call timeouts. - (call as? RealCall)?.timeoutEarlyExit() + reader = + scope.launch(dispatcher) { + response.use { + // Because they called `start`, this is a long-lived response, + // cancel full-call timeouts. + (call as? RealCall)?.timeoutEarlyExit() - val startEvent = Request.Event.Start(OkHttpResponse(response, httpClient)) + val startEvent = Request.Event.Start(OkHttpResponse(response, httpClient)) - scope.send(startEvent) + scope.send(startEvent) - val body = response.body - if (body != null) { + val body = response.body + if (body != null) { + logger.debug("Processing: response body") - logger.debug("Processing: response body") + while (isActive && scope.isActive) { + val buffer = Buffer() - while (isActive && scope.isActive) { + val bytesRead = body.source().read(buffer, READ_SIZE) + if (bytesRead == EOF) { + break + } - val buffer = Buffer() - - val bytesRead = body.source().read(buffer, READ_SIZE) - if (bytesRead == EOF) { - break + scope.send(Request.Event.Data(buffer)) } - scope.send(Request.Event.Data(buffer)) } + if (isActive && scope.isActive) { + scope.send(Request.Event.End(response.trailers())) + } } - if (isActive && scope.isActive) { + scope.close() - scope.send(Request.Event.End(response.trailers())) - } + logger.debug("Closed: response events") } - - scope.close() - - logger.debug("Closed: response events") - } } - override fun onFailure(call: Call, e: IOException) { - + override fun onFailure( + call: Call, + e: IOException, + ) { logger.debug("Received error") scope.cancel("Call failed", e) diff --git a/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactory.kt b/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactory.kt index c6422857..9f9b0a4f 100644 --- a/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactory.kt +++ b/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactory.kt @@ -56,7 +56,8 @@ class OkHttpRequestFactory( override val mediaTypeEncoders: MediaTypeEncoders = MediaTypeEncoders.default, override val mediaTypeDecoders: MediaTypeDecoders = MediaTypeDecoders.default, override val pathEncoders: Map, PathEncoder> = PathEncoders.default, -) : RequestFactory(), Closeable { +) : RequestFactory(), + Closeable { companion object { @@ -67,7 +68,10 @@ class OkHttpRequestFactory( get() = registeredProblemTypesStorage private val registeredProblemTypesStorage = mutableMapOf>() - override fun registerProblem(typeId: String, problemType: KClass) { + override fun registerProblem( + typeId: String, + problemType: KClass, + ) { registeredProblemTypesStorage[typeId] = problemType } @@ -85,21 +89,24 @@ class OkHttpRequestFactory( logger.trace("Building request") val urlBuilder = - baseURI.resolve(pathTemplate, pathParameters, pathEncoders) - .toURI().toHttpUrlOrNull()?.newBuilder() + baseURI + .resolve(pathTemplate, pathParameters, pathEncoders) + .toURI() + .toHttpUrlOrNull() + ?.newBuilder() ?: throw SundayError(InvalidBaseUri) if (!queryParameters.isNullOrEmpty()) { - // Encode & add query parameters to url - val urlQueryEncoder = mediaTypeEncoders.find(WWWFormUrlEncoded) - ?: throw SundayError(NoDecoder, WWWFormUrlEncoded.value) + val urlQueryEncoder = + mediaTypeEncoders.find(WWWFormUrlEncoded) + ?: throw SundayError(NoDecoder, WWWFormUrlEncoded.value) urlQueryEncoder as? URLQueryParamsEncoder ?: throw SundayError( NoDecoder, - "'$WWWFormUrlEncoded' encoder must implement ${URLQueryParamsEncoder::class.simpleName}" + "'$WWWFormUrlEncoded' encoder must implement ${URLQueryParamsEncoder::class.simpleName}", ) urlBuilder.encodedQuery(urlQueryEncoder.encodeQueryString(queryParameters)) @@ -128,16 +135,18 @@ class OkHttpRequestFactory( // Add `Content-Type` header (even if body is null, to match any expected server requirements) contentType?.let { requestBuilder.addHeader(ContentType, contentType.toString()) } - var requestBody = body?.let { - contentType ?: throw SundayError(NoSupportedContentTypes) + var requestBody = + body?.let { + contentType ?: throw SundayError(NoSupportedContentTypes) - val mediaTypeEncoder = mediaTypeEncoders.find(contentType) - ?: error("Cannot find encoder that was reported as supported") + val mediaTypeEncoder = + mediaTypeEncoders.find(contentType) + ?: error("Cannot find encoder that was reported as supported") - val encodedBody = mediaTypeEncoder.encode(body).buffer().readByteString() + val encodedBody = mediaTypeEncoder.encode(body).buffer().readByteString() - encodedBody.toRequestBody(contentType.value.toMediaType()) - } + encodedBody.toRequestBody(contentType.value.toMediaType()) + } if (requestBody == null && method.requiresBody) { requestBody = byteArrayOf().toRequestBody() @@ -165,10 +174,7 @@ class OkHttpRequestFactory( return request.execute() } - override fun eventSource(requestSupplier: suspend (Headers) -> Request): EventSource { - - return EventSource(requestSupplier) - } + override fun eventSource(requestSupplier: suspend (Headers) -> Request): EventSource = EventSource(requestSupplier) override fun close() { close(true) diff --git a/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpResponse.kt b/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpResponse.kt index 1d39883b..11ead9d1 100644 --- a/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpResponse.kt +++ b/okhttp/src/main/kotlin/io/outfoxx/sunday/okhttp/OkHttpResponse.kt @@ -26,7 +26,7 @@ import okio.BufferedSource */ class OkHttpResponse( private val response: okhttp3.Response, - private val httpClient: OkHttpClient + private val httpClient: OkHttpClient, ) : Response { override val statusCode: Int diff --git a/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpEventSourceTest.kt b/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpEventSourceTest.kt index 5addbb79..1f02420c 100644 --- a/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpEventSourceTest.kt +++ b/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpEventSourceTest.kt @@ -37,34 +37,34 @@ class OkHttpEventSourceTest : EventSourceTest() { private val onStart: () -> Unit, private val onCancel: () -> Unit, ) : OkHttpRequest( - request, - httpClient, - requestDispatcher, - ) { + request, + httpClient, + requestDispatcher, + ) { - override fun start(): Flow { - return super.start() + override fun start(): Flow = + super + .start() .onEach { if (it is Request.Event.Start) { onStart() } - } - .onCompletion { + }.onCompletion { if (it is CancellationException) { onCancel() } } - } } override fun createRequest( url: String, headers: Headers, onStart: () -> Unit, - onCancel: () -> Unit + onCancel: () -> Unit, ): Request = OkHttpTrackingRequest( - okhttp3.Request.Builder() + okhttp3.Request + .Builder() .method("GET", null) .url(url) .headers(headers.toMap().toHeaders()) diff --git a/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpGeneratedAPITest.kt b/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpGeneratedAPITest.kt index 7fba0b5f..980b52e4 100644 --- a/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpGeneratedAPITest.kt +++ b/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpGeneratedAPITest.kt @@ -27,7 +27,7 @@ class OkHttpGeneratedAPITest : GeneratedAPITests() { override fun createRequestFactory( uriTemplate: URITemplate, encoders: MediaTypeEncoders, - decoders: MediaTypeDecoders + decoders: MediaTypeDecoders, ): RequestFactory = OkHttpRequestFactory( uriTemplate, diff --git a/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactoryTest.kt b/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactoryTest.kt index 58ab6ef5..add199a2 100644 --- a/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactoryTest.kt +++ b/okhttp/src/test/kotlin/io/outfoxx/sunday/okhttp/OkHttpRequestFactoryTest.kt @@ -30,7 +30,7 @@ class OkHttpRequestFactoryTest : RequestFactoryTest() { override fun createRequestFactory( uriTemplate: URITemplate, encoders: MediaTypeEncoders, - decoders: MediaTypeDecoders + decoders: MediaTypeDecoders, ): RequestFactory = OkHttpRequestFactory( uriTemplate, diff --git a/settings.gradle.kts b/settings.gradle.kts index 9de8a555..1e86c270 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ pluginManagement { } val kotlinPluginVersion: String by settings + val koverPluginVersion: String by settings val dokkaPluginVersion: String by settings val licenserPluginVersion: String by settings val kotlinterPluginVersion: String by settings @@ -17,6 +18,7 @@ pluginManagement { plugins { kotlin("jvm") version kotlinPluginVersion + id("org.jetbrains.kotlinx.kover") version koverPluginVersion id("org.jetbrains.dokka") version dokkaPluginVersion id("org.cadixdev.licenser") version licenserPluginVersion id("org.jmailen.kotlinter") version kotlinterPluginVersion