Skip to content

Commit

Permalink
Revert breaking changes for 1.4 release (#2001)
Browse files Browse the repository at this point in the history
* Revert Semaphore breaking change

* Revert ContentType breaking changes

* Update public API
  • Loading branch information
e5l authored Aug 10, 2020
1 parent b5bcb7c commit 22b5d7a
Show file tree
Hide file tree
Showing 20 changed files with 246 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ public final class io/ktor/client/features/json/DefaultJvmKt {
public final class io/ktor/client/features/json/JsonFeature {
public static final field Feature Lio/ktor/client/features/json/JsonFeature$Feature;
public fun <init> (Lio/ktor/client/features/json/JsonSerializer;)V
public final fun getConfig ()Lio/ktor/client/features/json/JsonFeature$Config;
public final fun getAcceptContentTypes ()Ljava/util/List;
public final fun getSerializer ()Lio/ktor/client/features/json/JsonSerializer;
}

public final class io/ktor/client/features/json/JsonFeature$Config {
public fun <init> ()V
public final fun accept ([Lio/ktor/http/ContentTypeMatcher;)V
public final fun accept ([Lio/ktor/http/ContentType;)V
public final fun getAcceptContentTypes ()Ljava/util/List;
public final fun getReceiveContentTypeMatchers ()Ljava/util/List;
public final fun getSerializer ()Lio/ktor/client/features/json/JsonSerializer;
public final fun receive (Lio/ktor/http/ContentTypeMatcher;)V
public final fun setAcceptContentTypes (Ljava/util/List;)V
public final fun setReceiveContentTypeMatchers (Ljava/util/List;)V
public final fun setSerializer (Lio/ktor/client/features/json/JsonSerializer;)V
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2014-2020 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.features.json

import io.ktor.http.*

internal class JsonContentTypeMatcher : ContentTypeMatcher {
override fun contains(contentType: ContentType): Boolean {
if (ContentType.Application.Json.match(contentType)) {
return true
}

val value = contentType.withoutParameters().toString()
return value.startsWith("application/") && value.endsWith("+json")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ package io.ktor.client.features.json

import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.JsonFeature.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.client.utils.*
import io.ktor.http.*
import io.ktor.util.*
import io.ktor.utils.io.*
import io.ktor.utils.io.core.*


/**
Expand All @@ -28,26 +26,30 @@ expect fun defaultSerializer(): JsonSerializer

/**
* [HttpClient] feature that serializes/de-serializes as JSON custom objects
* to request and from response bodies using a [Config.serializer].
* to request and from response bodies using a [serializer].
*
* The default [Config.serializer] is [GsonSerializer].
* The default [serializer] is [GsonSerializer].
*
* The default [Config.acceptContentTypes] is a list with a [ContentTypeMatcher] accepting
* [ContentType.Application.Json] and any `application/...+json` pattern.
* The default [acceptContentTypes] is a list which contains [ContentType.Application.Json]
*
* Note:
* The request/response body is only serialized/deserialized if the specified type is a public
* accessible class and the Content-Type is matched by [Config.acceptContentTypes].
* Note: It will de-serialize the body response if the specified type is a public accessible class
* and the Content-Type is one of [acceptContentTypes] list (`application/json` by default).
*
* @property serializer that is used to serialize and deserialize request/response bodies
* @property acceptContentTypes that are allowed when receiving content
*/
class JsonFeature internal constructor(val config: Config) {
@Deprecated(
"Install feature properly instead of direct instantiation.",
level = DeprecationLevel.ERROR
)
constructor(serializer: JsonSerializer) : this(
Config().apply {
this.serializer = serializer
}
class JsonFeature internal constructor(
val serializer: JsonSerializer,
val acceptContentTypes: List<ContentType> = listOf(ContentType.Application.Json),
private val receiveContentTypeMatchers: List<ContentTypeMatcher> = listOf(JsonContentTypeMatcher()),
) {
@Deprecated("Install feature properly instead of direct instantiation.", level = DeprecationLevel.ERROR)
constructor(serializer: JsonSerializer) : this(serializer, listOf(ContentType.Application.Json))

internal constructor(config: Config) : this(
config.serializer ?: defaultSerializer(),
config.acceptContentTypes,
config.receiveContentTypeMatchers
)

/**
Expand All @@ -59,36 +61,65 @@ class JsonFeature internal constructor(val config: Config) {
*
* Default value for [serializer] is [defaultSerializer].
*/
var serializer: JsonSerializer by lazyVar { defaultSerializer() }
var serializer: JsonSerializer? = null

/**
* Backing field with mutable list of content types that are handled by this feature.
*/
private val _acceptContentTypes: MutableList<ContentTypeMatcher> =
private val _acceptContentTypes: MutableList<ContentType> = mutableListOf(ContentType.Application.Json)
private val _receiveContentTypeMatchers: MutableList<ContentTypeMatcher> =
mutableListOf(JsonContentTypeMatcher())

/**
* List of content types that are handled by this feature.
* It also affects `Accept` request header value.
* Please note that wildcard content types are supported but no quality specification provided.
*/
var acceptContentTypes: List<ContentTypeMatcher>
get() = _acceptContentTypes
@KtorExperimentalAPI
var acceptContentTypes: List<ContentType>
set(value) {
require(value.isNotEmpty()) { "At least one content type should be provided to acceptContentTypes" }

_acceptContentTypes.clear()
_acceptContentTypes.addAll(value.toSet())
_acceptContentTypes.addAll(value)
}
get() = _acceptContentTypes

/**
* List of content type matchers that are handled by this feature.
* Please note that wildcard content types are supported but no quality specification provided.
*/
@KtorExperimentalAPI
var receiveContentTypeMatchers: List<ContentTypeMatcher>
set(value) {
require(value.isNotEmpty()) { "At least one content type should be provided to acceptContentTypes" }
_receiveContentTypeMatchers.clear()
_receiveContentTypeMatchers.addAll(value)
}
get() = _receiveContentTypeMatchers

/**
* Adds accepted content types. Be aware that [ContentType.Application.Json] accepted by default is removed from
* the list if you use this function to provide accepted content types.
* It also affects `Accept` request header value.
*/
fun accept(vararg contentTypes: ContentType) {
_acceptContentTypes += contentTypes
}

/**
* Adds accepted content types. Existing content types will not be removed.
*/
fun accept(vararg contentTypes: ContentTypeMatcher) {
val values = _acceptContentTypes.toSet() + contentTypes
acceptContentTypes = values.toList()
fun receive(matcher: ContentTypeMatcher) {
_receiveContentTypeMatchers += matcher
}
}

internal fun matchesContentType(contentType: ContentType?): Boolean =
contentType != null && acceptContentTypes.any { it.match(contentType) }
internal fun canHandle(contentType: ContentType): Boolean {
val accepted = acceptContentTypes.any { contentType.match(it) }
val matchers = receiveContentTypeMatchers

return accepted || matchers.any { matcher -> matcher.contains(contentType) }
}

/**
Expand All @@ -97,54 +128,47 @@ class JsonFeature internal constructor(val config: Config) {
companion object Feature : HttpClientFeature<Config, JsonFeature> {
override val key: AttributeKey<JsonFeature> = AttributeKey("Json")

override fun prepare(block: Config.() -> Unit): JsonFeature =
JsonFeature(Config().apply(block))
override fun prepare(block: Config.() -> Unit): JsonFeature {
val config = Config().apply(block)
val serializer = config.serializer ?: defaultSerializer()
val allowedContentTypes = config.acceptContentTypes.toList()
val receiveContentTypeMatchers = config.receiveContentTypeMatchers

return JsonFeature(serializer, allowedContentTypes, receiveContentTypeMatchers)
}

override fun install(feature: JsonFeature, scope: HttpClient) {
val config = feature.config
scope.requestPipeline.intercept(HttpRequestPipeline.Transform) { payload ->
feature.acceptContentTypes.forEach { context.accept(it) }

val contentType = context.contentType() ?: return@intercept
if (!config.matchesContentType(contentType))
return@intercept
if (!feature.canHandle(contentType)) return@intercept

context.headers.remove(HttpHeaders.ContentType)

val serializedContent = when (payload) {
Unit -> EmptyContent
is EmptyContent -> EmptyContent
else -> config.serializer.write(payload, contentType)
else -> feature.serializer.write(payload, contentType)
}

proceedWith(serializedContent)
}

scope.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) ->
if (!config.matchesContentType(context.response.contentType()))
return@intercept
if (body !is ByteReadChannel) return@intercept

val data = when (body) {
is ByteReadChannel -> body.readRemaining()
is String -> ByteReadPacket(body.toByteArray())
else -> return@intercept
}
val parsedBody = config.serializer.read(info, data)
val contentType = context.response.contentType() ?: return@intercept
if (!feature.canHandle(contentType)) return@intercept

val parsedBody = feature.serializer.read(info, body.readRemaining())
val response = HttpResponseContainer(info, parsedBody)
proceedWith(response)
}
}
}
}

private class JsonContentTypeMatcher : ContentTypeMatcher {
override fun match(contentType: ContentType): Boolean {
if (ContentType.Application.Json.match(contentType)) {
return true
}
val value = contentType.withoutParameters().toString()
return value.startsWith("application/") && value.endsWith("+json")
}
}

/**
* Install [JsonFeature].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ class JsonFeatureTest {
val config = JsonFeature.Config()

assertEquals(1, config.acceptContentTypes.size)
assertTrue { config.matchesContentType(ContentType.Application.Json) }
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Json) }

assertTrue { config.matchesContentType(ContentType.parse("application/json")) }
assertTrue { config.matchesContentType(ContentType.parse("application/vnd.foo+json")) }
assertFalse { config.matchesContentType(ContentType.parse("text/json")) }
val feature = JsonFeature(config)
assertTrue { feature.canHandle(ContentType.parse("application/json")) }
assertTrue { feature.canHandle(ContentType.parse("application/vnd.foo+json")) }
assertFalse { feature.canHandle(ContentType.parse("text/json")) }
}

@Test
fun testAcceptCall() {
val config = JsonFeature.Config()
config.accept(ContentType.Application.Xml)
config.accept(ContentType.Application.Xml)

assertEquals(2, config.acceptContentTypes.size)
assertTrue { config.matchesContentType(ContentType.Application.Json) }
assertTrue { config.matchesContentType(ContentType.Application.Xml) }
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Json) }
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Xml) }
}

@Test
Expand All @@ -37,22 +37,22 @@ class JsonFeatureTest {
config.acceptContentTypes = listOf(ContentType.Application.Pdf, ContentType.Application.Xml)

assertEquals(2, config.acceptContentTypes.size)
assertFalse { config.matchesContentType(ContentType.Application.Json) }
assertTrue { config.matchesContentType(ContentType.Application.Xml) }
assertTrue { config.matchesContentType(ContentType.Application.Pdf) }
assertFalse { config.acceptContentTypes.contains(ContentType.Application.Json) }
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Xml) }
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Pdf) }
}

@Test
fun testContentTypesFilter() {
val config = JsonFeature.Config().apply {
acceptContentTypes = listOf(object : ContentTypeMatcher {
override fun match(contentType: ContentType): Boolean =
contentType.match("text/json")
receive(object : ContentTypeMatcher {
override fun contains(contentType: ContentType): Boolean {
return contentType.toString() == "text/json"
}
})
}

assertFalse { config.matchesContentType(ContentType.parse("application/json")) }
assertFalse { config.matchesContentType(ContentType.parse("application/vnd.foo+json")) }
assertTrue { config.matchesContentType(ContentType.parse("text/json")) }
val feature = JsonFeature(config)
assertTrue { feature.canHandle(ContentType.parse("text/json")) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class KotlinxSerializerTest : ClientLoader() {
val response = client.post<String>("$TEST_SERVER/echo-with-content-type") {
body = "Hello"
}
assertEquals("Hello", response)
assertEquals("\"Hello\"", response)

val textResponse = client.post<String>("$TEST_SERVER/echo") {
body = "Hello"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ actual fun defaultSerializer(): JsonSerializer {
" - ktor-client-serialization"
)

return serializers.maxBy { it::javaClass.name }!!
return serializers.maxByOrNull { it::javaClass.name }!!
}
6 changes: 3 additions & 3 deletions ktor-http/api/ktor-http.api
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,15 @@ public final class io/ktor/http/ContentRangeKt {
public static synthetic fun contentRangeHeaderValue$default (Lkotlin/ranges/LongRange;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
}

public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParameters, io/ktor/http/ContentTypeMatcher {
public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParameters {
public static final field Companion Lio/ktor/http/ContentType$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getContentSubtype ()Ljava/lang/String;
public final fun getContentType ()Ljava/lang/String;
public fun hashCode ()I
public fun match (Lio/ktor/http/ContentType;)Z
public final fun match (Lio/ktor/http/ContentType;)Z
public final fun match (Ljava/lang/String;)Z
public final fun withParameter (Ljava/lang/String;Ljava/lang/String;)Lio/ktor/http/ContentType;
public final fun withoutParameters ()Lio/ktor/http/ContentType;
Expand Down Expand Up @@ -229,7 +229,7 @@ public final class io/ktor/http/ContentType$Video {
}

public abstract interface class io/ktor/http/ContentTypeMatcher {
public abstract fun match (Lio/ktor/http/ContentType;)Z
public abstract fun contains (Lio/ktor/http/ContentType;)Z
}

public final class io/ktor/http/ContentTypesKt {
Expand Down
2 changes: 1 addition & 1 deletion ktor-http/common/src/io/ktor/http/ContentTypeMatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ interface ContentTypeMatcher {
/**
* Checks if `this` type matches a [contentType] type.
*/
fun match(contentType: ContentType): Boolean
fun contains(contentType: ContentType): Boolean
}
Loading

0 comments on commit 22b5d7a

Please sign in to comment.