Skip to content

Commit

Permalink
feat(client): add Headers class (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-app[bot] authored and stainless-bot committed Nov 1, 2024
1 parent 43a21da commit 9ae3cb0
Show file tree
Hide file tree
Showing 3 changed files with 372 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.google.common.collect.ImmutableListMultimap
import com.google.common.collect.ListMultimap
import com.moderntreasury.api.errors.ModernTreasuryInvalidDataException
import java.util.Collections
import java.util.SortedMap

@JvmSynthetic
internal fun <T : Any> T?.getOrThrow(name: String): T =
Expand All @@ -19,6 +20,11 @@ internal fun <T> List<T>.toImmutable(): List<T> =
internal fun <K, V> Map<K, V>.toImmutable(): Map<K, V> =
if (isEmpty()) Collections.emptyMap() else Collections.unmodifiableMap(toMap())

@JvmSynthetic
internal fun <K : Comparable<K>, V> SortedMap<K, V>.toImmutable(): SortedMap<K, V> =
if (isEmpty()) Collections.emptySortedMap()
else Collections.unmodifiableSortedMap(toSortedMap(comparator()))

@JvmSynthetic
internal fun <K, V> ListMultimap<K, V>.toImmutable(): ListMultimap<K, V> =
ImmutableListMultimap.copyOf(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.moderntreasury.api.core.http

import com.moderntreasury.api.core.toImmutable
import java.util.TreeMap

class Headers
private constructor(
private val map: Map<String, List<String>>,
@get:JvmName("size") val size: Int
) {

fun isEmpty(): Boolean = map.isEmpty()

fun names(): Set<String> = map.keys

fun values(name: String): List<String> = map[name].orEmpty()

fun toBuilder(): Builder = Builder().putAll(map)

companion object {

@JvmStatic fun builder() = Builder()
}

class Builder {

private val map: MutableMap<String, MutableList<String>> =
TreeMap(String.CASE_INSENSITIVE_ORDER)
private var size: Int = 0

fun put(name: String, value: String) = apply {
map.getOrPut(name) { mutableListOf() }.add(value)
size++
}

fun put(name: String, values: Iterable<String>) = apply { values.forEach { put(name, it) } }

fun putAll(headers: Map<String, Iterable<String>>) = apply { headers.forEach(::put) }

fun putAll(headers: Headers) = apply {
headers.names().forEach { put(it, headers.values(it)) }
}

fun remove(name: String) = apply { size -= map.remove(name).orEmpty().size }

fun removeAll(names: Set<String>) = apply { names.forEach(::remove) }

fun clear() = apply {
map.clear()
size = 0
}

fun replace(name: String, value: String) = apply {
remove(name)
put(name, value)
}

fun replace(name: String, values: Iterable<String>) = apply {
remove(name)
put(name, values)
}

fun replaceAll(headers: Map<String, Iterable<String>>) = apply {
headers.forEach(::replace)
}

fun replaceAll(headers: Headers) = apply {
headers.names().forEach { replace(it, headers.values(it)) }
}

fun build() =
Headers(
map.mapValuesTo(TreeMap(String.CASE_INSENSITIVE_ORDER)) { (_, values) ->
values.toImmutable()
}
.toImmutable(),
size
)
}

override fun hashCode(): Int = map.hashCode()

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}

return other is Headers && map == other.map
}

override fun toString(): String = "Headers{map=$map}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package com.moderntreasury.api.core.http

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.catchThrowable
import org.assertj.core.api.Assumptions.assumeThat
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

internal class HeadersTest {

enum class TestCase(
val headers: Headers,
val expectedMap: Map<String, List<String>>,
val expectedSize: Int
) {
EMPTY(Headers.builder().build(), expectedMap = mapOf(), expectedSize = 0),
PUT_ONE(
Headers.builder().put("name", "value").build(),
expectedMap = mapOf("name" to listOf("value")),
expectedSize = 1
),
PUT_MULTIPLE(
Headers.builder().put("name", listOf("value1", "value2")).build(),
expectedMap = mapOf("name" to listOf("value1", "value2")),
expectedSize = 2
),
MULTIPLE_PUT(
Headers.builder().put("name1", "value").put("name2", "value").build(),
expectedMap = mapOf("name1" to listOf("value"), "name2" to listOf("value")),
expectedSize = 2
),
MULTIPLE_PUT_SAME_NAME(
Headers.builder().put("name", "value1").put("name", "value2").build(),
expectedMap = mapOf("name" to listOf("value1", "value2")),
expectedSize = 2
),
MULTIPLE_PUT_MULTIPLE(
Headers.builder()
.put("name", listOf("value1", "value2"))
.put("name", listOf("value1", "value2"))
.build(),
expectedMap = mapOf("name" to listOf("value1", "value2", "value1", "value2")),
expectedSize = 4
),
PUT_CASE_INSENSITIVE(
Headers.builder()
.put("name", "value1")
.put("NAME", "value2")
.put("nAmE", "value3")
.build(),
expectedMap = mapOf("name" to listOf("value1", "value2", "value3")),
expectedSize = 3
),
PUT_ALL_MAP(
Headers.builder()
.putAll(
mapOf(
"name1" to listOf("value1", "value2"),
"name2" to listOf("value1", "value2")
)
)
.build(),
expectedMap =
mapOf("name1" to listOf("value1", "value2"), "name2" to listOf("value1", "value2")),
expectedSize = 4
),
PUT_ALL_HEADERS(
Headers.builder().putAll(Headers.builder().put("name", "value").build()).build(),
expectedMap = mapOf("name" to listOf("value")),
expectedSize = 1
),
PUT_ALL_CASE_INSENSITIVE(
Headers.builder()
.putAll(
mapOf(
"name" to listOf("value1"),
"NAME" to listOf("value2"),
"nAmE" to listOf("value3")
)
)
.build(),
expectedMap = mapOf("name" to listOf("value1", "value2", "value3")),
expectedSize = 3
),
REMOVE_ABSENT(
Headers.builder().remove("name").build(),
expectedMap = mapOf(),
expectedSize = 0
),
REMOVE_PRESENT_ONE(
Headers.builder().put("name", "value").remove("name").build(),
expectedMap = mapOf(),
expectedSize = 0
),
REMOVE_PRESENT_MULTIPLE(
Headers.builder().put("name", listOf("value1", "value2")).remove("name").build(),
expectedMap = mapOf(),
expectedSize = 0
),
REMOVE_CASE_INSENSITIVE(
Headers.builder().put("name", listOf("value1", "value2")).remove("NAME").build(),
expectedMap = mapOf(),
expectedSize = 0
),
REMOVE_ALL(
Headers.builder()
.put("name1", "value")
.put("name3", "value")
.removeAll(setOf("name1", "name2", "name3"))
.build(),
expectedMap = mapOf(),
expectedSize = 0
),
REMOVE_ALL_CASE_INSENSITIVE(
Headers.builder()
.put("name1", "value")
.put("name3", "value")
.removeAll(setOf("NAME1", "nAmE3"))
.build(),
expectedMap = mapOf(),
expectedSize = 0
),
CLEAR(
Headers.builder().put("name1", "value").put("name2", "value").clear().build(),
expectedMap = mapOf(),
expectedSize = 0
),
REPLACE_ONE_ABSENT(
Headers.builder().replace("name", "value").build(),
expectedMap = mapOf("name" to listOf("value")),
expectedSize = 1
),
REPLACE_ONE_PRESENT_ONE(
Headers.builder().put("name", "value1").replace("name", "value2").build(),
expectedMap = mapOf("name" to listOf("value2")),
expectedSize = 1
),
REPLACE_ONE_PRESENT_MULTIPLE(
Headers.builder()
.put("name", listOf("value1", "value2"))
.replace("name", "value3")
.build(),
expectedMap = mapOf("name" to listOf("value3")),
expectedSize = 1
),
REPLACE_MULTIPLE_ABSENT(
Headers.builder().replace("name", listOf("value1", "value2")).build(),
expectedMap = mapOf("name" to listOf("value1", "value2")),
expectedSize = 2
),
REPLACE_MULTIPLE_PRESENT_ONE(
Headers.builder()
.put("name", "value1")
.replace("name", listOf("value2", "value3"))
.build(),
expectedMap = mapOf("name" to listOf("value2", "value3")),
expectedSize = 2
),
REPLACE_MULTIPLE_PRESENT_MULTIPLE(
Headers.builder()
.put("name", listOf("value1", "value2"))
.replace("name", listOf("value3", "value4"))
.build(),
expectedMap = mapOf("name" to listOf("value3", "value4")),
expectedSize = 2
),
REPLACE_CASE_INSENSITIVE(
Headers.builder()
.put("name", "value1")
.replace("NAME", listOf("value2", "value3"))
.build(),
expectedMap = mapOf("NAME" to listOf("value2", "value3")),
expectedSize = 2
),
REPLACE_ALL_MAP(
Headers.builder()
.put("name1", "value1")
.put("name2", "value1")
.put("name3", "value1")
.replaceAll(mapOf("name1" to listOf("value2"), "name3" to listOf("value2")))
.build(),
expectedMap =
mapOf(
"name1" to listOf("value2"),
"name2" to listOf("value1"),
"name3" to listOf("value2")
),
expectedSize = 3
),
REPLACE_ALL_HEADERS(
Headers.builder()
.put("name1", "value1")
.put("name2", "value1")
.put("name3", "value1")
.replaceAll(Headers.builder().put("name1", "value2").put("name3", "value2").build())
.build(),
expectedMap =
mapOf(
"name1" to listOf("value2"),
"name2" to listOf("value1"),
"name3" to listOf("value2")
),
expectedSize = 3
),
REPLACE_ALL_CASE_INSENSITIVE(
Headers.builder()
.put("name1", "value1")
.put("name2", "value1")
.replaceAll(mapOf("NAME1" to listOf("value2"), "nAmE2" to listOf("value2")))
.build(),
expectedMap = mapOf("NAME1" to listOf("value2"), "nAmE2" to listOf("value2")),
expectedSize = 2
)
}

@ParameterizedTest
@EnumSource
fun namesAndValues(testCase: TestCase) {
val map = mutableMapOf<String, List<String>>()
val headers = testCase.headers
headers.names().forEach { name -> map[name] = headers.values(name) }

assertThat(map).isEqualTo(testCase.expectedMap)
}

@ParameterizedTest
@EnumSource
fun caseInsensitiveNames(testCase: TestCase) {
val headers = testCase.headers

for (name in headers.names()) {
assertThat(headers.values(name)).isEqualTo(headers.values(name.lowercase()))
assertThat(headers.values(name)).isEqualTo(headers.values(name.uppercase()))
}
}

@ParameterizedTest
@EnumSource
fun size(testCase: TestCase) {
val size = testCase.headers.size

assertThat(size).isEqualTo(testCase.expectedSize)
}

@ParameterizedTest
@EnumSource
fun namesAreImmutable(testCase: TestCase) {
val headers = testCase.headers
val headerNamesCopy = headers.names().toSet()

val throwable = catchThrowable {
(headers.names() as MutableSet<String>).add("another name")
}

assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
assertThat(headers.names()).isEqualTo(headerNamesCopy)
}

@ParameterizedTest
@EnumSource
fun valuesAreImmutable(testCase: TestCase) {
val headers = testCase.headers
assumeThat(headers.size).isNotEqualTo(0)
val name = headers.names().first()
val headerValuesCopy = headers.values(name).toList()

val throwable = catchThrowable {
(headers.values(name) as MutableList<String>).add("another value")
}

assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
assertThat(headers.values(name)).isEqualTo(headerValuesCopy)
}
}

0 comments on commit 9ae3cb0

Please sign in to comment.