Skip to content

Commit 339e93c

Browse files
committed
feat(client): add Headers class
1 parent 13c9038 commit 339e93c

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed

openai-java-core/src/main/kotlin/com/openai/core/Utils.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.google.common.collect.ImmutableListMultimap
66
import com.google.common.collect.ListMultimap
77
import com.openai.errors.OpenAIInvalidDataException
88
import java.util.Collections
9+
import java.util.SortedMap
910

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

23+
@JvmSynthetic
24+
internal fun <K : Comparable<K>, V> SortedMap<K, V>.toImmutable(): SortedMap<K, V> =
25+
if (isEmpty()) Collections.emptySortedMap()
26+
else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
27+
2228
@JvmSynthetic
2329
internal fun <K, V> ListMultimap<K, V>.toImmutable(): ListMultimap<K, V> =
2430
ImmutableListMultimap.copyOf(this)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.openai.core.http
2+
3+
import com.openai.core.toImmutable
4+
import java.util.TreeMap
5+
6+
class Headers
7+
private constructor(
8+
private val map: Map<String, List<String>>,
9+
@get:JvmName("size") val size: Int
10+
) {
11+
12+
fun isEmpty(): Boolean = map.isEmpty()
13+
14+
fun names(): Set<String> = map.keys
15+
16+
fun values(name: String): List<String> = map[name].orEmpty()
17+
18+
fun toBuilder(): Builder = Builder().putAll(map)
19+
20+
companion object {
21+
22+
@JvmStatic fun builder() = Builder()
23+
}
24+
25+
class Builder {
26+
27+
private val map: MutableMap<String, MutableList<String>> =
28+
TreeMap(String.CASE_INSENSITIVE_ORDER)
29+
private var size: Int = 0
30+
31+
fun put(name: String, value: String) = apply {
32+
map.getOrPut(name) { mutableListOf() }.add(value)
33+
size++
34+
}
35+
36+
fun put(name: String, values: Iterable<String>) = apply { values.forEach { put(name, it) } }
37+
38+
fun putAll(headers: Map<String, Iterable<String>>) = apply { headers.forEach(::put) }
39+
40+
fun putAll(headers: Headers) = apply {
41+
headers.names().forEach { put(it, headers.values(it)) }
42+
}
43+
44+
fun remove(name: String) = apply { size -= map.remove(name).orEmpty().size }
45+
46+
fun removeAll(names: Set<String>) = apply { names.forEach(::remove) }
47+
48+
fun clear() = apply {
49+
map.clear()
50+
size = 0
51+
}
52+
53+
fun replace(name: String, value: String) = apply {
54+
remove(name)
55+
put(name, value)
56+
}
57+
58+
fun replace(name: String, values: Iterable<String>) = apply {
59+
remove(name)
60+
put(name, values)
61+
}
62+
63+
fun replaceAll(headers: Map<String, Iterable<String>>) = apply {
64+
headers.forEach(::replace)
65+
}
66+
67+
fun replaceAll(headers: Headers) = apply {
68+
headers.names().forEach { replace(it, headers.values(it)) }
69+
}
70+
71+
fun build() =
72+
Headers(
73+
map.mapValuesTo(TreeMap(String.CASE_INSENSITIVE_ORDER)) { (_, values) ->
74+
values.toImmutable()
75+
}
76+
.toImmutable(),
77+
size
78+
)
79+
}
80+
81+
override fun hashCode(): Int = map.hashCode()
82+
83+
override fun equals(other: Any?): Boolean {
84+
if (this === other) {
85+
return true
86+
}
87+
88+
return other is Headers && map == other.map
89+
}
90+
91+
override fun toString(): String = "Headers{map=$map}"
92+
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package com.openai.core.http
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.assertj.core.api.Assertions.catchThrowable
5+
import org.assertj.core.api.Assumptions.assumeThat
6+
import org.junit.jupiter.params.ParameterizedTest
7+
import org.junit.jupiter.params.provider.EnumSource
8+
9+
internal class HeadersTest {
10+
11+
enum class TestCase(
12+
val headers: Headers,
13+
val expectedMap: Map<String, List<String>>,
14+
val expectedSize: Int
15+
) {
16+
EMPTY(Headers.builder().build(), expectedMap = mapOf(), expectedSize = 0),
17+
PUT_ONE(
18+
Headers.builder().put("name", "value").build(),
19+
expectedMap = mapOf("name" to listOf("value")),
20+
expectedSize = 1
21+
),
22+
PUT_MULTIPLE(
23+
Headers.builder().put("name", listOf("value1", "value2")).build(),
24+
expectedMap = mapOf("name" to listOf("value1", "value2")),
25+
expectedSize = 2
26+
),
27+
MULTIPLE_PUT(
28+
Headers.builder().put("name1", "value").put("name2", "value").build(),
29+
expectedMap = mapOf("name1" to listOf("value"), "name2" to listOf("value")),
30+
expectedSize = 2
31+
),
32+
MULTIPLE_PUT_SAME_NAME(
33+
Headers.builder().put("name", "value1").put("name", "value2").build(),
34+
expectedMap = mapOf("name" to listOf("value1", "value2")),
35+
expectedSize = 2
36+
),
37+
MULTIPLE_PUT_MULTIPLE(
38+
Headers.builder()
39+
.put("name", listOf("value1", "value2"))
40+
.put("name", listOf("value1", "value2"))
41+
.build(),
42+
expectedMap = mapOf("name" to listOf("value1", "value2", "value1", "value2")),
43+
expectedSize = 4
44+
),
45+
PUT_CASE_INSENSITIVE(
46+
Headers.builder()
47+
.put("name", "value1")
48+
.put("NAME", "value2")
49+
.put("nAmE", "value3")
50+
.build(),
51+
expectedMap = mapOf("name" to listOf("value1", "value2", "value3")),
52+
expectedSize = 3
53+
),
54+
PUT_ALL_MAP(
55+
Headers.builder()
56+
.putAll(
57+
mapOf(
58+
"name1" to listOf("value1", "value2"),
59+
"name2" to listOf("value1", "value2")
60+
)
61+
)
62+
.build(),
63+
expectedMap =
64+
mapOf("name1" to listOf("value1", "value2"), "name2" to listOf("value1", "value2")),
65+
expectedSize = 4
66+
),
67+
PUT_ALL_HEADERS(
68+
Headers.builder().putAll(Headers.builder().put("name", "value").build()).build(),
69+
expectedMap = mapOf("name" to listOf("value")),
70+
expectedSize = 1
71+
),
72+
PUT_ALL_CASE_INSENSITIVE(
73+
Headers.builder()
74+
.putAll(
75+
mapOf(
76+
"name" to listOf("value1"),
77+
"NAME" to listOf("value2"),
78+
"nAmE" to listOf("value3")
79+
)
80+
)
81+
.build(),
82+
expectedMap = mapOf("name" to listOf("value1", "value2", "value3")),
83+
expectedSize = 3
84+
),
85+
REMOVE_ABSENT(
86+
Headers.builder().remove("name").build(),
87+
expectedMap = mapOf(),
88+
expectedSize = 0
89+
),
90+
REMOVE_PRESENT_ONE(
91+
Headers.builder().put("name", "value").remove("name").build(),
92+
expectedMap = mapOf(),
93+
expectedSize = 0
94+
),
95+
REMOVE_PRESENT_MULTIPLE(
96+
Headers.builder().put("name", listOf("value1", "value2")).remove("name").build(),
97+
expectedMap = mapOf(),
98+
expectedSize = 0
99+
),
100+
REMOVE_CASE_INSENSITIVE(
101+
Headers.builder().put("name", listOf("value1", "value2")).remove("NAME").build(),
102+
expectedMap = mapOf(),
103+
expectedSize = 0
104+
),
105+
REMOVE_ALL(
106+
Headers.builder()
107+
.put("name1", "value")
108+
.put("name3", "value")
109+
.removeAll(setOf("name1", "name2", "name3"))
110+
.build(),
111+
expectedMap = mapOf(),
112+
expectedSize = 0
113+
),
114+
REMOVE_ALL_CASE_INSENSITIVE(
115+
Headers.builder()
116+
.put("name1", "value")
117+
.put("name3", "value")
118+
.removeAll(setOf("NAME1", "nAmE3"))
119+
.build(),
120+
expectedMap = mapOf(),
121+
expectedSize = 0
122+
),
123+
CLEAR(
124+
Headers.builder().put("name1", "value").put("name2", "value").clear().build(),
125+
expectedMap = mapOf(),
126+
expectedSize = 0
127+
),
128+
REPLACE_ONE_ABSENT(
129+
Headers.builder().replace("name", "value").build(),
130+
expectedMap = mapOf("name" to listOf("value")),
131+
expectedSize = 1
132+
),
133+
REPLACE_ONE_PRESENT_ONE(
134+
Headers.builder().put("name", "value1").replace("name", "value2").build(),
135+
expectedMap = mapOf("name" to listOf("value2")),
136+
expectedSize = 1
137+
),
138+
REPLACE_ONE_PRESENT_MULTIPLE(
139+
Headers.builder()
140+
.put("name", listOf("value1", "value2"))
141+
.replace("name", "value3")
142+
.build(),
143+
expectedMap = mapOf("name" to listOf("value3")),
144+
expectedSize = 1
145+
),
146+
REPLACE_MULTIPLE_ABSENT(
147+
Headers.builder().replace("name", listOf("value1", "value2")).build(),
148+
expectedMap = mapOf("name" to listOf("value1", "value2")),
149+
expectedSize = 2
150+
),
151+
REPLACE_MULTIPLE_PRESENT_ONE(
152+
Headers.builder()
153+
.put("name", "value1")
154+
.replace("name", listOf("value2", "value3"))
155+
.build(),
156+
expectedMap = mapOf("name" to listOf("value2", "value3")),
157+
expectedSize = 2
158+
),
159+
REPLACE_MULTIPLE_PRESENT_MULTIPLE(
160+
Headers.builder()
161+
.put("name", listOf("value1", "value2"))
162+
.replace("name", listOf("value3", "value4"))
163+
.build(),
164+
expectedMap = mapOf("name" to listOf("value3", "value4")),
165+
expectedSize = 2
166+
),
167+
REPLACE_CASE_INSENSITIVE(
168+
Headers.builder()
169+
.put("name", "value1")
170+
.replace("NAME", listOf("value2", "value3"))
171+
.build(),
172+
expectedMap = mapOf("NAME" to listOf("value2", "value3")),
173+
expectedSize = 2
174+
),
175+
REPLACE_ALL_MAP(
176+
Headers.builder()
177+
.put("name1", "value1")
178+
.put("name2", "value1")
179+
.put("name3", "value1")
180+
.replaceAll(mapOf("name1" to listOf("value2"), "name3" to listOf("value2")))
181+
.build(),
182+
expectedMap =
183+
mapOf(
184+
"name1" to listOf("value2"),
185+
"name2" to listOf("value1"),
186+
"name3" to listOf("value2")
187+
),
188+
expectedSize = 3
189+
),
190+
REPLACE_ALL_HEADERS(
191+
Headers.builder()
192+
.put("name1", "value1")
193+
.put("name2", "value1")
194+
.put("name3", "value1")
195+
.replaceAll(Headers.builder().put("name1", "value2").put("name3", "value2").build())
196+
.build(),
197+
expectedMap =
198+
mapOf(
199+
"name1" to listOf("value2"),
200+
"name2" to listOf("value1"),
201+
"name3" to listOf("value2")
202+
),
203+
expectedSize = 3
204+
),
205+
REPLACE_ALL_CASE_INSENSITIVE(
206+
Headers.builder()
207+
.put("name1", "value1")
208+
.put("name2", "value1")
209+
.replaceAll(mapOf("NAME1" to listOf("value2"), "nAmE2" to listOf("value2")))
210+
.build(),
211+
expectedMap = mapOf("NAME1" to listOf("value2"), "nAmE2" to listOf("value2")),
212+
expectedSize = 2
213+
)
214+
}
215+
216+
@ParameterizedTest
217+
@EnumSource
218+
fun namesAndValues(testCase: TestCase) {
219+
val map = mutableMapOf<String, List<String>>()
220+
val headers = testCase.headers
221+
headers.names().forEach { name -> map[name] = headers.values(name) }
222+
223+
assertThat(map).isEqualTo(testCase.expectedMap)
224+
}
225+
226+
@ParameterizedTest
227+
@EnumSource
228+
fun caseInsensitiveNames(testCase: TestCase) {
229+
val headers = testCase.headers
230+
231+
for (name in headers.names()) {
232+
assertThat(headers.values(name)).isEqualTo(headers.values(name.lowercase()))
233+
assertThat(headers.values(name)).isEqualTo(headers.values(name.uppercase()))
234+
}
235+
}
236+
237+
@ParameterizedTest
238+
@EnumSource
239+
fun size(testCase: TestCase) {
240+
val size = testCase.headers.size
241+
242+
assertThat(size).isEqualTo(testCase.expectedSize)
243+
}
244+
245+
@ParameterizedTest
246+
@EnumSource
247+
fun namesAreImmutable(testCase: TestCase) {
248+
val headers = testCase.headers
249+
val headerNamesCopy = headers.names().toSet()
250+
251+
val throwable = catchThrowable {
252+
(headers.names() as MutableSet<String>).add("another name")
253+
}
254+
255+
assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
256+
assertThat(headers.names()).isEqualTo(headerNamesCopy)
257+
}
258+
259+
@ParameterizedTest
260+
@EnumSource
261+
fun valuesAreImmutable(testCase: TestCase) {
262+
val headers = testCase.headers
263+
assumeThat(headers.size).isNotEqualTo(0)
264+
val name = headers.names().first()
265+
val headerValuesCopy = headers.values(name).toList()
266+
267+
val throwable = catchThrowable {
268+
(headers.values(name) as MutableList<String>).add("another value")
269+
}
270+
271+
assertThat(throwable).isInstanceOf(UnsupportedOperationException::class.java)
272+
assertThat(headers.values(name)).isEqualTo(headerValuesCopy)
273+
}
274+
}

0 commit comments

Comments
 (0)