Skip to content

Commit 22e538b

Browse files
chore(client)!: refactor multipart formdata impl (#296)
chore(internal): move `HttpRequestBodies` file chore(internal): extract function for checking for lists in json fields # Migration 1. Builder methods that used to take `contentType` and `filename` as positional parameters after the main argument now no longer do. To set a custom `contentType` or `filename`, pass `MultipartField`, which can be constructed via `MultipartField.builder()`. 2. It's unlikely you were referencing it, but `MultipartFormValue` is now called `MultipartField` if you were.
1 parent 70b0eb8 commit 22e538b

File tree

97 files changed

+1429
-4860
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+1429
-4860
lines changed

orb-java-core/src/main/kotlin/com/withorb/api/core/Check.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ package com.withorb.api.core
55
fun <T : Any> checkRequired(name: String, value: T?): T =
66
checkNotNull(value) { "`$name` is required, but was not set" }
77

8+
@JvmSynthetic
9+
internal fun <T : Any> checkKnown(name: String, value: JsonField<T>): T =
10+
value.asKnown().orElseThrow {
11+
IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
12+
}
13+
14+
@JvmSynthetic
15+
internal fun <T : Any> checkKnown(name: String, value: MultipartField<T>): T =
16+
value.value.asKnown().orElseThrow {
17+
IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
18+
}
19+
820
@JvmSynthetic
921
internal fun checkLength(name: String, value: String, length: Int): String =
1022
value.also {

orb-java-core/src/main/kotlin/com/withorb/api/core/HttpRequestBodies.kt

Lines changed: 0 additions & 108 deletions
This file was deleted.

orb-java-core/src/main/kotlin/com/withorb/api/core/Values.kt

Lines changed: 61 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,8 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
2727
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
2828
import com.fasterxml.jackson.databind.ser.std.NullSerializer
2929
import com.withorb.api.errors.OrbInvalidDataException
30-
import java.nio.charset.Charset
3130
import java.util.Objects
3231
import java.util.Optional
33-
import org.apache.hc.core5.http.ContentType
3432

3533
@JsonDeserialize(using = JsonField.Deserializer::class)
3634
sealed class JsonField<out T : Any> {
@@ -287,12 +285,12 @@ private constructor(
287285
return true
288286
}
289287

290-
return other is KnownValue<*> && value == other.value
288+
return other is KnownValue<*> && value contentEquals other.value
291289
}
292290

293-
override fun hashCode() = value.hashCode()
291+
override fun hashCode() = contentHash(value)
294292

295-
override fun toString() = value.toString()
293+
override fun toString() = value.contentToString()
296294

297295
companion object {
298296
@JsonCreator @JvmStatic fun <T : Any> of(value: T) = KnownValue(value)
@@ -462,15 +460,63 @@ annotation class ExcludeMissing
462460
)
463461
annotation class NoAutoDetect
464462

465-
class MultipartFormValue<T>
466-
internal constructor(
467-
val name: String,
468-
val value: T,
469-
val contentType: ContentType,
470-
val filename: String? = null,
463+
class MultipartField<T : Any>
464+
private constructor(
465+
@get:JvmName("value") val value: JsonField<T>,
466+
@get:JvmName("contentType") val contentType: String,
467+
private val filename: String?,
471468
) {
472469

473-
private val hashCode: Int by lazy { contentHash(name, value, contentType, filename) }
470+
companion object {
471+
472+
@JvmStatic fun <T : Any> of(value: T?) = builder<T>().value(value).build()
473+
474+
@JvmStatic fun <T : Any> of(value: JsonField<T>) = builder<T>().value(value).build()
475+
476+
@JvmStatic fun <T : Any> builder() = Builder<T>()
477+
}
478+
479+
fun filename(): Optional<String> = Optional.ofNullable(filename)
480+
481+
@JvmSynthetic
482+
internal fun <R : Any> map(transform: (T) -> R): MultipartField<R> =
483+
MultipartField.builder<R>()
484+
.value(value.map(transform))
485+
.contentType(contentType)
486+
.filename(filename)
487+
.build()
488+
489+
/** A builder for [MultipartField]. */
490+
class Builder<T : Any> internal constructor() {
491+
492+
private var value: JsonField<T>? = null
493+
private var contentType: String? = null
494+
private var filename: String? = null
495+
496+
fun value(value: JsonField<T>) = apply { this.value = value }
497+
498+
fun value(value: T?) = value(JsonField.ofNullable(value))
499+
500+
fun contentType(contentType: String) = apply { this.contentType = contentType }
501+
502+
fun filename(filename: String?) = apply { this.filename = filename }
503+
504+
fun filename(filename: Optional<String>) = filename(filename.orElse(null))
505+
506+
fun build(): MultipartField<T> {
507+
val value = checkRequired("value", value)
508+
return MultipartField(
509+
value,
510+
contentType
511+
?: if (value is KnownValue && value.value is ByteArray)
512+
"application/octet-stream"
513+
else "text/plain; charset=utf-8",
514+
filename,
515+
)
516+
}
517+
}
518+
519+
private val hashCode: Int by lazy { contentHash(value, contentType, filename) }
474520

475521
override fun hashCode(): Int = hashCode
476522

@@ -479,63 +525,12 @@ internal constructor(
479525
return true
480526
}
481527

482-
return other is MultipartFormValue<*> &&
483-
name == other.name &&
484-
value contentEquals other.value &&
528+
return other is MultipartField<*> &&
529+
value == other.value &&
485530
contentType == other.contentType &&
486531
filename == other.filename
487532
}
488533

489534
override fun toString(): String =
490-
"MultipartFormValue{name=$name, contentType=$contentType, filename=$filename, value=${valueToString()}}"
491-
492-
private fun valueToString(): String =
493-
when (value) {
494-
is ByteArray -> "ByteArray of size ${value.size}"
495-
else -> value.toString()
496-
}
497-
498-
companion object {
499-
internal fun fromString(
500-
name: String,
501-
value: String,
502-
contentType: ContentType,
503-
): MultipartFormValue<String> = MultipartFormValue(name, value, contentType)
504-
505-
internal fun fromBoolean(
506-
name: String,
507-
value: Boolean,
508-
contentType: ContentType,
509-
): MultipartFormValue<Boolean> = MultipartFormValue(name, value, contentType)
510-
511-
internal fun fromLong(
512-
name: String,
513-
value: Long,
514-
contentType: ContentType,
515-
): MultipartFormValue<Long> = MultipartFormValue(name, value, contentType)
516-
517-
internal fun fromDouble(
518-
name: String,
519-
value: Double,
520-
contentType: ContentType,
521-
): MultipartFormValue<Double> = MultipartFormValue(name, value, contentType)
522-
523-
internal fun <T : Enum> fromEnum(
524-
name: String,
525-
value: T,
526-
contentType: ContentType,
527-
): MultipartFormValue<T> = MultipartFormValue(name, value, contentType)
528-
529-
internal fun fromByteArray(
530-
name: String,
531-
value: ByteArray,
532-
contentType: ContentType,
533-
filename: String? = null,
534-
): MultipartFormValue<ByteArray> = MultipartFormValue(name, value, contentType, filename)
535-
}
536-
}
537-
538-
internal object ContentTypes {
539-
val DefaultText = ContentType.create(ContentType.TEXT_PLAIN.mimeType, Charset.forName("UTF-8"))
540-
val DefaultBinary = ContentType.DEFAULT_BINARY
535+
"MultipartField{value=$value, contentType=$contentType, filename=$filename}"
541536
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// File generated from our OpenAPI spec by Stainless.
2+
3+
@file:JvmName("HttpRequestBodies")
4+
5+
package com.withorb.api.core.http
6+
7+
import com.fasterxml.jackson.databind.JsonNode
8+
import com.fasterxml.jackson.databind.json.JsonMapper
9+
import com.fasterxml.jackson.databind.node.JsonNodeType
10+
import com.withorb.api.core.MultipartField
11+
import com.withorb.api.errors.OrbInvalidDataException
12+
import java.io.OutputStream
13+
import kotlin.jvm.optionals.getOrNull
14+
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
15+
import org.apache.hc.core5.http.ContentType
16+
import org.apache.hc.core5.http.HttpEntity
17+
18+
@JvmSynthetic
19+
internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
20+
object : HttpRequestBody {
21+
private val bytes: ByteArray by lazy { jsonMapper.writeValueAsBytes(value) }
22+
23+
override fun writeTo(outputStream: OutputStream) = outputStream.write(bytes)
24+
25+
override fun contentType(): String = "application/json"
26+
27+
override fun contentLength(): Long = bytes.size.toLong()
28+
29+
override fun repeatable(): Boolean = true
30+
31+
override fun close() {}
32+
}
33+
34+
@JvmSynthetic
35+
internal fun multipartFormData(
36+
jsonMapper: JsonMapper,
37+
fields: Map<String, MultipartField<*>>,
38+
): HttpRequestBody =
39+
object : HttpRequestBody {
40+
private val entity: HttpEntity by lazy {
41+
MultipartEntityBuilder.create()
42+
.apply {
43+
fields.forEach { (name, field) ->
44+
val node = jsonMapper.valueToTree<JsonNode>(field.value)
45+
serializePart(name, node).forEach { (name, bytes) ->
46+
addBinaryBody(
47+
name,
48+
bytes,
49+
ContentType.parseLenient(field.contentType),
50+
field.filename().getOrNull(),
51+
)
52+
}
53+
}
54+
}
55+
.build()
56+
}
57+
58+
private fun serializePart(name: String, node: JsonNode): Sequence<Pair<String, ByteArray>> =
59+
when (node.nodeType) {
60+
JsonNodeType.MISSING,
61+
JsonNodeType.NULL -> emptySequence()
62+
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue())
63+
JsonNodeType.STRING -> sequenceOf(name to node.textValue().toByteArray())
64+
JsonNodeType.BOOLEAN ->
65+
sequenceOf(name to node.booleanValue().toString().toByteArray())
66+
JsonNodeType.NUMBER ->
67+
sequenceOf(name to node.numberValue().toString().toByteArray())
68+
JsonNodeType.ARRAY ->
69+
node.elements().asSequence().flatMap { element ->
70+
serializePart("$name[]", element)
71+
}
72+
JsonNodeType.OBJECT ->
73+
node.fields().asSequence().flatMap { (key, value) ->
74+
serializePart("$name[$key]", value)
75+
}
76+
JsonNodeType.POJO,
77+
null -> throw OrbInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
78+
}
79+
80+
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
81+
82+
override fun contentType(): String = entity.contentType
83+
84+
override fun contentLength(): Long = entity.contentLength
85+
86+
override fun repeatable(): Boolean = entity.isRepeatable
87+
88+
override fun close() = entity.close()
89+
}

0 commit comments

Comments
 (0)