Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ private void processMultiplatformLibrary(final String infrastructureFolder) {
private void commonJvmMultiplatformSupportingFiles(String infrastructureFolder) {
supportingFiles.add(new SupportingFile("infrastructure/ApiClient.kt.mustache", infrastructureFolder, "ApiClient.kt"));
supportingFiles.add(new SupportingFile("infrastructure/ApiAbstractions.kt.mustache", infrastructureFolder, "ApiAbstractions.kt"));
supportingFiles.add(new SupportingFile("infrastructure/PartConfig.kt.mustache", infrastructureFolder, "PartConfig.kt"));
supportingFiles.add(new SupportingFile("infrastructure/RequestConfig.kt.mustache", infrastructureFolder, "RequestConfig.kt"));
supportingFiles.add(new SupportingFile("infrastructure/RequestMethod.kt.mustache", infrastructureFolder, "RequestMethod.kt"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package {{packageName}}.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.
*/
{{#nonPublicApi}}internal {{/nonPublicApi}}data class PartConfig<T>(
val headers: MutableMap<String, String> = mutableMapOf(),
val body: T? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {{packageName}}.infrastructure.ClientError
import {{packageName}}.infrastructure.ServerException
import {{packageName}}.infrastructure.ServerError
import {{packageName}}.infrastructure.MultiValueMap
import {{packageName}}.infrastructure.PartConfig
import {{packageName}}.infrastructure.RequestConfig
import {{packageName}}.infrastructure.RequestMethod
import {{packageName}}.infrastructure.ResponseType
Expand Down Expand Up @@ -169,7 +170,7 @@ import {{packageName}}.infrastructure.toMultiValue
{{/isDeprecated}}
val localVariableConfig = {{operationId}}RequestConfig({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})

return{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}@withContext{{/useCoroutines}}{{/doNotUseRxAndCoroutines}} request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, Any?>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
return{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}@withContext{{/useCoroutines}}{{/doNotUseRxAndCoroutines}} request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, PartConfig<*>>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
localVariableConfig
)
}
Expand All @@ -183,8 +184,14 @@ import {{packageName}}.infrastructure.toMultiValue
{{#isDeprecated}}
@Deprecated(message = "This operation is deprecated.")
{{/isDeprecated}}
fun {{operationId}}RequestConfig({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}_{{operationId}}>{{/isContainer}}{{^isContainer}}{{enumName}}_{{operationId}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : RequestConfig<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, Any?>{{/hasFormParams}}{{/hasBodyParam}}> {
val localVariableBody = {{#hasBodyParam}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to {{{paramName}}}{{^-last}}, {{/-last}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
fun {{operationId}}RequestConfig({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}_{{operationId}}>{{/isContainer}}{{^isContainer}}{{enumName}}_{{operationId}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : RequestConfig<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, PartConfig<*>>{{/hasFormParams}}{{/hasBodyParam}}> {
val localVariableBody = {{#hasBodyParam}}{{!
}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{!
}}{{^hasFormParams}}null{{/hasFormParams}}{{!
}}{{#hasFormParams}}mapOf({{#formParams}}
"{{{baseName}}}" to PartConfig(body = {{{paramName}}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
}}{{/formParams}}){{/hasFormParams}}{{!
}}{{/hasBodyParam}}
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf()
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
{{/jvm-okhttp4}}
import okhttp3.Request
import okhttp3.Headers
{{#jvm-okhttp4}}
import okhttp3.Headers.Companion.toHeaders
{{/jvm-okhttp4}}
import okhttp3.MultipartBody
import okhttp3.Call
import okhttp3.Callback
Expand Down Expand Up @@ -111,78 +114,62 @@ import com.squareup.moshi.adapter
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 {
{{#jvm-okhttp3}}
content is File -> RequestBody.create(MediaType.parse(mediaType), content)
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
content is File -> content.asRequestBody(mediaType.toMediaTypeOrNull())
{{/jvm-okhttp4}}
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.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
"Content-Disposition",
"form-data; name=\"$key\"; filename=\"${value.name}\""
)
{{#jvm-okhttp3}}
val fileMediaType = MediaType.parse(guessContentTypeFromFile(value))
addPart(partHeaders, RequestBody.create(fileMediaType, value))
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
addPart(partHeaders, value.asRequestBody(fileMediaType))
{{/jvm-okhttp4}}
} else {
val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
"Content-Disposition",
"form-data; name=\"$key\""
)
addPart(
partHeaders,
{{#jvm-okhttp3}}
RequestBody.create(null, parameterToString(value))
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
parameterToString(value).toRequestBody(null)
{{/jvm-okhttp4}}
)
(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({{#jvm-okhttp3}}Headers.of(headers){{/jvm-okhttp3}}{{#jvm-okhttp4}}headers.toHeaders(){{/jvm-okhttp4}},
requestSingleBody(body, contentType))
}
}
}.build()
}
else -> requestSingleBody(content, mediaType)
}

protected inline fun <reified T> requestSingleBody(content: T, mediaType: String?): RequestBody =
when {
{{#jvm-okhttp3}}
content is File -> RequestBody.create(MediaType.parse(mediaType ?: guessContentTypeFromFile(content)), content)
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
content is File -> content.asRequestBody((mediaType ?: guessContentTypeFromFile(content)).toMediaTypeOrNull())
{{/jvm-okhttp4}}
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") ->
{{#jvm-okhttp3}}
if (content == null) {
EMPTY_REQUEST
} else {
RequestBody.create(
{{#moshi}}
MediaType.parse(mediaType), Serializer.moshi.adapter(T::class.java).toJson(content)
MediaType.parse(mediaType ?: JsonMediaType), Serializer.moshi.adapter(T::class.java).toJson(content)
{{/moshi}}
{{#gson}}
MediaType.parse(mediaType), Serializer.gson.toJson(content, T::class.java)
MediaType.parse(mediaType ?: JsonMediaType), Serializer.gson.toJson(content, T::class.java)
{{/gson}}
{{#jackson}}
MediaType.parse(mediaType), Serializer.jacksonObjectMapper.writeValueAsString(content)
MediaType.parse(mediaType ?: JsonMediaType), Serializer.jacksonObjectMapper.writeValueAsString(content)
{{/jackson}}
{{#kotlinx_serialization}}
MediaType.parse(mediaType), Serializer.jvmJson.encodeToString(content)
MediaType.parse(mediaType ?: JsonMediaType), Serializer.jvmJson.encodeToString(content)
{{/kotlinx_serialization}}
)
}
Expand All @@ -203,9 +190,7 @@ import com.squareup.moshi.adapter
{{#kotlinx_serialization}}
Serializer.jvmJson.encodeToString(content)
{{/kotlinx_serialization}}
.toRequestBody(
mediaType.toMediaTypeOrNull()
)
.toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull())
}
{{/jvm-okhttp4}}
mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.")
Expand All @@ -220,10 +205,6 @@ import com.squareup.moshi.adapter
if(body == null) {
return null
}
val bodyContent = body.string()
if (bodyContent.isEmpty()) {
return null
}
if (T::class.java == File::class.java) {
// return tempfile
{{^supportAndroidApiLevel25AndBelow}}
Expand All @@ -239,14 +220,19 @@ import com.squareup.moshi.adapter
}
{{/supportAndroidApiLevel25AndBelow}}
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")) ->
{{#moshi}}Serializer.moshi.adapter<T>().fromJson(bodyContent){{/moshi}}{{#gson}}Serializer.gson.fromJson(bodyContent, (object: TypeToken<T>(){}).getType()){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, object: TypeReference<T>() {}){{/jackson}}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString<T>(bodyContent){{/kotlinx_serialization}}
mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) ->
{{#moshi}}Serializer.moshi.adapter<T>().fromJson(bodyContent){{/moshi}}{{!
}}{{#gson}}Serializer.gson.fromJson(bodyContent, (object: TypeToken<T>(){}).getType()){{/gson}}{{!
}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, object: TypeReference<T>() {}){{/jackson}}{{!
}}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString<T>(bodyContent){{/kotlinx_serialization}}
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ public void testBuiltinLibraryTemplates() throws IOException {

List<File> files = generator.opts(clientOptInput).generate();

Assert.assertEquals(files.size(), 26);
Assert.assertEquals(files.size(), 27);

// Generator should report a library templated file as a generated file
TestUtils.ensureContainsFile(files, output, "src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt");
Expand Down Expand Up @@ -502,7 +502,7 @@ public void testBuiltinNonLibraryTemplates() throws IOException {

List<File> files = generator.opts(clientOptInput).generate();

Assert.assertEquals(files.size(), 26);
Assert.assertEquals(files.size(), 27);

// Generator should report README.md as a generated file
TestUtils.ensureContainsFile(files, output, "README.md");
Expand Down Expand Up @@ -567,7 +567,7 @@ public void testCustomLibraryTemplates() throws IOException {

List<File> files = generator.opts(clientOptInput).generate();

Assert.assertEquals(files.size(), 26);
Assert.assertEquals(files.size(), 27);

// Generator should report a library templated file as a generated file
TestUtils.ensureContainsFile(files, output, "src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt");
Expand Down Expand Up @@ -621,7 +621,7 @@ public void testCustomNonLibraryTemplates() throws IOException {

List<File> files = generator.opts(clientOptInput).generate();

Assert.assertEquals(files.size(), 26);
Assert.assertEquals(files.size(), 27);

// Generator should report README.md as a generated file
TestUtils.ensureContainsFile(files, output, "README.md");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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
Expand Down
Loading