Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lithic-java-core/src/main/kotlin/com/lithic/api/core/Utils.kt
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.lithic.api.errors.LithicInvalidDataException
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.lithic.api.core.http

import com.lithic.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.lithic.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)
}
}