diff --git a/api/knbt.api b/api/knbt.api index 3c60ab24..6729a33c 100644 --- a/api/knbt.api +++ b/api/knbt.api @@ -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; diff --git a/src/commonMain/kotlin/NbtFormat.kt b/src/commonMain/kotlin/NbtFormat.kt index 36898611..64d77893 100644 --- a/src/commonMain/kotlin/NbtFormat.kt +++ b/src/commonMain/kotlin/NbtFormat.kt @@ -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 encodeToNamedNbtTag(serializer: SerializationStrategy, value: T): NbtNamed { + 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 decodeFromNamedNbtTag(deserializer: DeserializationStrategy, namedTag: NbtNamed): 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) + } } /** @@ -113,3 +152,21 @@ public inline fun NbtFormat.encodeToNbtTag(value: T): NbtTag = */ public inline fun 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 NbtFormat.encodeToNamedNbtTag(value: T): NbtNamed = + 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 NbtFormat.decodeFromNamedNbtTag(namedTag: NbtNamed): T = + decodeFromNamedNbtTag(serializersModule.serializer(), namedTag) diff --git a/src/commonTest/kotlin/NbtFormatTest_NamedNbtTag.kt b/src/commonTest/kotlin/NbtFormatTest_NamedNbtTag.kt new file mode 100644 index 00000000..625a7886 --- /dev/null +++ b/src/commonTest/kotlin/NbtFormatTest_NamedNbtTag.kt @@ -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( + val value: T, + val serializer: KSerializer, + ) + + 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> // 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 { + nbt.decodeFromNamedNbtTag(value.serializer, differentlyNamedNbtTag) + } + + assertEquals( + "Expected tag named '$name', but got '${differentlyNamedNbtTag.name}'", + failure.message, + "failure message" + ) + } +}