-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(client): add
Headers
class (#253)
- Loading branch information
1 parent
43a21da
commit 9ae3cb0
Showing
3 changed files
with
372 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
modern-treasury-java-core/src/main/kotlin/com/moderntreasury/api/core/http/Headers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" | ||
} |
274 changes: 274 additions & 0 deletions
274
modern-treasury-java-core/src/test/kotlin/com/moderntreasury/api/core/http/HeadersTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |