Skip to content

Commit

Permalink
[Kotlin] fix OpenAPITools#20231, OkHttp client can handle a field wit…
Browse files Browse the repository at this point in the history
…h a list of files (OpenAPITools#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
  • Loading branch information
jorgeferdz authored Dec 4, 2024
1 parent 0183620 commit 71ccc88
Show file tree
Hide file tree
Showing 22 changed files with 1,188 additions and 330 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,48 @@ import com.squareup.moshi.adapter
return contentType ?: "application/octet-stream"
}

/**
* Adds a File to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param file The file that will be added as the field value
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
file.asRequestBody(fileMediaType)
)
}

/**
* Adds any type to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param obj The field name to add in the request
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
if (obj == null) return
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(obj).toRequestBody(null)
)
}

protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
Expand All @@ -124,21 +166,18 @@ import com.squareup.moshi.adapter
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
if (part.body is File) {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
part.body.asRequestBody(fileMediaType)
)
} else {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(part.body).toRequestBody(null)
)
when (part.body) {
is File -> addPartToMultiPart(name, part.headers, part.body)
is List<*> -> {
part.body.forEach {
if (it is File) {
addPartToMultiPart(name, part.headers, it)
} else {
addPartToMultiPart(name, part.headers, it)
}
}
}
else -> addPartToMultiPart(name, part.headers, part.body)
}
}
}.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,48 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
return contentType ?: "application/octet-stream"
}

/**
* Adds a File to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param file The file that will be added as the field value
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
file.asRequestBody(fileMediaType)
)
}

/**
* Adds any type to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param obj The field name to add in the request
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
if (obj == null) return
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(obj).toRequestBody(null)
)
}

protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
Expand All @@ -95,21 +137,18 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
if (part.body is File) {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
part.body.asRequestBody(fileMediaType)
)
} else {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(part.body).toRequestBody(null)
)
when (part.body) {
is File -> addPartToMultiPart(name, part.headers, part.body)
is List<*> -> {
part.body.forEach {
if (it is File) {
addPartToMultiPart(name, part.headers, it)
} else {
addPartToMultiPart(name, part.headers, it)
}
}
}
else -> addPartToMultiPart(name, part.headers, part.body)
}
}
}.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,48 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
return contentType ?: "application/octet-stream"
}

/**
* Adds a File to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param file The file that will be added as the field value
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
file.asRequestBody(fileMediaType)
)
}

/**
* Adds any type to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param obj The field name to add in the request
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
if (obj == null) return
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(obj).toRequestBody(null)
)
}

protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
Expand All @@ -95,21 +137,18 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
if (part.body is File) {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
part.body.asRequestBody(fileMediaType)
)
} else {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(part.body).toRequestBody(null)
)
when (part.body) {
is File -> addPartToMultiPart(name, part.headers, part.body)
is List<*> -> {
part.body.forEach {
if (it is File) {
addPartToMultiPart(name, part.headers, it)
} else {
addPartToMultiPart(name, part.headers, it)
}
}
}
else -> addPartToMultiPart(name, part.headers, part.body)
}
}
}.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,48 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
return contentType ?: "application/octet-stream"
}

/**
* Adds a File to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param file The file that will be added as the field value
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
file.asRequestBody(fileMediaType)
)
}

/**
* Adds any type to a MultipartBody.Builder
* Defined a helper in the requestBody method to not duplicate code
* It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File.
*
* @param name The field name to add in the request
* @param headers The headers that are in the PartConfig
* @param obj The field name to add in the request
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
* @see requestBody
*/
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
if (obj == null) return
val partHeaders = headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(obj).toRequestBody(null)
)
}

protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull())
Expand All @@ -95,21 +137,18 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
if (part.body is File) {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${part.body.name}\"")
val fileMediaType = guessContentTypeFromFile(part.body).toMediaTypeOrNull()
addPart(
partHeaders.toHeaders(),
part.body.asRequestBody(fileMediaType)
)
} else {
val partHeaders = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"")
addPart(
partHeaders.toHeaders(),
parameterToString(part.body).toRequestBody(null)
)
when (part.body) {
is File -> addPartToMultiPart(name, part.headers, part.body)
is List<*> -> {
part.body.forEach {
if (it is File) {
addPartToMultiPart(name, part.headers, it)
} else {
addPartToMultiPart(name, part.headers, it)
}
}
}
else -> addPartToMultiPart(name, part.headers, part.body)
}
}
}.build()
Expand Down
Loading

0 comments on commit 71ccc88

Please sign in to comment.