Skip to content

Commit

Permalink
Add TypedOptions (#1417)
Browse files Browse the repository at this point in the history
* Add TypedOptions

* apiDump

* Expose the basic TypedOptions constructor

* Spotless

* Update okio/src/commonMain/kotlin/okio/TypedOptions.kt

Co-authored-by: Jake Wharton <jw@squareup.com>

---------

Co-authored-by: Jake Wharton <jw@squareup.com>
  • Loading branch information
swankjesse and JakeWharton committed Feb 5, 2024
1 parent 13e2f46 commit 0d76d41
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 3 deletions.
14 changes: 14 additions & 0 deletions okio/api/okio.api
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public final class okio/Buffer : java/lang/Cloneable, java/nio/channels/ByteChan
public fun request (J)Z
public fun require (J)V
public fun select (Lokio/Options;)I
public fun select (Lokio/TypedOptions;)Ljava/lang/Object;
public final fun sha1 ()Lokio/ByteString;
public final fun sha256 ()Lokio/ByteString;
public final fun sha512 ()Lokio/ByteString;
Expand Down Expand Up @@ -284,6 +285,7 @@ public abstract interface class okio/BufferedSource : java/nio/channels/Readable
public abstract fun request (J)Z
public abstract fun require (J)V
public abstract fun select (Lokio/Options;)I
public abstract fun select (Lokio/TypedOptions;)Ljava/lang/Object;
public abstract fun skip (J)V
}

Expand Down Expand Up @@ -790,6 +792,18 @@ public final class okio/Timeout$Companion {
public final fun timeout-HG0u8IE (Lokio/Timeout;J)Lokio/Timeout;
}

