[kotlin][client] fix encoding of individual parts of a multipart request (#11911)

* [kotlin][client] fix encoding (and Content-Type headers) of individual parts of a multipart request

* [kotlin][client] fix incorrect handling of binary downloads

* [kotlin][client] update samples
This commit is contained in:
Anton Koscejev
2022-04-13 19:52:01 +02:00
committed by GitHub
parent 498ba58881
commit 7851dfe148
91 changed files with 855 additions and 690 deletions

View File

@@ -27,6 +27,7 @@ src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt
src/main/kotlin/org/openapitools/client/infrastructure/LocalDateAdapter.kt
src/main/kotlin/org/openapitools/client/infrastructure/LocalDateTimeAdapter.kt
src/main/kotlin/org/openapitools/client/infrastructure/OffsetDateTimeAdapter.kt
src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt
src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt
src/main/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt
src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt

View File

@@ -34,6 +34,7 @@ import org.openapitools.client.infrastructure.ClientError
import org.openapitools.client.infrastructure.ServerException
import org.openapitools.client.infrastructure.ServerError
import org.openapitools.client.infrastructure.MultiValueMap
import org.openapitools.client.infrastructure.PartConfig
import org.openapitools.client.infrastructure.RequestConfig
import org.openapitools.client.infrastructure.RequestMethod
import org.openapitools.client.infrastructure.ResponseType
@@ -532,7 +533,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
fun updatePetWithFormWithHttpInfo(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : ApiResponse<Unit?> {
val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status)
return request<Map<String, Any?>, Unit>(
return request<Map<String, PartConfig<*>>, Unit>(
localVariableConfig
)
}
@@ -545,8 +546,10 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
* @param status Updated status of the pet (optional)
* @return RequestConfig
*/
fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig<Map<String, Any?>> {
val localVariableBody = mapOf("name" to name, "status" to status)
fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig<Map<String, PartConfig<*>>> {
val localVariableBody = mapOf(
"name" to PartConfig(body = name, headers = mutableMapOf()),
"status" to PartConfig(body = status, headers = mutableMapOf()),)
val localVariableQuery: MultiValueMap = mutableMapOf()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded")
@@ -607,7 +610,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
fun uploadFileWithHttpInfo(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse<ModelApiResponse?> {
val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file)
return request<Map<String, Any?>, ModelApiResponse>(
return request<Map<String, PartConfig<*>>, ModelApiResponse>(
localVariableConfig
)
}
@@ -620,8 +623,10 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
* @param file file to upload (optional)
* @return RequestConfig
*/
fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig<Map<String, Any?>> {
val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file)
fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig<Map<String, PartConfig<*>>> {
val localVariableBody = mapOf(
"additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()),
"file" to PartConfig(body = file, headers = mutableMapOf()),)
val localVariableQuery: MultiValueMap = mutableMapOf()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "multipart/form-data")
localVariableHeaders["Accept"] = "application/json"

View File

@@ -33,6 +33,7 @@ import org.openapitools.client.infrastructure.ClientError
import org.openapitools.client.infrastructure.ServerException
import org.openapitools.client.infrastructure.ServerError
import org.openapitools.client.infrastructure.MultiValueMap
import org.openapitools.client.infrastructure.PartConfig
import org.openapitools.client.infrastructure.RequestConfig
import org.openapitools.client.infrastructure.RequestMethod
import org.openapitools.client.infrastructure.ResponseType

View File

@@ -33,6 +33,7 @@ import org.openapitools.client.infrastructure.ClientError
import org.openapitools.client.infrastructure.ServerException
import org.openapitools.client.infrastructure.ServerError
import org.openapitools.client.infrastructure.MultiValueMap
import org.openapitools.client.infrastructure.PartConfig
import org.openapitools.client.infrastructure.RequestConfig
import org.openapitools.client.infrastructure.RequestMethod
import org.openapitools.client.infrastructure.ResponseType

View File

@@ -10,6 +10,7 @@ import okhttp3.ResponseBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.MultipartBody
import okhttp3.Call
import okhttp3.Callback
@@ -65,53 +66,46 @@ open class ApiClient(val baseUrl: String) {
return contentType ?: "application/octet-stream"
}
protected inline fun <reified T> requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is File -> content.asRequestBody(mediaType.toMediaTypeOrNull())
mediaType == FormDataMediaType -> {
mediaType == FormDataMediaType ->
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.apply {
// content's type *must* be Map<String, Any?>
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, Any?>).forEach { (key, value) ->
if (value is File) {
val partHeaders = Headers.headersOf(
"Content-Disposition",
"form-data; name=\"$key\"; filename=\"${value.name}\""
)
val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
addPart(partHeaders, value.asRequestBody(fileMediaType))
} else {
val partHeaders = Headers.headersOf(
"Content-Disposition",
"form-data; name=\"$key\""
)
addPart(
partHeaders,
parameterToString(value).toRequestBody(null)
)
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
val contentType = part.headers.remove("Content-Type")
val bodies = if (part.body is Iterable<*>) part.body else listOf(part.body)
bodies.forEach { body ->
val headers = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"" + if (body is File) "; filename=\"${body.name}\"" else "")
addPart(headers.toHeaders(),
requestSingleBody(body, contentType))
}
}
}.build()
}
else -> requestSingleBody(content, mediaType)
}
protected inline fun <reified T> requestSingleBody(content: T, mediaType: String?): RequestBody =
when {
content is File -> content.asRequestBody((mediaType ?: guessContentTypeFromFile(content)).toMediaTypeOrNull())
mediaType == FormUrlEncMediaType -> {
FormBody.Builder().apply {
// content's type *must* be Map<String, Any?>
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, Any?>).forEach { (key, value) ->
add(key, parameterToString(value))
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
add(name, parameterToString(part.body))
}
}.build()
}
mediaType.startsWith("application/") && mediaType.endsWith("json") ->
mediaType == null || mediaType.startsWith("application/") && mediaType.endsWith("json") ->
if (content == null) {
EMPTY_REQUEST
} else {
Serializer.moshi.adapter(T::class.java).toJson(content)
.toRequestBody(
mediaType.toMediaTypeOrNull()
)
.toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull())
}
mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.")
// TODO: this should be extended with other serializers
@@ -123,22 +117,20 @@ open class ApiClient(val baseUrl: String) {
if(body == null) {
return null
}
val bodyContent = body.string()
if (bodyContent.isEmpty()) {
return null
}
if (T::class.java == File::class.java) {
// return tempfile
// Attention: if you are developing an android app that supports API Level 25 and bellow, please check flag supportAndroidApiLevel25AndBelow in https://openapi-generator.tech/docs/generators/kotlin#config-options
val f = java.nio.file.Files.createTempFile("tmp.org.openapitools.client", null).toFile()
f.deleteOnExit()
val out = BufferedWriter(FileWriter(f))
out.write(bodyContent)
out.close()
body.byteStream().use { java.nio.file.Files.copy(it, f.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) }
return f as T
}
val bodyContent = body.string()
if (bodyContent.isEmpty()) {
return null
}
return when {
mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) ->
mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) ->
Serializer.moshi.adapter<T>().fromJson(bodyContent)
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.")
}

View File

@@ -0,0 +1,11 @@
package org.openapitools.client.infrastructure
/**
* Defines a config object for a given part of a multi-part request.
* NOTE: Headers is a Map<String,String> because rfc2616 defines
* multi-valued headers as csv-only.
*/
data class PartConfig<T>(
val headers: MutableMap<String, String> = mutableMapOf(),
val body: T? = null
)