Skip to content

Commit 22b5d7a

Browse files
authored
Revert breaking changes for 1.4 release (#2001)
* Revert Semaphore breaking change * Revert ContentType breaking changes * Update public API
1 parent b5bcb7c commit 22b5d7a

File tree

20 files changed

+246
-155
lines changed

20 files changed

+246
-155
lines changed

ktor-client/ktor-client-features/ktor-client-json/api/ktor-client-json.api

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ public final class io/ktor/client/features/json/DefaultJvmKt {
55
public final class io/ktor/client/features/json/JsonFeature {
66
public static final field Feature Lio/ktor/client/features/json/JsonFeature$Feature;
77
public fun <init> (Lio/ktor/client/features/json/JsonSerializer;)V
8-
public final fun getConfig ()Lio/ktor/client/features/json/JsonFeature$Config;
8+
public final fun getAcceptContentTypes ()Ljava/util/List;
9+
public final fun getSerializer ()Lio/ktor/client/features/json/JsonSerializer;
910
}
1011

1112
public final class io/ktor/client/features/json/JsonFeature$Config {
1213
public fun <init> ()V
13-
public final fun accept ([Lio/ktor/http/ContentTypeMatcher;)V
14+
public final fun accept ([Lio/ktor/http/ContentType;)V
1415
public final fun getAcceptContentTypes ()Ljava/util/List;
16+
public final fun getReceiveContentTypeMatchers ()Ljava/util/List;
1517
public final fun getSerializer ()Lio/ktor/client/features/json/JsonSerializer;
18+
public final fun receive (Lio/ktor/http/ContentTypeMatcher;)V
1619
public final fun setAcceptContentTypes (Ljava/util/List;)V
20+
public final fun setReceiveContentTypeMatchers (Ljava/util/List;)V
1721
public final fun setSerializer (Lio/ktor/client/features/json/JsonSerializer;)V
1822
}
1923

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2014-2020 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.client.features.json
6+
7+
import io.ktor.http.*
8+
9+
internal class JsonContentTypeMatcher : ContentTypeMatcher {
10+
override fun contains(contentType: ContentType): Boolean {
11+
if (ContentType.Application.Json.match(contentType)) {
12+
return true
13+
}
14+
15+
val value = contentType.withoutParameters().toString()
16+
return value.startsWith("application/") && value.endsWith("+json")
17+
}
18+
}

ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt

+76-52
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ package io.ktor.client.features.json
66

77
import io.ktor.client.*
88
import io.ktor.client.features.*
9-
import io.ktor.client.features.json.JsonFeature.*
109
import io.ktor.client.request.*
1110
import io.ktor.client.statement.*
1211
import io.ktor.client.utils.*
1312
import io.ktor.http.*
1413
import io.ktor.util.*
1514
import io.ktor.utils.io.*
16-
import io.ktor.utils.io.core.*
1715

1816

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

2927
/**
3028
* [HttpClient] feature that serializes/de-serializes as JSON custom objects
31-
* to request and from response bodies using a [Config.serializer].
29+
* to request and from response bodies using a [serializer].
3230
*
33-
* The default [Config.serializer] is [GsonSerializer].
31+
* The default [serializer] is [GsonSerializer].
3432
*
35-
* The default [Config.acceptContentTypes] is a list with a [ContentTypeMatcher] accepting
36-
* [ContentType.Application.Json] and any `application/...+json` pattern.
33+
* The default [acceptContentTypes] is a list which contains [ContentType.Application.Json]
3734
*
38-
* Note:
39-
* The request/response body is only serialized/deserialized if the specified type is a public
40-
* accessible class and the Content-Type is matched by [Config.acceptContentTypes].
35+
* Note: It will de-serialize the body response if the specified type is a public accessible class
36+
* and the Content-Type is one of [acceptContentTypes] list (`application/json` by default).
37+
*
38+
* @property serializer that is used to serialize and deserialize request/response bodies
39+
* @property acceptContentTypes that are allowed when receiving content
4140
*/
42-
class JsonFeature internal constructor(val config: Config) {
43-
@Deprecated(
44-
"Install feature properly instead of direct instantiation.",
45-
level = DeprecationLevel.ERROR
46-
)
47-
constructor(serializer: JsonSerializer) : this(
48-
Config().apply {
49-
this.serializer = serializer
50-
}
41+
class JsonFeature internal constructor(
42+
val serializer: JsonSerializer,
43+
val acceptContentTypes: List<ContentType> = listOf(ContentType.Application.Json),
44+
private val receiveContentTypeMatchers: List<ContentTypeMatcher> = listOf(JsonContentTypeMatcher()),
45+
) {
46+
@Deprecated("Install feature properly instead of direct instantiation.", level = DeprecationLevel.ERROR)
47+
constructor(serializer: JsonSerializer) : this(serializer, listOf(ContentType.Application.Json))
48+
49+
internal constructor(config: Config) : this(
50+
config.serializer ?: defaultSerializer(),
51+
config.acceptContentTypes,
52+
config.receiveContentTypeMatchers
5153
)
5254

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

6466
/**
6567
* Backing field with mutable list of content types that are handled by this feature.
6668
*/
67-
private val _acceptContentTypes: MutableList<ContentTypeMatcher> =
69+
private val _acceptContentTypes: MutableList<ContentType> = mutableListOf(ContentType.Application.Json)
70+
private val _receiveContentTypeMatchers: MutableList<ContentTypeMatcher> =
6871
mutableListOf(JsonContentTypeMatcher())
6972

7073
/**
7174
* List of content types that are handled by this feature.
7275
* It also affects `Accept` request header value.
7376
* Please note that wildcard content types are supported but no quality specification provided.
7477
*/
75-
var acceptContentTypes: List<ContentTypeMatcher>
76-
get() = _acceptContentTypes
78+
@KtorExperimentalAPI
79+
var acceptContentTypes: List<ContentType>
7780
set(value) {
81+
require(value.isNotEmpty()) { "At least one content type should be provided to acceptContentTypes" }
82+
7883
_acceptContentTypes.clear()
79-
_acceptContentTypes.addAll(value.toSet())
84+
_acceptContentTypes.addAll(value)
85+
}
86+
get() = _acceptContentTypes
87+
88+
/**
89+
* List of content type matchers that are handled by this feature.
90+
* Please note that wildcard content types are supported but no quality specification provided.
91+
*/
92+
@KtorExperimentalAPI
93+
var receiveContentTypeMatchers: List<ContentTypeMatcher>
94+
set(value) {
95+
require(value.isNotEmpty()) { "At least one content type should be provided to acceptContentTypes" }
96+
_receiveContentTypeMatchers.clear()
97+
_receiveContentTypeMatchers.addAll(value)
8098
}
99+
get() = _receiveContentTypeMatchers
100+
101+
/**
102+
* Adds accepted content types. Be aware that [ContentType.Application.Json] accepted by default is removed from
103+
* the list if you use this function to provide accepted content types.
104+
* It also affects `Accept` request header value.
105+
*/
106+
fun accept(vararg contentTypes: ContentType) {
107+
_acceptContentTypes += contentTypes
108+
}
81109

82110
/**
83111
* Adds accepted content types. Existing content types will not be removed.
84112
*/
85-
fun accept(vararg contentTypes: ContentTypeMatcher) {
86-
val values = _acceptContentTypes.toSet() + contentTypes
87-
acceptContentTypes = values.toList()
113+
fun receive(matcher: ContentTypeMatcher) {
114+
_receiveContentTypeMatchers += matcher
88115
}
116+
}
89117

90-
internal fun matchesContentType(contentType: ContentType?): Boolean =
91-
contentType != null && acceptContentTypes.any { it.match(contentType) }
118+
internal fun canHandle(contentType: ContentType): Boolean {
119+
val accepted = acceptContentTypes.any { contentType.match(it) }
120+
val matchers = receiveContentTypeMatchers
121+
122+
return accepted || matchers.any { matcher -> matcher.contains(contentType) }
92123
}
93124

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

100-
override fun prepare(block: Config.() -> Unit): JsonFeature =
101-
JsonFeature(Config().apply(block))
131+
override fun prepare(block: Config.() -> Unit): JsonFeature {
132+
val config = Config().apply(block)
133+
val serializer = config.serializer ?: defaultSerializer()
134+
val allowedContentTypes = config.acceptContentTypes.toList()
135+
val receiveContentTypeMatchers = config.receiveContentTypeMatchers
136+
137+
return JsonFeature(serializer, allowedContentTypes, receiveContentTypeMatchers)
138+
}
102139

103140
override fun install(feature: JsonFeature, scope: HttpClient) {
104-
val config = feature.config
105141
scope.requestPipeline.intercept(HttpRequestPipeline.Transform) { payload ->
142+
feature.acceptContentTypes.forEach { context.accept(it) }
143+
106144
val contentType = context.contentType() ?: return@intercept
107-
if (!config.matchesContentType(contentType))
108-
return@intercept
145+
if (!feature.canHandle(contentType)) return@intercept
109146

110147
context.headers.remove(HttpHeaders.ContentType)
111148

112149
val serializedContent = when (payload) {
113150
Unit -> EmptyContent
114151
is EmptyContent -> EmptyContent
115-
else -> config.serializer.write(payload, contentType)
152+
else -> feature.serializer.write(payload, contentType)
116153
}
117154

118155
proceedWith(serializedContent)
119156
}
120157

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

125-
val data = when (body) {
126-
is ByteReadChannel -> body.readRemaining()
127-
is String -> ByteReadPacket(body.toByteArray())
128-
else -> return@intercept
129-
}
130-
val parsedBody = config.serializer.read(info, data)
161+
val contentType = context.response.contentType() ?: return@intercept
162+
if (!feature.canHandle(contentType)) return@intercept
163+
164+
val parsedBody = feature.serializer.read(info, body.readRemaining())
131165
val response = HttpResponseContainer(info, parsedBody)
132166
proceedWith(response)
133167
}
134168
}
135169
}
136170
}
137171

138-
private class JsonContentTypeMatcher : ContentTypeMatcher {
139-
override fun match(contentType: ContentType): Boolean {
140-
if (ContentType.Application.Json.match(contentType)) {
141-
return true
142-
}
143-
val value = contentType.withoutParameters().toString()
144-
return value.startsWith("application/") && value.endsWith("+json")
145-
}
146-
}
147-
148172
/**
149173
* Install [JsonFeature].
150174
*/

ktor-client/ktor-client-features/ktor-client-json/common/test/JsonFeatureTest.kt

+16-16
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@ class JsonFeatureTest {
1313
val config = JsonFeature.Config()
1414

1515
assertEquals(1, config.acceptContentTypes.size)
16-
assertTrue { config.matchesContentType(ContentType.Application.Json) }
16+
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Json) }
1717

18-
assertTrue { config.matchesContentType(ContentType.parse("application/json")) }
19-
assertTrue { config.matchesContentType(ContentType.parse("application/vnd.foo+json")) }
20-
assertFalse { config.matchesContentType(ContentType.parse("text/json")) }
18+
val feature = JsonFeature(config)
19+
assertTrue { feature.canHandle(ContentType.parse("application/json")) }
20+
assertTrue { feature.canHandle(ContentType.parse("application/vnd.foo+json")) }
21+
assertFalse { feature.canHandle(ContentType.parse("text/json")) }
2122
}
2223

2324
@Test
2425
fun testAcceptCall() {
2526
val config = JsonFeature.Config()
2627
config.accept(ContentType.Application.Xml)
27-
config.accept(ContentType.Application.Xml)
2828

2929
assertEquals(2, config.acceptContentTypes.size)
30-
assertTrue { config.matchesContentType(ContentType.Application.Json) }
31-
assertTrue { config.matchesContentType(ContentType.Application.Xml) }
30+
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Json) }
31+
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Xml) }
3232
}
3333

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

