mirror of
https://github.com/square/okhttp.git
synced 2026-01-12 10:23:16 +03:00
New datastructure to hold Tags (#9163)
* New datastructure to hold Tags We have been using MutableMap and Map. These have worked fine. But we'd like to soon support mutating tags on an existing Request object, which I'd like to do without a ton of collections or concurrency overhead. Instead we can do some lock-free stuff, which needs a simple data structure as a starting point. * Fill in some type parameters
This commit is contained in:
106
okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/Tags.kt
Normal file
106
okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/Tags.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3.internal
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* An immutable collection of key-value pairs implemented as a singly-linked list.
|
||||
*
|
||||
* Build up a collection by starting with [EmptyTags] and repeatedly calling [plus]. Each such call
|
||||
* returns a new instance.
|
||||
*
|
||||
* This collection is optimized for safe concurrent access over a very small number of elements.
|
||||
*
|
||||
* This collection and is expected to hold fewer than 10 elements. Each operation is _O(N)_, and so
|
||||
* building an instance with _N_ elements is _O(N**2)_.
|
||||
*/
|
||||
internal sealed class Tags {
|
||||
/**
|
||||
* Returns a tags instance that maps [key] to [value]. If [value] is null, this returns a tags
|
||||
* instance that does not have any mapping for [key].
|
||||
*/
|
||||
abstract fun <T : Any> plus(
|
||||
key: KClass<T>,
|
||||
value: T?,
|
||||
): Tags
|
||||
|
||||
abstract operator fun <T : Any> get(key: KClass<T>): T?
|
||||
}
|
||||
|
||||
/** An empty tags. This is always the tail of a [LinkedTags] chain. */
|
||||
internal object EmptyTags : Tags() {
|
||||
override fun <T : Any> plus(
|
||||
key: KClass<T>,
|
||||
value: T?,
|
||||
): Tags =
|
||||
when {
|
||||
value != null -> LinkedTags(key, value, this)
|
||||
else -> this
|
||||
}
|
||||
|
||||
override fun <T : Any> get(key: KClass<T>): T? = null
|
||||
|
||||
override fun toString() = "{}"
|
||||
}
|
||||
|
||||
/**
|
||||
* An invariant of this implementation is that [next] must not contain a mapping for [key].
|
||||
* Otherwise, we would have two values for the same key.
|
||||
*/
|
||||
private class LinkedTags<K : Any>(
|
||||
private val key: KClass<K>,
|
||||
private val value: K,
|
||||
private val next: Tags,
|
||||
) : Tags() {
|
||||
override fun <T : Any> plus(
|
||||
key: KClass<T>,
|
||||
value: T?,
|
||||
): Tags {
|
||||
// Create a copy of this `LinkedTags` that doesn't have a mapping for `key`.
|
||||
val thisMinusKey =
|
||||
when {
|
||||
key == this.key -> next // Subtract this!
|
||||
|
||||
else -> {
|
||||
val nextMinusKey = next.plus(key, null)
|
||||
when {
|
||||
nextMinusKey === next -> this // Same as the following line, but with fewer allocations.
|
||||
else -> LinkedTags(this.key, this.value, nextMinusKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a new `Tags` that maps `key` to `value`.
|
||||
return when {
|
||||
value != null -> LinkedTags(key, value, thisMinusKey)
|
||||
else -> thisMinusKey
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> get(key: KClass<T>): T? =
|
||||
when {
|
||||
key == this.key -> key.java.cast(value)
|
||||
else -> next[key]
|
||||
}
|
||||
|
||||
/** Returns a [toString] consistent with [Map], with elements in insertion order. */
|
||||
override fun toString(): String =
|
||||
generateSequence<LinkedTags<*>>(seed = this) { it.next as? LinkedTags<*> }
|
||||
.toList()
|
||||
.reversed()
|
||||
.joinToString(prefix = "{", postfix = "}") { "${it.key}=${it.value}" }
|
||||
}
|
||||
@@ -579,6 +579,23 @@ class RequestTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requestToStringIncludesTags() {
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url("https://square.com/".toHttpUrl())
|
||||
.tag<String>("hello")
|
||||
.tag<Int>(5)
|
||||
.build()
|
||||
assertThat(request.toString()).isEqualTo(
|
||||
"Request{method=GET, url=https://square.com/, tags={" +
|
||||
"class kotlin.String=hello, " +
|
||||
"class kotlin.Int=5" +
|
||||
"}}",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gzip() {
|
||||
val mediaType = "text/plain; charset=utf-8".toMediaType()
|
||||
|
||||
160
okhttp/src/jvmTest/kotlin/okhttp3/internal/TagsTest.kt
Normal file
160
okhttp/src/jvmTest/kotlin/okhttp3/internal/TagsTest.kt
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package okhttp3.internal
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class TagsTest {
|
||||
@Test
|
||||
fun emptyTags() {
|
||||
val tags = EmptyTags
|
||||
assertThat(tags[String::class]).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleElement() {
|
||||
val tags = EmptyTags.plus(String::class, "hello")
|
||||
assertThat(tags[String::class]).isEqualTo("hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleElements() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(String::class, "hello")
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
assertThat(tags[String::class]).isEqualTo("hello")
|
||||
assertThat(tags[Integer::class]).isEqualTo(5)
|
||||
}
|
||||
|
||||
/** The implementation retains no nodes from the original linked list. */
|
||||
@Test
|
||||
fun replaceFirstElement() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(String::class, "a")
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, "b")
|
||||
assertThat(tags[String::class]).isEqualTo("b")
|
||||
assertThat(tags.toString())
|
||||
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}")
|
||||
}
|
||||
|
||||
/** The implementation retains only the first node from the original linked list. */
|
||||
@Test
|
||||
fun replaceMiddleElement() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(String::class, "a")
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, "b")
|
||||
assertThat(tags[String::class]).isEqualTo("b")
|
||||
assertThat(tags.toString())
|
||||
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}")
|
||||
}
|
||||
|
||||
/** The implementation retains all but the first node from the original linked list. */
|
||||
@Test
|
||||
fun replaceLastElement() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, "a")
|
||||
.plus(String::class, "b")
|
||||
assertThat(tags[String::class]).isEqualTo("b")
|
||||
assertThat(tags.toString())
|
||||
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}")
|
||||
}
|
||||
|
||||
/** The implementation retains no nodes from the original linked list. */
|
||||
@Test
|
||||
fun removeFirstElement() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(String::class, "a")
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, null)
|
||||
assertThat(tags[String::class]).isNull()
|
||||
assertThat(tags.toString())
|
||||
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}")
|
||||
}
|
||||
|
||||
/** The implementation retains only the first node from the original linked list. */
|
||||
@Test
|
||||
fun removeMiddleElement() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(String::class, "a")
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, null)
|
||||
assertThat(tags[String::class]).isNull()
|
||||
assertThat(tags.toString())
|
||||
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}")
|
||||
}
|
||||
|
||||
/** The implementation retains all but the first node from the original linked list. */
|
||||
@Test
|
||||
fun removeLastElement() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, "a")
|
||||
.plus(String::class, null)
|
||||
assertThat(tags[String::class]).isNull()
|
||||
assertThat(tags.toString())
|
||||
.isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeUntilEmpty() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(Integer::class, 5 as Integer)
|
||||
.plus(Boolean::class, true)
|
||||
.plus(String::class, "a")
|
||||
.plus(String::class, null)
|
||||
.plus(Integer::class, null)
|
||||
.plus(Boolean::class, null)
|
||||
assertThat(tags).isEqualTo(EmptyTags)
|
||||
assertThat(tags.toString()).isEqualTo("{}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeAbsentFromEmpty() {
|
||||
val tags = EmptyTags.plus(String::class, null)
|
||||
assertThat(tags).isEqualTo(EmptyTags)
|
||||
assertThat(tags.toString()).isEqualTo("{}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeAbsentFromNonEmpty() {
|
||||
val tags =
|
||||
EmptyTags
|
||||
.plus(String::class, "a")
|
||||
.plus(Integer::class, null)
|
||||
assertThat(tags[String::class]).isEqualTo("a")
|
||||
assertThat(tags.toString()).isEqualTo("{class kotlin.String=a}")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user