diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index ce65a6bcd85f..ae3b7d17d849 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -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")); } diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache new file mode 100644 index 000000000000..f70c18eff465 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +{{#nonPublicApi}}internal {{/nonPublicApi}}data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache index 1616e309ff1a..6f67117e6cdb 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache @@ -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 @@ -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{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>( + return{{^doNotUseRxAndCoroutines}}{{#useCoroutines}}@withContext{{/useCoroutines}}{{/doNotUseRxAndCoroutines}} request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>( localVariableConfig ) } @@ -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{{/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>{{/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>() .apply { diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache index c3504b36f697..a4079e73dcb9 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache @@ -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 @@ -111,78 +114,62 @@ import com.squareup.moshi.adapter return contentType ?: "application/octet-stream" } - protected inline fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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}} ) } @@ -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.") @@ -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}} @@ -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().fromJson(bodyContent){{/moshi}}{{#gson}}Serializer.gson.fromJson(bodyContent, (object: TypeToken(){}).getType()){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, object: TypeReference() {}){{/jackson}}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString(bodyContent){{/kotlinx_serialization}} + mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) -> + {{#moshi}}Serializer.moshi.adapter().fromJson(bodyContent){{/moshi}}{{! + }}{{#gson}}Serializer.gson.fromJson(bodyContent, (object: TypeToken(){}).getType()){{/gson}}{{! + }}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, object: TypeReference() {}){{/jackson}}{{! + }}{{#kotlinx_serialization}}Serializer.jvmJson.decodeFromString(bodyContent){{/kotlinx_serialization}} else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java index d2ae77e2190e..75c7388f27e9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java @@ -460,7 +460,7 @@ public void testBuiltinLibraryTemplates() throws IOException { List 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"); @@ -502,7 +502,7 @@ public void testBuiltinNonLibraryTemplates() throws IOException { List 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"); @@ -567,7 +567,7 @@ public void testCustomLibraryTemplates() throws IOException { List 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"); @@ -621,7 +621,7 @@ public void testCustomNonLibraryTemplates() throws IOException { List 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"); diff --git a/samples/client/petstore/kotlin-array-simple-string/.openapi-generator/FILES b/samples/client/petstore/kotlin-array-simple-string/.openapi-generator/FILES index d731beb9d87a..2d00928eb930 100644 --- a/samples/client/petstore/kotlin-array-simple-string/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-array-simple-string/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt b/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt index 3a6862048908..40a4e4707f15 100644 --- a/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index f7ef39a50b30..c3639b96e1cf 100644 --- a/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-array-simple-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-enum-default-value/.openapi-generator/FILES b/samples/client/petstore/kotlin-enum-default-value/.openapi-generator/FILES index edff1ccea7b5..23e025a3bbac 100644 --- a/samples/client/petstore/kotlin-enum-default-value/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-enum-default-value/.openapi-generator/FILES @@ -18,6 +18,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 diff --git a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt index 3925a4181e8a..413b8d2a5a6f 100644 --- a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index f7ef39a50b30..c3639b96e1cf 100644 --- a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-gson/.openapi-generator/FILES b/samples/client/petstore/kotlin-gson/.openapi-generator/FILES index 4bfcf9fb54e9..11e6ffb28906 100644 --- a/samples/client/petstore/kotlin-gson/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-gson/.openapi-generator/FILES @@ -25,6 +25,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 diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 54b52a811860..820ebed32c29 100644 --- a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 5dff82cca137..3f3533cb6bfe 100644 --- a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 3cd3a17289b9..c9ccc491c909 100644 --- a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3db7de855d64..cf6463edfe28 100644 --- a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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.gson.toJson(content, T::class.java) - .toRequestBody( - mediaType.toMediaTypeOrNull() - ) + .toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull()) } mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.") // TODO: this should be extended with other serializers @@ -122,22 +116,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.gson.fromJson(bodyContent, (object: TypeToken(){}).getType()) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-jackson/.openapi-generator/FILES b/samples/client/petstore/kotlin-jackson/.openapi-generator/FILES index 26ee35abac5c..34bf95b7ef48 100644 --- a/samples/client/petstore/kotlin-jackson/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-jackson/.openapi-generator/FILES @@ -22,6 +22,7 @@ src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt src/main/kotlin/org/openapitools/client/infrastructure/ApiResponse.kt src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt src/main/kotlin/org/openapitools/client/infrastructure/Errors.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 diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index b07f6fb114f3..58dd195b591d 100644 --- a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 5f5f7ecffe81..f749c070ff81 100644 --- a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 332503b18953..0e91b2e1897b 100644 --- a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 2d8824444973..245c4b723808 100644 --- a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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.jacksonObjectMapper.writeValueAsString(content) - .toRequestBody( - mediaType.toMediaTypeOrNull() - ) + .toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull()) } mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.") // TODO: this should be extended with other serializers @@ -122,22 +116,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.jacksonObjectMapper.readValue(bodyContent, object: TypeReference() {}) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-json-request-string/.openapi-generator/FILES b/samples/client/petstore/kotlin-json-request-string/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-json-request-string/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-json-request-string/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 5c62ed5b98f7..5a758ad7528c 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 @@ -524,7 +525,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) { fun updatePetWithFormWithHttpInfo(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : ApiResponse { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, Unit>( localVariableConfig ) } @@ -537,8 +538,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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded") @@ -599,7 +602,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) { fun uploadFileWithHttpInfo(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, ModelApiResponse>( localVariableConfig ) } @@ -612,8 +615,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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 916a865f7acc..72b1bb3fc5ae 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 1c294b5e2215..66aba7a248a6 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 2e95b6cbadca..3fcca4f30fed 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -11,6 +11,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 @@ -66,53 +67,46 @@ open class ApiClient(val baseUrl: String) { return contentType ?: "application/octet-stream" } - protected inline fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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 @@ -124,10 +118,6 @@ 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 val f = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -137,13 +127,15 @@ open class ApiClient(val baseUrl: String) { createTempFile("tmp.net.medicineone.teleconsultationandroid.openapi.openapicommon", null) } 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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/.openapi-generator/FILES b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/.openapi-generator/FILES index 4bfcf9fb54e9..11e6ffb28906 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/.openapi-generator/FILES @@ -25,6 +25,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 diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 6b26a13e51ae..83569408489a 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -36,6 +36,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 @@ -534,7 +535,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) { suspend fun updatePetWithFormWithHttpInfo(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : ApiResponse = withContext(Dispatchers.IO) { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return@withContext request, Unit>( + return@withContext request>, Unit>( localVariableConfig ) } @@ -547,8 +548,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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded") @@ -609,7 +612,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) { suspend fun uploadFileWithHttpInfo(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse = withContext(Dispatchers.IO) { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return@withContext request, ModelApiResponse>( + return@withContext request>, ModelApiResponse>( localVariableConfig ) } @@ -622,8 +625,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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index ffa098db6ca1..3d8dca029049 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -35,6 +35,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 diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 724e2d6fd2e8..86d590568c21 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -35,6 +35,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 diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3377fa7b5348..c8cfe7c0deaa 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 @@ -68,53 +69,46 @@ open class ApiClient(val baseUrl: String) { return contentType ?: "application/octet-stream" } - protected inline fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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.gson.toJson(content, T::class.java) - .toRequestBody( - mediaType.toMediaTypeOrNull() - ) + .toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull()) } mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.") // TODO: this should be extended with other serializers @@ -125,22 +119,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.gson.fromJson(bodyContent, (object: TypeToken(){}).getType()) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-modelMutable/.openapi-generator/FILES b/samples/client/petstore/kotlin-modelMutable/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-modelMutable/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-modelMutable/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 24c2ccc75bdd..f7e870bd019e 100644 --- a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 7a773bb79ef1..d3523bd0a6df 100644 --- a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index eac1ec32436e..705e4364456c 100644 --- a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3868a5465d33..1cce280abfe5 100644 --- a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-moshi-codegen/.openapi-generator/FILES b/samples/client/petstore/kotlin-moshi-codegen/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-moshi-codegen/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 601ca6b01f88..10c06c849f7e 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 916a865f7acc..72b1bb3fc5ae 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 1c294b5e2215..66aba7a248a6 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3868a5465d33..1cce280abfe5 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-multiplatform/.openapi-generator/FILES b/samples/client/petstore/kotlin-multiplatform/.openapi-generator/FILES index c917cc86876d..633537acb582 100644 --- a/samples/client/petstore/kotlin-multiplatform/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-multiplatform/.openapi-generator/FILES @@ -24,6 +24,7 @@ src/commonMain/kotlin/org/openapitools/client/infrastructure/Base64ByteArray.kt src/commonMain/kotlin/org/openapitools/client/infrastructure/Bytes.kt src/commonMain/kotlin/org/openapitools/client/infrastructure/HttpResponse.kt src/commonMain/kotlin/org/openapitools/client/infrastructure/OctetByteArray.kt +src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt src/commonMain/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt src/commonMain/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt src/commonMain/kotlin/org/openapitools/client/models/Category.kt diff --git a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-nonpublic/.openapi-generator/FILES b/samples/client/petstore/kotlin-nonpublic/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-nonpublic/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-nonpublic/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 4eefd44019f8..ea535d24bc38 100644 --- a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 @@ internal class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(bas fun updatePetWithFormWithHttpInfo(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : ApiResponse { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, Unit>( localVariableConfig ) } @@ -545,8 +546,10 @@ internal class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(bas * @param status Updated status of the pet (optional) * @return RequestConfig */ - fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded") @@ -607,7 +610,7 @@ internal class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(bas fun uploadFileWithHttpInfo(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, ModelApiResponse>( localVariableConfig ) } @@ -620,8 +623,10 @@ internal class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(bas * @param file file to upload (optional) * @return RequestConfig */ - fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index e5d64330d94d..efa55a4b1bbc 100644 --- a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 363f3924a001..5ed88f77825e 100644 --- a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 40599b8784a8..69401893569b 100644 --- a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 @@ internal open class ApiClient(val baseUrl: String) { return contentType ?: "application/octet-stream" } - protected inline fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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 @@ internal 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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..2da623bb7079 --- /dev/null +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +internal data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-nullable/.openapi-generator/FILES b/samples/client/petstore/kotlin-nullable/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-nullable/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-nullable/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 9e40a8d6be31..92e75fe253d6 100644 --- a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index a305a6661a0c..31818a851192 100644 --- a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index bc8b00bc288a..e0bdf90a640a 100644 --- a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3868a5465d33..1cce280abfe5 100644 --- a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-okhttp3/.openapi-generator/FILES b/samples/client/petstore/kotlin-okhttp3/.openapi-generator/FILES index 2710ee42441f..1c7bf3702210 100644 --- a/samples/client/petstore/kotlin-okhttp3/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-okhttp3/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 601ca6b01f88..10c06c849f7e 100644 --- a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 916a865f7acc..72b1bb3fc5ae 100644 --- a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 1c294b5e2215..66aba7a248a6 100644 --- a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 0f552c1eb837..a57c9c9931d1 100644 --- a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -63,51 +63,46 @@ open class ApiClient(val baseUrl: String) { return contentType ?: "application/octet-stream" } - protected inline fun requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun requestBody(content: T, mediaType: String?): RequestBody = when { - content is File -> RequestBody.create(MediaType.parse(mediaType), content) - mediaType == FormDataMediaType -> { + mediaType == FormDataMediaType -> MultipartBody.Builder() .setType(MultipartBody.FORM) .apply { - // content's type *must* be Map + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - if (value is File) { - val partHeaders = Headers.of( - "Content-Disposition", - "form-data; name=\"$key\"; filename=\"${value.name}\"" - ) - val fileMediaType = MediaType.parse(guessContentTypeFromFile(value)) - addPart(partHeaders, RequestBody.create(fileMediaType, value)) - } else { - val partHeaders = Headers.of( - "Content-Disposition", - "form-data; name=\"$key\"" - ) - addPart( - partHeaders, - RequestBody.create(null, parameterToString(value)) - ) + (content as Map>).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.of(headers), + requestSingleBody(body, contentType)) } } }.build() - } + else -> requestSingleBody(content, mediaType) + } + + protected inline fun requestSingleBody(content: T, mediaType: String?): RequestBody = + when { + content is File -> RequestBody.create(MediaType.parse(mediaType ?: guessContentTypeFromFile(content)), content) mediaType == FormUrlEncMediaType -> { FormBody.Builder().apply { - // content's type *must* be Map + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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 { RequestBody.create( - MediaType.parse(mediaType), Serializer.moshi.adapter(T::class.java).toJson(content) + MediaType.parse(mediaType ?: JsonMediaType), Serializer.moshi.adapter(T::class.java).toJson(content) ) } mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.") @@ -120,22 +115,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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-okhttp3/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-string/.openapi-generator/FILES b/samples/client/petstore/kotlin-string/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-string/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-string/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 870811a2c93a..506b6e727ec9 100644 --- a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 916a865f7acc..72b1bb3fc5ae 100644 --- a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 1c294b5e2215..66aba7a248a6 100644 --- a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3868a5465d33..1cce280abfe5 100644 --- a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-threetenbp/.openapi-generator/FILES b/samples/client/petstore/kotlin-threetenbp/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin-threetenbp/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-threetenbp/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 601ca6b01f88..10c06c849f7e 100644 --- a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 916a865f7acc..72b1bb3fc5ae 100644 --- a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 1c294b5e2215..66aba7a248a6 100644 --- a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 76937e094ad3..04b43a81c66e 100644 --- a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin-uppercase-enum/.openapi-generator/FILES b/samples/client/petstore/kotlin-uppercase-enum/.openapi-generator/FILES index 113f981ccd51..9a212390c6b9 100644 --- a/samples/client/petstore/kotlin-uppercase-enum/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin-uppercase-enum/.openapi-generator/FILES @@ -18,6 +18,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 diff --git a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt index 325c66305b88..eb501fcd5220 100644 --- a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt +++ b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index f7ef39a50b30..c3639b96e1cf 100644 --- a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/petstore/kotlin/.openapi-generator/FILES b/samples/client/petstore/kotlin/.openapi-generator/FILES index c7a409ac169e..ab27444a312c 100644 --- a/samples/client/petstore/kotlin/.openapi-generator/FILES +++ b/samples/client/petstore/kotlin/.openapi-generator/FILES @@ -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 diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 601ca6b01f88..10c06c849f7e 100644 --- a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -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 { val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status) - return request, Unit>( + return request>, 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> { - val localVariableBody = mapOf("name" to name, "status" to status) + fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig>> { + val localVariableBody = mapOf( + "name" to PartConfig(body = name, headers = mutableMapOf()), + "status" to PartConfig(body = status, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = 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 { val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file) - return request, ModelApiResponse>( + return request>, 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> { - val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file) + fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig>> { + val localVariableBody = mapOf( + "additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()), + "file" to PartConfig(body = file, headers = mutableMapOf()),) val localVariableQuery: MultiValueMap = mutableMapOf() val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") localVariableHeaders["Accept"] = "application/json" diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 916a865f7acc..72b1bb3fc5ae 100644 --- a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index 1c294b5e2215..66aba7a248a6 100644 --- a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -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 diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 3868a5465d33..1cce280abfe5 100644 --- a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -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 requestBody(content: T, mediaType: String = JsonMediaType): RequestBody = + protected inline fun 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).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>).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 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 + // content's type *must* be Map> @Suppress("UNCHECKED_CAST") - (content as Map).forEach { (key, value) -> - add(key, parameterToString(value)) + (content as Map>).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().fromJson(bodyContent) else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.") } diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -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 because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +)