Skip to content

Commit af29043

Browse files
committed
Refactor multipart helpers for reified type parameter support
1 parent 298e7bb commit af29043

28 files changed

Lines changed: 616 additions & 308 deletions

File tree

  • modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure
  • samples/client
    • echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure
    • others
      • kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure
    • petstore
      • kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure
      • kotlin-array-integer-enum/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

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ import com.squareup.moshi.adapter
115115
return contentType ?: "application/octet-stream"
116116
}
117117

118+
/**
119+
* Builds headers for a multipart form-data part.
120+
* OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers.
121+
* This function filters out Content-Type and builds the appropriate Content-Disposition header.
122+
*
123+
* @param name The field name
124+
* @param headers The headers from the PartConfig (may include Content-Type)
125+
* @param filename Optional filename for file uploads
126+
* @return Headers object ready for addPart()
127+
*/
128+
protected fun buildPartHeaders(name: String, headers: Map<String, String>, filename: String? = null): Headers {
129+
val disposition = if (filename != null) {
130+
"form-data; name=\"$name\"; filename=\"$filename\""
131+
} else {
132+
"form-data; name=\"$name\""
133+
}
134+
return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders()
135+
}
136+
118137
/**
119138
* Adds a File to a MultipartBody.Builder
120139
* Defined a helper in the requestBody method to not duplicate code
@@ -127,13 +146,9 @@ import com.squareup.moshi.adapter
127146
* @see requestBody
128147
*/
129148
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
130-
// Filter out Content-Type from headers as OkHttp requires it to be passed
131-
// separately via asRequestBody(mediaType), not in the headers map
132-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
133-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
134149
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
135150
addPart(
136-
partHeaders.toHeaders(),
151+
buildPartHeaders(name, headers, file.name),
137152
file.asRequestBody(fileMediaType)
138153
)
139154
}
@@ -176,16 +191,12 @@ import com.squareup.moshi.adapter
176191
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
177192
* @see requestBody
178193
*/
179-
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
194+
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
180195
val partContentType = headers["Content-Type"]
181196
val partMediaType = partContentType?.toMediaTypeOrNull()
182-
// Filter out Content-Type from headers as OkHttp requires it to be passed
183-
// separately via toRequestBody(mediaType), not in the headers map
184-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
185-
("Content-Disposition" to "form-data; name=\"$name\"")
186197
val partBody = serializePartBody(obj, partContentType)
187198
addPart(
188-
partHeaders.toHeaders(),
199+
buildPartHeaders(name, headers),
189200
partBody.toRequestBody(partMediaType)
190201
)
191202
}

samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8686
return contentType ?: "application/octet-stream"
8787
}
8888

89+
/**
90+
* Builds headers for a multipart form-data part.
91+
* OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers.
92+
* This function filters out Content-Type and builds the appropriate Content-Disposition header.
93+
*
94+
* @param name The field name
95+
* @param headers The headers from the PartConfig (may include Content-Type)
96+
* @param filename Optional filename for file uploads
97+
* @return Headers object ready for addPart()
98+
*/
99+
protected fun buildPartHeaders(name: String, headers: Map<String, String>, filename: String? = null): Headers {
100+
val disposition = if (filename != null) {
101+
"form-data; name=\"$name\"; filename=\"$filename\""
102+
} else {
103+
"form-data; name=\"$name\""
104+
}
105+
return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders()
106+
}
107+
89108
/**
90109
* Adds a File to a MultipartBody.Builder
91110
* Defined a helper in the requestBody method to not duplicate code
@@ -98,13 +117,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
98117
* @see requestBody
99118
*/
100119
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
101-
// Filter out Content-Type from headers as OkHttp requires it to be passed
102-
// separately via asRequestBody(mediaType), not in the headers map
103-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
104-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
105120
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
106121
addPart(
107-
partHeaders.toHeaders(),
122+
buildPartHeaders(name, headers, file.name),
108123
file.asRequestBody(fileMediaType)
109124
)
110125
}
@@ -136,16 +151,12 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
136151
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
137152
* @see requestBody
138153
*/
139-
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
154+
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
140155
val partContentType = headers["Content-Type"]
141156
val partMediaType = partContentType?.toMediaTypeOrNull()
142-
// Filter out Content-Type from headers as OkHttp requires it to be passed
143-
// separately via toRequestBody(mediaType), not in the headers map
144-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
145-
("Content-Disposition" to "form-data; name=\"$name\"")
146157
val partBody = serializePartBody(obj, partContentType)
147158
addPart(
148-
partHeaders.toHeaders(),
159+
buildPartHeaders(name, headers),
149160
partBody.toRequestBody(partMediaType)
150161
)
151162
}

samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8686
return contentType ?: "application/octet-stream"
8787
}
8888

89+
/**
90+
* Builds headers for a multipart form-data part.
91+
* OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers.
92+
* This function filters out Content-Type and builds the appropriate Content-Disposition header.
93+
*
94+
* @param name The field name
95+
* @param headers The headers from the PartConfig (may include Content-Type)
96+
* @param filename Optional filename for file uploads
97+
* @return Headers object ready for addPart()
98+
*/
99+
protected fun buildPartHeaders(name: String, headers: Map<String, String>, filename: String? = null): Headers {
100+
val disposition = if (filename != null) {
101+
"form-data; name=\"$name\"; filename=\"$filename\""
102+
} else {
103+
"form-data; name=\"$name\""
104+
}
105+
return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders()
106+
}
107+
89108
/**
90109
* Adds a File to a MultipartBody.Builder
91110
* Defined a helper in the requestBody method to not duplicate code
@@ -98,13 +117,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
98117
* @see requestBody
99118
*/
100119
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
101-
// Filter out Content-Type from headers as OkHttp requires it to be passed
102-
// separately via asRequestBody(mediaType), not in the headers map
103-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
104-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
105120
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
106121
addPart(
107-
partHeaders.toHeaders(),
122+
buildPartHeaders(name, headers, file.name),
108123
file.asRequestBody(fileMediaType)
109124
)
110125
}
@@ -136,16 +151,12 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
136151
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
137152
* @see requestBody
138153
*/
139-
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
154+
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
140155
val partContentType = headers["Content-Type"]
141156
val partMediaType = partContentType?.toMediaTypeOrNull()
142-
// Filter out Content-Type from headers as OkHttp requires it to be passed
143-
// separately via toRequestBody(mediaType), not in the headers map
144-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
145-
("Content-Disposition" to "form-data; name=\"$name\"")
146157
val partBody = serializePartBody(obj, partContentType)
147158
addPart(
148-
partHeaders.toHeaders(),
159+
buildPartHeaders(name, headers),
149160
partBody.toRequestBody(partMediaType)
150161
)
151162
}

samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8686
return contentType ?: "application/octet-stream"
8787
}
8888

89+
/**
90+
* Builds headers for a multipart form-data part.
91+
* OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers.
92+
* This function filters out Content-Type and builds the appropriate Content-Disposition header.
93+
*
94+
* @param name The field name
95+
* @param headers The headers from the PartConfig (may include Content-Type)
96+
* @param filename Optional filename for file uploads
97+
* @return Headers object ready for addPart()
98+
*/
99+
protected fun buildPartHeaders(name: String, headers: Map<String, String>, filename: String? = null): Headers {
100+
val disposition = if (filename != null) {
101+
"form-data; name=\"$name\"; filename=\"$filename\""
102+
} else {
103+
"form-data; name=\"$name\""
104+
}
105+
return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders()
106+
}
107+
89108
/**
90109
* Adds a File to a MultipartBody.Builder
91110
* Defined a helper in the requestBody method to not duplicate code
@@ -98,13 +117,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
98117
* @see requestBody
99118
*/
100119
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
101-
// Filter out Content-Type from headers as OkHttp requires it to be passed
102-
// separately via asRequestBody(mediaType), not in the headers map
103-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
104-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
105120
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
106121
addPart(
107-
partHeaders.toHeaders(),
122+
buildPartHeaders(name, headers, file.name),
108123
file.asRequestBody(fileMediaType)
109124
)
110125
}
@@ -136,16 +151,12 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
136151
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
137152
* @see requestBody
138153
*/
139-
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
154+
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
140155
val partContentType = headers["Content-Type"]
141156
val partMediaType = partContentType?.toMediaTypeOrNull()
142-
// Filter out Content-Type from headers as OkHttp requires it to be passed
143-
// separately via toRequestBody(mediaType), not in the headers map
144-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
145-
("Content-Disposition" to "form-data; name=\"$name\"")
146157
val partBody = serializePartBody(obj, partContentType)
147158
addPart(
148-
partHeaders.toHeaders(),
159+
buildPartHeaders(name, headers),
149160
partBody.toRequestBody(partMediaType)
150161
)
151162
}

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8686
return contentType ?: "application/octet-stream"
8787
}
8888

89+
/**
90+
* Builds headers for a multipart form-data part.
91+
* OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers.
92+
* This function filters out Content-Type and builds the appropriate Content-Disposition header.
93+
*
94+
* @param name The field name
95+
* @param headers The headers from the PartConfig (may include Content-Type)
96+
* @param filename Optional filename for file uploads
97+
* @return Headers object ready for addPart()
98+
*/
99+
protected fun buildPartHeaders(name: String, headers: Map<String, String>, filename: String? = null): Headers {
100+
val disposition = if (filename != null) {
101+
"form-data; name=\"$name\"; filename=\"$filename\""
102+
} else {
103+
"form-data; name=\"$name\""
104+
}
105+
return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders()
106+
}
107+
89108
/**
90109
* Adds a File to a MultipartBody.Builder
91110
* Defined a helper in the requestBody method to not duplicate code
@@ -98,13 +117,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
98117
* @see requestBody
99118
*/
100119
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
101-
// Filter out Content-Type from headers as OkHttp requires it to be passed
102-
// separately via asRequestBody(mediaType), not in the headers map
103-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
104-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
105120
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
106121
addPart(
107-
partHeaders.toHeaders(),
122+
buildPartHeaders(name, headers, file.name),
108123
file.asRequestBody(fileMediaType)
109124
)
110125
}
@@ -136,16 +151,12 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
136151
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
137152
* @see requestBody
138153
*/
139-
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
154+
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
140155
val partContentType = headers["Content-Type"]
141156
val partMediaType = partContentType?.toMediaTypeOrNull()
142-
// Filter out Content-Type from headers as OkHttp requires it to be passed
143-
// separately via toRequestBody(mediaType), not in the headers map
144-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
145-
("Content-Disposition" to "form-data; name=\"$name\"")
146157
val partBody = serializePartBody(obj, partContentType)
147158
addPart(
148-
partHeaders.toHeaders(),
159+
buildPartHeaders(name, headers),
149160
partBody.toRequestBody(partMediaType)
150161
)
151162
}

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
8686
return contentType ?: "application/octet-stream"
8787
}
8888

89+
/**
90+
* Builds headers for a multipart form-data part.
91+
* OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers.
92+
* This function filters out Content-Type and builds the appropriate Content-Disposition header.
93+
*
94+
* @param name The field name
95+
* @param headers The headers from the PartConfig (may include Content-Type)
96+
* @param filename Optional filename for file uploads
97+
* @return Headers object ready for addPart()
98+
*/
99+
protected fun buildPartHeaders(name: String, headers: Map<String, String>, filename: String? = null): Headers {
100+
val disposition = if (filename != null) {
101+
"form-data; name=\"$name\"; filename=\"$filename\""
102+
} else {
103+
"form-data; name=\"$name\""
104+
}
105+
return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders()
106+
}
107+
89108
/**
90109
* Adds a File to a MultipartBody.Builder
91110
* Defined a helper in the requestBody method to not duplicate code
@@ -98,13 +117,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
98117
* @see requestBody
99118
*/
100119
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
101-
// Filter out Content-Type from headers as OkHttp requires it to be passed
102-
// separately via asRequestBody(mediaType), not in the headers map
103-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
104-
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
105120
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
106121
addPart(
107-
partHeaders.toHeaders(),
122+
buildPartHeaders(name, headers, file.name),
108123
file.asRequestBody(fileMediaType)
109124
)
110125
}
@@ -136,16 +151,12 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
136151
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
137152
* @see requestBody
138153
*/
139-
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
154+
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
140155
val partContentType = headers["Content-Type"]
141156
val partMediaType = partContentType?.toMediaTypeOrNull()
142-
// Filter out Content-Type from headers as OkHttp requires it to be passed
143-
// separately via toRequestBody(mediaType), not in the headers map
144-
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
145-
("Content-Disposition" to "form-data; name=\"$name\"")
146157
val partBody = serializePartBody(obj, partContentType)
147158
addPart(
148-
partHeaders.toHeaders(),
159+
buildPartHeaders(name, headers),
149160
partBody.toRequestBody(partMediaType)
150161
)
151162
}

0 commit comments

Comments
 (0)