Skip to content

Commit 71ccc88

Browse files
authored
[Kotlin] fix #20231, OkHttp client can handle a field with a list of files (#20232)
* feat(issue-20231): Kotlin okhttp client handles correctly fields that are optional with multiple files. * docs(issue-20231): add docstrings * feat(issue-20231): Remove unnecessary test spec
1 parent 0183620 commit 71ccc88

File tree

22 files changed

+1188
-330
lines changed
  • modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure
  • samples/client
    • others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure
    • petstore
      • kotlin-allOff-discriminator/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin/src/main/kotlin/org/openapitools/client/infrastructure

22 files changed

+1188
-330
lines changed

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,48 @@ import com.squareup.moshi.adapter
113113
return contentType ?: "application/octet-stream"
114114
}
115115

116+
/**
117+
* Adds a File to a MultipartBody.Builder
118+
* Defined a helper in the requestBody method to not duplicate code
119+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
120+
*
121+
* @param name The field name to add in the request
122+
* @param headers The headers that are in the PartConfig
123+
* @param file The file that will be added as the field value
124+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
125+
* @see requestBody
126+
*/
127+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
128+
val partHeaders = headers.toMutableMap() +
129+
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
130+
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
131+
addPart(
132+
partHeaders.toHeaders(),
133+
file.asRequestBody(fileMediaType)
134+
)
135+
}
136+
137+
/**
138+
* Adds any type to a MultipartBody.Builder
139+
* Defined a helper in the requestBody method to not duplicate code
140+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
141+
*
142+
* @param name The field name to add in the request
143+
* @param headers The headers that are in the PartConfig
144+
* @param obj The field name to add in the request
145+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
146+
* @see requestBody
147+
*/
148+
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
149+
if (obj == null) return
150+
val partHeaders = headers.toMutableMap() +
151+
("Content-Disposition" to "form-data; name=\"$name\"")
152+
addPart(
153+
partHeaders.toHeaders(),
154+
parameterToString(obj).toRequestBody(null)
155+
)
156+
}
157+
116158
protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
117159
when {
118160
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
@@ -124,21 +166,18 @@ import com.squareup.moshi.adapter
124166
// content's type *must* be Map<String, PartConfig<*>>
125167
@Suppress("UNCHECKED_CAST")
126168
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
127-
if (part.body is File) {
128-
val partHeaders = part.headers.toMutableMap() +
129-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
130-
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
131-
addPart(
132-
partHeaders.toHeaders(),
133-
part.body.asRequestBody(fileMediaType)
134-
)
135-
} else {
136-
val partHeaders = part.headers.toMutableMap() +
137-
("Content-Disposition" to "form-data; name=\"$name\"")
138-
addPart(
139-
partHeaders.toHeaders(),
140-
parameterToString(part.body).toRequestBody(null)
141-
)
169+
when (part.body) {
170+
is File -> addPartToMultiPart(name, part.headers, part.body)
171+
is List<*> -> {
172+
part.body.forEach {
173+
if (it is File) {
174+
addPartToMultiPart(name, part.headers, it)
175+
} else {
176+
addPartToMultiPart(name, part.headers, it)
177+
}
178+
}
179+
}
180+
else -> addPartToMultiPart(name, part.headers, part.body)
142181
}
143182
}
144183
}.build()

samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,48 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8484
return contentType ?: "application/octet-stream"
8585
}
8686

87+
/**
88+
* Adds a File to a MultipartBody.Builder
89+
* Defined a helper in the requestBody method to not duplicate code
90+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
91+
*
92+
* @param name The field name to add in the request
93+
* @param headers The headers that are in the PartConfig
94+
* @param file The file that will be added as the field value
95+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
96+
* @see requestBody
97+
*/
98+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
99+
val partHeaders = headers.toMutableMap() +
100+
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
101+
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
102+
addPart(
103+
partHeaders.toHeaders(),
104+
file.asRequestBody(fileMediaType)
105+
)
106+
}
107+
108+
/**
109+
* Adds any type to a MultipartBody.Builder
110+
* Defined a helper in the requestBody method to not duplicate code
111+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
112+
*
113+
* @param name The field name to add in the request
114+
* @param headers The headers that are in the PartConfig
115+
* @param obj The field name to add in the request
116+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
117+
* @see requestBody
118+
*/
119+
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
120+
if (obj == null) return
121+
val partHeaders = headers.toMutableMap() +
122+
("Content-Disposition" to "form-data; name=\"$name\"")
123+
addPart(
124+
partHeaders.toHeaders(),
125+
parameterToString(obj).toRequestBody(null)
126+
)
127+
}
128+
87129
protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
88130
when {
89131
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
@@ -95,21 +137,18 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
95137
// content's type *must* be Map<String, PartConfig<*>>
96138
@Suppress("UNCHECKED_CAST")
97139
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
98-
if (part.body is File) {
99-
val partHeaders = part.headers.toMutableMap() +
100-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
101-
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
102-
addPart(
103-
partHeaders.toHeaders(),
104-
part.body.asRequestBody(fileMediaType)
105-
)
106-
} else {
107-
val partHeaders = part.headers.toMutableMap() +
108-
("Content-Disposition" to "form-data; name=\"$name\"")
109-
addPart(
110-
partHeaders.toHeaders(),
111-
parameterToString(part.body).toRequestBody(null)
112-
)
140+
when (part.body) {
141+
is File -> addPartToMultiPart(name, part.headers, part.body)
142+
is List<*> -> {
143+
part.body.forEach {
144+
if (it is File) {
145+
addPartToMultiPart(name, part.headers, it)
146+
} else {
147+
addPartToMultiPart(name, part.headers, it)
148+
}
149+
}
150+
}
151+
else -> addPartToMultiPart(name, part.headers, part.body)
113152
}
114153
}
115154
}.build()

samples/client/petstore/kotlin-allOff-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,48 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8484
return contentType ?: "application/octet-stream"
8585
}
8686

87+
/**
88+
* Adds a File to a MultipartBody.Builder
89+
* Defined a helper in the requestBody method to not duplicate code
90+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
91+
*
92+
* @param name The field name to add in the request
93+
* @param headers The headers that are in the PartConfig
94+
* @param file The file that will be added as the field value
95+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
96+
* @see requestBody
97+
*/
98+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
99+
val partHeaders = headers.toMutableMap() +
100+
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
101+
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
102+
addPart(
103+
partHeaders.toHeaders(),
104+
file.asRequestBody(fileMediaType)
105+
)
106+
}
107+
108+
/**
109+
* Adds any type to a MultipartBody.Builder
110+
* Defined a helper in the requestBody method to not duplicate code
111+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
112+
*
113+
* @param name The field name to add in the request
114+
* @param headers The headers that are in the PartConfig
115+
* @param obj The field name to add in the request
116+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
117+
* @see requestBody
118+
*/
119+
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
120+
if (obj == null) return
121+
val partHeaders = headers.toMutableMap() +
122+
("Content-Disposition" to "form-data; name=\"$name\"")
123+
addPart(
124+
partHeaders.toHeaders(),
125+
parameterToString(obj).toRequestBody(null)
126+
)
127+
}
128+
87129
protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
88130
when {
89131
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
@@ -95,21 +137,18 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
95137
// content's type *must* be Map<String, PartConfig<*>>
96138
@Suppress("UNCHECKED_CAST")
97139
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
98-
if (part.body is File) {
99-
val partHeaders = part.headers.toMutableMap() +
100-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
101-
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
102-
addPart(
103-
partHeaders.toHeaders(),
104-
part.body.asRequestBody(fileMediaType)
105-
)
106-
} else {
107-
val partHeaders = part.headers.toMutableMap() +
108-
("Content-Disposition" to "form-data; name=\"$name\"")
109-
addPart(
110-
partHeaders.toHeaders(),
111-
parameterToString(part.body).toRequestBody(null)
112-
)
140+
when (part.body) {
141+
is File -> addPartToMultiPart(name, part.headers, part.body)
142+
is List<*> -> {
143+
part.body.forEach {
144+
if (it is File) {
145+
addPartToMultiPart(name, part.headers, it)
146+
} else {
147+
addPartToMultiPart(name, part.headers, it)
148+
}
149+
}
150+
}
151+
else -> addPartToMultiPart(name, part.headers, part.body)
113152
}
114153
}
115154
}.build()

samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,48 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8484
return contentType ?: "application/octet-stream"
8585
}
8686

87+
/**
88+
* Adds a File to a MultipartBody.Builder
89+
* Defined a helper in the requestBody method to not duplicate code
90+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
91+
*
92+
* @param name The field name to add in the request
93+
* @param headers The headers that are in the PartConfig
94+
* @param file The file that will be added as the field value
95+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
96+
* @see requestBody
97+
*/
98+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
99+
val partHeaders = headers.toMutableMap() +
100+
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
101+
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
102+
addPart(
103+
partHeaders.toHeaders(),
104+
file.asRequestBody(fileMediaType)
105+
)
106+
}
107+
108+
/**
109+
* Adds any type to a MultipartBody.Builder
110+
* Defined a helper in the requestBody method to not duplicate code
111+
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
112+
*
113+
* @param name The field name to add in the request
114+
* @param headers The headers that are in the PartConfig
115+
* @param obj The field name to add in the request
116+
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
117+
* @see requestBody
118+
*/
119+
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
120+
if (obj == null) return
121+
val partHeaders = headers.toMutableMap() +
122+
("Content-Disposition" to "form-data; name=\"$name\"")
123+
addPart(
124+
partHeaders.toHeaders(),
125+
parameterToString(obj).toRequestBody(null)
126+
)
127+
}
128+
87129
protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
88130
when {
89131
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
@@ -95,21 +137,18 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
95137
// content's type *must* be Map<String, PartConfig<*>>
96138
@Suppress("UNCHECKED_CAST")
97139
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
98-
if (part.body is File) {
99-
val partHeaders = part.headers.toMutableMap() +
100-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
101-
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
102-
addPart(
103-
partHeaders.toHeaders(),
104-
part.body.asRequestBody(fileMediaType)
105-
)
106-
} else {
107-
val partHeaders = part.headers.toMutableMap() +
108-
("Content-Disposition" to "form-data; name=\"$name\"")
109-
addPart(
110-
partHeaders.toHeaders(),
111-
parameterToString(part.body).toRequestBody(null)
112-
)
140+
when (part.body) {
141+
is File -> addPartToMultiPart(name, part.headers, part.body)
142+
is List<*> -> {
143+
part.body.forEach {
144+
if (it is File) {
145+
addPartToMultiPart(name, part.headers, it)
146+
} else {
147+
addPartToMultiPart(name, part.headers, it)
148+
}
149+
}
150+
}
151+
else -> addPartToMultiPart(name, part.headers, part.body)
113152
}
114153
}
115154
}.build()

0 commit comments

Comments
 (0)