@@ -6,14 +6,12 @@ package io.ktor.client.features.json
6
6
7
7
import io.ktor.client.*
8
8
import io.ktor.client.features.*
9
- import io.ktor.client.features.json.JsonFeature.*
10
9
import io.ktor.client.request.*
11
10
import io.ktor.client.statement.*
12
11
import io.ktor.client.utils.*
13
12
import io.ktor.http.*
14
13
import io.ktor.util.*
15
14
import io.ktor.utils.io.*
16
- import io.ktor.utils.io.core.*
17
15
18
16
19
17
/* *
@@ -28,26 +26,30 @@ expect fun defaultSerializer(): JsonSerializer
28
26
29
27
/* *
30
28
* [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].
32
30
*
33
- * The default [Config. serializer] is [GsonSerializer].
31
+ * The default [serializer] is [GsonSerializer].
34
32
*
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]
37
34
*
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
41
40
*/
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
51
53
)
52
54
53
55
/* *
@@ -59,36 +61,65 @@ class JsonFeature internal constructor(val config: Config) {
59
61
*
60
62
* Default value for [serializer] is [defaultSerializer].
61
63
*/
62
- var serializer: JsonSerializer by lazyVar { defaultSerializer() }
64
+ var serializer: JsonSerializer ? = null
63
65
64
66
/* *
65
67
* Backing field with mutable list of content types that are handled by this feature.
66
68
*/
67
- private val _acceptContentTypes : MutableList <ContentTypeMatcher > =
69
+ private val _acceptContentTypes : MutableList <ContentType > = mutableListOf (ContentType .Application .Json )
70
+ private val _receiveContentTypeMatchers : MutableList <ContentTypeMatcher > =
68
71
mutableListOf (JsonContentTypeMatcher ())
69
72
70
73
/* *
71
74
* List of content types that are handled by this feature.
72
75
* It also affects `Accept` request header value.
73
76
* Please note that wildcard content types are supported but no quality specification provided.
74
77
*/
75
- var acceptContentTypes : List < ContentTypeMatcher >
76
- get() = _acceptContentTypes
78
+ @KtorExperimentalAPI
79
+ var acceptContentTypes : List < ContentType >
77
80
set(value) {
81
+ require(value.isNotEmpty()) { " At least one content type should be provided to acceptContentTypes" }
82
+
78
83
_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)
80
98
}
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
+ }
81
109
82
110
/* *
83
111
* Adds accepted content types. Existing content types will not be removed.
84
112
*/
85
- fun accept (vararg contentTypes : ContentTypeMatcher ) {
86
- val values = _acceptContentTypes .toSet() + contentTypes
87
- acceptContentTypes = values.toList()
113
+ fun receive (matcher : ContentTypeMatcher ) {
114
+ _receiveContentTypeMatchers + = matcher
88
115
}
116
+ }
89
117
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) }
92
123
}
93
124
94
125
/* *
@@ -97,54 +128,47 @@ class JsonFeature internal constructor(val config: Config) {
97
128
companion object Feature : HttpClientFeature<Config, JsonFeature> {
98
129
override val key: AttributeKey <JsonFeature > = AttributeKey (" Json" )
99
130
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
+ }
102
139
103
140
override fun install (feature : JsonFeature , scope : HttpClient ) {
104
- val config = feature.config
105
141
scope.requestPipeline.intercept(HttpRequestPipeline .Transform ) { payload ->
142
+ feature.acceptContentTypes.forEach { context.accept(it) }
143
+
106
144
val contentType = context.contentType() ? : return @intercept
107
- if (! config.matchesContentType(contentType))
108
- return @intercept
145
+ if (! feature.canHandle(contentType)) return @intercept
109
146
110
147
context.headers.remove(HttpHeaders .ContentType )
111
148
112
149
val serializedContent = when (payload) {
113
150
Unit -> EmptyContent
114
151
is EmptyContent -> EmptyContent
115
- else -> config .serializer.write(payload, contentType)
152
+ else -> feature .serializer.write(payload, contentType)
116
153
}
117
154
118
155
proceedWith(serializedContent)
119
156
}
120
157
121
158
scope.responsePipeline.intercept(HttpResponsePipeline .Transform ) { (info, body) ->
122
- if (! config.matchesContentType(context.response.contentType()))
123
- return @intercept
159
+ if (body !is ByteReadChannel ) return @intercept
124
160
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())
131
165
val response = HttpResponseContainer (info, parsedBody)
132
166
proceedWith(response)
133
167
}
134
168
}
135
169
}
136
170
}
137
171
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
-
148
172
/* *
149
173
* Install [JsonFeature].
150
174
*/
0 commit comments