Skip to content

Commit

Permalink
WIP Add encodeToNamedNbtTag and decodeFromNamedNbtTag
Browse files Browse the repository at this point in the history
  • Loading branch information
BenWoodworth committed Jun 17, 2024
1 parent c579707 commit ac18958
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 0 deletions.
2 changes: 2 additions & 0 deletions api/knbt.api
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ public final class net/benwoodworth/knbt/NbtFloat$Companion {

public class net/benwoodworth/knbt/NbtFormat : kotlinx/serialization/SerialFormat {
public static final field Default Lnet/benwoodworth/knbt/NbtFormat$Default;
public final fun decodeFromNamedNbtTag (Lkotlinx/serialization/DeserializationStrategy;Lnet/benwoodworth/knbt/NbtNamed;)Ljava/lang/Object;
public final fun decodeFromNbtTag (Lkotlinx/serialization/DeserializationStrategy;Lnet/benwoodworth/knbt/NbtTag;)Ljava/lang/Object;
public final fun encodeToNamedNbtTag (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Lnet/benwoodworth/knbt/NbtNamed;
public final fun encodeToNbtTag (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Lnet/benwoodworth/knbt/NbtTag;
public fun getConfiguration ()Lnet/benwoodworth/knbt/NbtFormatConfiguration;
public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
Expand Down
57 changes: 57 additions & 0 deletions src/commonMain/kotlin/NbtFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,45 @@ public open class NbtFormat internal constructor(

return decoder.decodeSerializableValue(deserializer)
}

/**
* Serializes the given [value] into an equivalent named [NbtTag] using the given [serializer].
*
* @throws [SerializationException] if the given value cannot be serialized to NBT
*/
public fun <T> encodeToNamedNbtTag(serializer: SerializationStrategy<T>, value: T): NbtNamed<NbtTag> {
val tag = encodeToNbtTag(serializer, value)

val name = (tag as? NbtCompound)
?.content?.keys?.singleOrNull()
?: throw NbtEncodingException( // TODO Encoder should handle this
EmptyNbtContext,
"A named NbtTag only supports ${NbtTagType.TAG_Compound} with one entry"
)

return NbtNamed(name, tag)
}

/**
* Deserializes the given [namedTag] into a value of type [T] using the given [deserializer].
*
* @throws [SerializationException] if the given NBT tag is not a valid NBT input for the type [T]
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
*/
public fun <T> decodeFromNamedNbtTag(deserializer: DeserializationStrategy<T>, namedTag: NbtNamed<NbtTag>): T {
val unnamedTag = (namedTag.value as? NbtCompound)
?.content?.values?.singleOrNull()
?: throw NbtDecodingException( // TODO Decoder should handle this
EmptyNbtContext,
"A named NbtTag only supports ${NbtTagType.TAG_Compound} with one entry"
)

val renamedTag = buildNbtCompound {
put(namedTag.name, unnamedTag)
}

return decodeFromNbtTag(deserializer, renamedTag)
}
}

/**
Expand Down Expand Up @@ -113,3 +152,21 @@ public inline fun <reified T> NbtFormat.encodeToNbtTag(value: T): NbtTag =
*/
public inline fun <reified T> NbtFormat.decodeFromNbtTag(tag: NbtTag): T =
decodeFromNbtTag(serializersModule.serializer(), tag)

/**
* Serializes the given [value] into an equivalent named [NbtTag] using a serializer retrieved from the reified type
* parameter.
*
* @throws [SerializationException] if the given value cannot be serialized to NBT
*/
public inline fun <reified T> NbtFormat.encodeToNamedNbtTag(value: T): NbtNamed<NbtTag> =
encodeToNamedNbtTag(serializersModule.serializer(), value)

/**
* Deserializes the given [namedTag] into a value of type [T] using a serializer retrieved from the reified type parameter.
*
* @throws [SerializationException] if the given NBT tag is not a valid NBT input for the type [T]
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
*/
public inline fun <reified T> NbtFormat.decodeFromNamedNbtTag(namedTag: NbtNamed<NbtTag>): T =
decodeFromNamedNbtTag(serializersModule.serializer(), namedTag)
101 changes: 101 additions & 0 deletions src/commonTest/kotlin/NbtFormatTest_NamedNbtTag.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package net.benwoodworth.knbt

import com.benwoodworth.parameterize.parameter
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import net.benwoodworth.knbt.internal.NbtDecodingException
import net.benwoodworth.knbt.internal.nbtName
import net.benwoodworth.knbt.test.parameterizeTest
import net.benwoodworth.knbt.test.parameters.parameterOfNbtFormats
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

@Suppress("ClassName")
class NbtFormatTest_NamedNbtTag {
private data class SerializableValueWithNbtName<T>(
val value: T,
val serializer: KSerializer<T>,
)

private val valuesWithStaticNbtNames = buildList {
@Serializable
@NbtName("Name")
class Value {
override fun toString(): String = "Value"
override fun equals(other: Any?): Boolean = other is Value
override fun hashCode(): Int = this::class.hashCode()
}

add(SerializableValueWithNbtName(Value(), Value.serializer()))


@Serializable
@NbtName("DifferentName")
class DifferentValue {
override fun toString(): String = "DifferentValue"
override fun equals(other: Any?): Boolean = other is DifferentValue
override fun hashCode(): Int = this::class.hashCode()
}

add(SerializableValueWithNbtName(DifferentValue(), DifferentValue.serializer()))
}.let {
@Suppress("UNCHECKED_CAST")
it as List<SerializableValueWithNbtName<Any?>> // KT-68606: Remove cast
}


@Test
fun encode_should_return_the_NBT_name_of_the_value() = parameterizeTest {
val nbt by parameterOfNbtFormats()
val value by parameter(valuesWithStaticNbtNames)

val namedNbtTag = nbt.encodeToNamedNbtTag(value.serializer, value.value)
assertEquals(value.serializer.descriptor.nbtName, namedNbtTag.name)
}

@Test
fun encode_should_return_the_same_tag_as_encoding_to_an_nbt_tag() = parameterizeTest {
val nbt by parameterOfNbtFormats()
val value by parameter(valuesWithStaticNbtNames)

val nbtTag = nbt.encodeToNbtTag(value.serializer, value.value)
val namedNbtTag = nbt.encodeToNamedNbtTag(value.serializer, value.value)

assertEquals(nbtTag, namedNbtTag.value)
}

@Test
fun decode_value_with_static_NBT_name_should_succeed_with_same_name() = parameterizeTest {
val nbt by parameterOfNbtFormats()
val value by parameter(valuesWithStaticNbtNames)

val name = value.serializer.descriptor.nbtName!!
val nbtTag = nbt.encodeToNbtTag(value.serializer, value.value)

val namedNbtTag = NbtNamed(name, nbtTag)
nbt.decodeFromNamedNbtTag(value.serializer, namedNbtTag)
}

@Test
fun decode_value_with_static_NBT_name_should_fail_with_different_name() =
parameterizeTest { // TODO Move to NbtNameTest?
val nbt by parameterOfNbtFormats()
val value by parameter(valuesWithStaticNbtNames)

val name = value.serializer.descriptor.nbtName!!
val nbtTag = nbt.encodeToNbtTag(value.serializer, value.value)

val differentlyNamedNbtTag = NbtNamed("different-than-$name", nbtTag)

val failure = assertFailsWith<NbtDecodingException> {
nbt.decodeFromNamedNbtTag(value.serializer, differentlyNamedNbtTag)
}

assertEquals(
"Expected tag named '$name', but got '${differentlyNamedNbtTag.name}'",
failure.message,
"failure message"
)
}
}

0 comments on commit ac18958

Please sign in to comment.