public final class okio/TypedOptions : kotlin/collections/AbstractList, java/util/RandomAccess {
public static final field Companion Lokio/TypedOptions$Companion;
public fun <init> (Ljava/util/List;Lokio/Options;)V
public fun get (I)Ljava/lang/Object;
public fun getSize ()I
public static final fun of (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Lokio/TypedOptions;
}

public final class okio/TypedOptions$Companion {
public final fun of (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Lokio/TypedOptions;
}

public final class okio/Utf8 {
public static final fun size (Ljava/lang/String;)J
public static final fun size (Ljava/lang/String;I)J
Expand Down
35 changes: 33 additions & 2 deletions okio/src/commonMain/kotlin/okio/BufferedSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ expect sealed interface BufferedSource : Source {
fun readByteString(byteCount: Long): ByteString

/**
* Finds the first string in `options` that is a prefix of this buffer, consumes it from this
* buffer, and returns its index. If no byte string in `options` is a prefix of this buffer this
* Finds the first byte string in `options` that is a prefix of this buffer, consumes it from this
* source, and returns its index. If no byte string in `options` is a prefix of this buffer this
* returns -1 and no bytes are consumed.
*
* This can be used as an alternative to [readByteString] or even [readUtf8] if the set of
Expand All @@ -268,6 +268,37 @@ expect sealed interface BufferedSource : Source {
*/
fun select(options: Options): Int

/**
* Finds the first item in [options] whose encoding is a prefix of this buffer, consumes it from
* this buffer, and returns it. If no item in [options] is a prefix of this source, this function
* returns null and no bytes are consumed.
*
* This can be used as an alternative to [readByteString] or even [readUtf8] if the set of
* expected values is known in advance.
*
* ```
* TypedOptions<Direction> options = TypedOptions.of(
* Arrays.asList(Direction.values()),
* (direction) -> ByteString.encodeUtf8(direction.name().toLowerCase(Locale.ROOT))
* );
*
* Buffer buffer = new Buffer()
* .writeUtf8("north:100\n")
* .writeUtf8("east:50\n");
*
* assertEquals(Direction.NORTH, buffer.select(options));
* assertEquals(':', buffer.readByte());
* assertEquals(100L, buffer.readDecimalLong());
* assertEquals('\n', buffer.readByte());
*
* assertEquals(Direction.EAST, buffer.select(options));
* assertEquals(':', buffer.readByte());
* assertEquals(50L, buffer.readDecimalLong());
* assertEquals('\n', buffer.readByte());
* ```
*/
fun <T : Any> select(options: TypedOptions<T>): T?

/** Removes all bytes from this and returns them as a byte array. */
fun readByteArray(): ByteArray

Expand Down
6 changes: 5 additions & 1 deletion okio/src/commonMain/kotlin/okio/Options.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ package okio

import kotlin.jvm.JvmStatic

/** An indexed set of values that may be read with [BufferedSource.select]. */
/**
* An indexed set of values that may be read with [BufferedSource.select].
*
* Also consider [TypedOptions] to select a typed value _T_.
*/
class Options private constructor(
internal val byteStrings: Array<out ByteString>,
internal val trie: IntArray,
Expand Down
51 changes: 51 additions & 0 deletions okio/src/commonMain/kotlin/okio/TypedOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2024 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 okio

import kotlin.jvm.JvmStatic

/**
* A list of values that may be read with [BufferedSource.select].
*
* Also consider [Options] to select an integer index.
*/
class TypedOptions<T : Any>(
list: List<T>,
internal val options: Options,
) : AbstractList<T>(), RandomAccess {
internal val list = list.toList() // Defensive copy.

init {
require(this.list.size == options.size)
}

override val size: Int
get() = list.size

override fun get(index: Int) = list[index]

companion object {
@JvmStatic
inline fun <T : Any> of(
values: Iterable<T>,
encode: (T) -> ByteString,
): TypedOptions<T> {
val list = values.toList()
val options = Options.of(*Array(list.size) { encode(list[it]) })
return TypedOptions(list, options)
}
}
}
30 changes: 30 additions & 0 deletions okio/src/commonMain/kotlin/okio/internal/BufferedSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2024 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.
*/

@file:JvmName("-BufferedSource") // A leading '-' hides this class from Java.

package okio.internal

import kotlin.jvm.JvmName
import okio.BufferedSource
import okio.TypedOptions

internal inline fun <T : Any> BufferedSource.commonSelect(options: TypedOptions<T>): T? {
return when (val index = select(options.options)) {
-1 -> null
else -> options[index]
}
}
90 changes: 90 additions & 0 deletions okio/src/commonTest/kotlin/okio/TypedOptionsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2024 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 okio

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import okio.ByteString.Companion.encodeUtf8

class TypedOptionsTest {
@Test
fun happyPath() {
val colors = listOf("Red", "Green", "Blue")
val colorOptions = TypedOptions.of(colors) { it.lowercase().encodeUtf8() }
val buffer = Buffer().writeUtf8("bluegreenyellow")
assertEquals("Blue", buffer.select(colorOptions))
assertEquals("greenyellow", buffer.snapshot().utf8())
assertEquals("Green", buffer.select(colorOptions))
assertEquals("yellow", buffer.snapshot().utf8())
assertEquals(null, buffer.select(colorOptions))
assertEquals("yellow", buffer.snapshot().utf8())
}

@Test
fun typedOptionsConstructor() {
val colors = listOf("Red", "Green", "Blue")
val colorOptions = TypedOptions(
colors,
Options.of("red".encodeUtf8(), "green".encodeUtf8(), "blue".encodeUtf8()),
)
val buffer = Buffer().writeUtf8("bluegreenyellow")
assertEquals("Blue", buffer.select(colorOptions))
assertEquals("greenyellow", buffer.snapshot().utf8())
assertEquals("Green", buffer.select(colorOptions))
assertEquals("yellow", buffer.snapshot().utf8())
assertEquals(null, buffer.select(colorOptions))
assertEquals("yellow", buffer.snapshot().utf8())
}

@Test
fun typedOptionsConstructorEnforcesSizeMatch() {
val colors = listOf("Red", "Green", "Blue")
assertFailsWith<IllegalArgumentException> {
TypedOptions(
colors,
Options.of("red".encodeUtf8(), "green".encodeUtf8()),
)
}
}

@Test
fun listFunctionsWork() {
val colors = listOf("Red", "Green", "Blue")
val colorOptions = TypedOptions.of(colors) { it.lowercase().encodeUtf8() }
assertEquals(3, colorOptions.size)
assertEquals("Red", colorOptions[0])
assertEquals("Green", colorOptions[1])
assertEquals("Blue", colorOptions[2])
assertFailsWith<IndexOutOfBoundsException> {
colorOptions[3]
}
}

/**
* Confirm we can mutate the collection used to create our [TypedOptions] without corrupting its
* behavior.
*/
@Test
fun safeToMutateSourceCollectionAfterConstruction() {
val colors = mutableListOf("Red", "Green")
val colorOptions = TypedOptions.of(colors) { it.lowercase().encodeUtf8() }
colors[0] = "Black"

val buffer = Buffer().writeUtf8("red")
assertEquals("Red", buffer.select(colorOptions))
}
}
2 changes: 2 additions & 0 deletions okio/src/jvmMain/kotlin/okio/Buffer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel {

override fun select(options: Options): Int = commonSelect(options)

override fun <T : Any> select(options: TypedOptions<T>): T? = commonSelect(options)

@Throws(EOFException::class)
override fun readFully(sink: Buffer, byteCount: Long): Unit = commonReadFully(sink, byteCount)

Expand Down
3 changes: 3 additions & 0 deletions okio/src/jvmMain/kotlin/okio/BufferedSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ actual sealed interface BufferedSource : Source, ReadableByteChannel {
@Throws(IOException::class)
actual fun select(options: Options): Int

@Throws(IOException::class)
actual fun <T : Any> select(options: TypedOptions<T>): T?

@Throws(IOException::class)
actual fun readByteArray(): ByteArray

Expand Down
1 change: 1 addition & 0 deletions okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ internal actual class RealBufferedSource actual constructor(
override fun readByteString(): ByteString = commonReadByteString()
override fun readByteString(byteCount: Long): ByteString = commonReadByteString(byteCount)
override fun select(options: Options): Int = commonSelect(options)
override fun <T : Any> select(options: TypedOptions<T>): T? = commonSelect(options)
override fun readByteArray(): ByteArray = commonReadByteArray()
override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
override fun read(sink: ByteArray): Int = read(sink, 0, sink.size)
Expand Down
2 changes: 2 additions & 0 deletions okio/src/nonJvmMain/kotlin/okio/Buffer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ actual class Buffer : BufferedSource, BufferedSink {

override fun select(options: Options): Int = commonSelect(options)

override fun <T : Any> select(options: TypedOptions<T>): T? = commonSelect(options)

override fun readByteArray(): ByteArray = commonReadByteArray()

override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
Expand Down
2 changes: 2 additions & 0 deletions okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ actual sealed interface BufferedSource : Source {

actual fun select(options: Options): Int

actual fun <T : Any> select(options: TypedOptions<T>): T?

actual fun readByteArray(): ByteArray

actual fun readByteArray(byteCount: Long): ByteArray
Expand Down
1 change: 1 addition & 0 deletions okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ internal actual class RealBufferedSource actual constructor(
override fun readByteString(): ByteString = commonReadByteString()
override fun readByteString(byteCount: Long): ByteString = commonReadByteString(byteCount)
override fun select(options: Options): Int = commonSelect(options)
override fun <T : Any> select(options: TypedOptions<T>): T? = commonSelect(options)
override fun readByteArray(): ByteArray = commonReadByteArray()
override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
override fun read(sink: ByteArray): Int = read(sink, 0, sink.size)
Expand Down

0 comments on commit 0d76d41

Please sign in to comment.