3939
assertEquals(2, config.acceptContentTypes.size)
40-
assertFalse { config.matchesContentType(ContentType.Application.Json) }
41-
assertTrue { config.matchesContentType(ContentType.Application.Xml) }
42-
assertTrue { config.matchesContentType(ContentType.Application.Pdf) }
40+
assertFalse { config.acceptContentTypes.contains(ContentType.Application.Json) }
41+
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Xml) }
42+
assertTrue { config.acceptContentTypes.contains(ContentType.Application.Pdf) }
4343
}
4444

4545
@Test
4646
fun testContentTypesFilter() {
4747
val config = JsonFeature.Config().apply {
48-
acceptContentTypes = listOf(object : ContentTypeMatcher {
49-
override fun match(contentType: ContentType): Boolean =
50-
contentType.match("text/json")
48+
receive(object : ContentTypeMatcher {
49+
override fun contains(contentType: ContentType): Boolean {
50+
return contentType.toString() == "text/json"
51+
}
5152
})
5253
}
5354

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

ktor-client/ktor-client-features/ktor-client-json/common/test/KotlinxSerializerTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class KotlinxSerializerTest : ClientLoader() {
9999
val response = client.post<String>("$TEST_SERVER/echo-with-content-type") {
100100
body = "Hello"
101101
}
102-
assertEquals("Hello", response)
102+
assertEquals("\"Hello\"", response)
103103

104104
val textResponse = client.post<String>("$TEST_SERVER/echo") {
105105
body = "Hello"

ktor-client/ktor-client-features/ktor-client-json/jvm/src/io/ktor/client/features/json/DefaultJvm.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ actual fun defaultSerializer(): JsonSerializer {
1717
" - ktor-client-serialization"
1818
)
1919

20-
return serializers.maxBy { it::javaClass.name }!!
20+
return serializers.maxByOrNull { it::javaClass.name }!!
2121
}

ktor-http/api/ktor-http.api

+3-3
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,15 @@ public final class io/ktor/http/ContentRangeKt {
128128
public static synthetic fun contentRangeHeaderValue$default (Lkotlin/ranges/LongRange;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
129129
}
130130

131-
public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParameters, io/ktor/http/ContentTypeMatcher {
131+
public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParameters {
132132
public static final field Companion Lio/ktor/http/ContentType$Companion;
133133
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
134134
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
135135
public fun equals (Ljava/lang/Object;)Z
136136
public final fun getContentSubtype ()Ljava/lang/String;
137137
public final fun getContentType ()Ljava/lang/String;
138138
public fun hashCode ()I
139-
public fun match (Lio/ktor/http/ContentType;)Z
139+
public final fun match (Lio/ktor/http/ContentType;)Z
140140
public final fun match (Ljava/lang/String;)Z
141141
public final fun withParameter (Ljava/lang/String;Ljava/lang/String;)Lio/ktor/http/ContentType;
142142
public final fun withoutParameters ()Lio/ktor/http/ContentType;
@@ -229,7 +229,7 @@ public final class io/ktor/http/ContentType$Video {
229229
}
230230

231231
public abstract interface class io/ktor/http/ContentTypeMatcher {
232-
public abstract fun match (Lio/ktor/http/ContentType;)Z
232+
public abstract fun contains (Lio/ktor/http/ContentType;)Z
233233
}
234234

235235
public final class io/ktor/http/ContentTypesKt {

ktor-http/common/src/io/ktor/http/ContentTypeMatcher.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ interface ContentTypeMatcher {
1111
/**
1212
* Checks if `this` type matches a [contentType] type.
1313
*/
14-
fun match(contentType: ContentType): Boolean
14+
fun contains(contentType: ContentType): Boolean
1515
}

0 commit comments

Comments
 (0)