From 9ae8c573a532316f5bb17d3548beaa1eea5b8b1a Mon Sep 17 00:00:00 2001 From: Ben Woodworth Date: Thu, 11 Apr 2024 12:59:56 -0400 Subject: [PATCH] Add `nameRootClasses` configuration option The default root class nesting behavior will eventually be removed due to it being problematic for a number of reasons. (See #29) But until then, this provides a way to opt out of that behavior entirely. --- api/knbt.api | 9 ++ src/commonMain/kotlin/Nbt.kt | 6 +- src/commonMain/kotlin/NbtConfiguration.kt | 2 + src/commonMain/kotlin/NbtFormat.kt | 35 ++++- .../kotlin/NbtFormatConfiguration.kt | 1 + src/commonMain/kotlin/StringifiedNbt.kt | 8 +- .../kotlin/StringifiedNbtConfiguration.kt | 2 + .../kotlin/NbtFormatConfigurationTest.kt | 23 +++- src/commonTest/kotlin/NestRootClassesTest.kt | 124 ++++++++++++++++++ src/commonTest/kotlin/Util.kt | 6 + 10 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 src/commonTest/kotlin/NestRootClassesTest.kt diff --git a/api/knbt.api b/api/knbt.api index f5c40f6b..928a23a7 100644 --- a/api/knbt.api +++ b/api/knbt.api @@ -50,6 +50,7 @@ public final class net/benwoodworth/knbt/NbtBuilder : net/benwoodworth/knbt/NbtF public final fun getCompressionLevel ()Ljava/lang/Integer; public fun getEncodeDefaults ()Z public fun getIgnoreUnknownKeys ()Z + public fun getNameRootClasses ()Z public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; public final fun getVariant ()Lnet/benwoodworth/knbt/NbtVariant; public fun setClassDiscriminator (Ljava/lang/String;)V @@ -57,6 +58,7 @@ public final class net/benwoodworth/knbt/NbtBuilder : net/benwoodworth/knbt/NbtF public final fun setCompressionLevel (Ljava/lang/Integer;)V public fun setEncodeDefaults (Z)V public fun setIgnoreUnknownKeys (Z)V + public fun setNameRootClasses (Z)V public fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V public final fun setVariant (Lnet/benwoodworth/knbt/NbtVariant;)V } @@ -232,6 +234,7 @@ public final class net/benwoodworth/knbt/NbtConfiguration : net/benwoodworth/knb public final fun getCompressionLevel ()Ljava/lang/Integer; public fun getEncodeDefaults ()Z public fun getIgnoreUnknownKeys ()Z + public fun getNameRootClasses ()Z public final fun getVariant ()Lnet/benwoodworth/knbt/NbtVariant; public fun toString ()Ljava/lang/String; } @@ -347,10 +350,12 @@ public abstract interface class net/benwoodworth/knbt/NbtFormatBuilder { public abstract fun getClassDiscriminator ()Ljava/lang/String; public abstract fun getEncodeDefaults ()Z public abstract fun getIgnoreUnknownKeys ()Z + public abstract fun getNameRootClasses ()Z public abstract fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; public abstract fun setClassDiscriminator (Ljava/lang/String;)V public abstract fun setEncodeDefaults (Z)V public abstract fun setIgnoreUnknownKeys (Z)V + public abstract fun setNameRootClasses (Z)V public abstract fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V } @@ -358,6 +363,7 @@ public abstract interface class net/benwoodworth/knbt/NbtFormatConfiguration { public abstract fun getClassDiscriminator ()Ljava/lang/String; public abstract fun getEncodeDefaults ()Z public abstract fun getIgnoreUnknownKeys ()Z + public abstract fun getNameRootClasses ()Z } public final class net/benwoodworth/knbt/NbtInt : net/benwoodworth/knbt/NbtTag { @@ -738,12 +744,14 @@ public final class net/benwoodworth/knbt/StringifiedNbtBuilder : net/benwoodwort public fun getClassDiscriminator ()Ljava/lang/String; public fun getEncodeDefaults ()Z public fun getIgnoreUnknownKeys ()Z + public fun getNameRootClasses ()Z public final fun getPrettyPrint ()Z public final fun getPrettyPrintIndent ()Ljava/lang/String; public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; public fun setClassDiscriminator (Ljava/lang/String;)V public fun setEncodeDefaults (Z)V public fun setIgnoreUnknownKeys (Z)V + public fun setNameRootClasses (Z)V public final fun setPrettyPrint (Z)V public final fun setPrettyPrintIndent (Ljava/lang/String;)V public fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V @@ -753,6 +761,7 @@ public final class net/benwoodworth/knbt/StringifiedNbtConfiguration : net/benwo public fun getClassDiscriminator ()Ljava/lang/String; public fun getEncodeDefaults ()Z public fun getIgnoreUnknownKeys ()Z + public fun getNameRootClasses ()Z public final fun getPrettyPrint ()Z public final fun getPrettyPrintIndent ()Ljava/lang/String; public fun toString ()Ljava/lang/String; diff --git a/src/commonMain/kotlin/Nbt.kt b/src/commonMain/kotlin/Nbt.kt index 5a201604..c20e6465 100644 --- a/src/commonMain/kotlin/Nbt.kt +++ b/src/commonMain/kotlin/Nbt.kt @@ -53,6 +53,7 @@ private object DefaultNbt : Nbt( encodeDefaults = false, ignoreUnknownKeys = false, classDiscriminator = "type", + nameRootClasses = true, ), serializersModule = EmptySerializersModule(), ) @@ -107,6 +108,8 @@ public class NbtBuilder internal constructor(nbt: Nbt) : NbtFormatBuilder { override var classDiscriminator: String = nbt.configuration.classDiscriminator + override var nameRootClasses: Boolean = nbt.configuration.nameRootClasses + /** * Module with contextual and polymorphic serializers to be used in the resulting [Nbt] instance. */ @@ -131,7 +134,8 @@ public class NbtBuilder internal constructor(nbt: Nbt) : NbtFormatBuilder { compressionLevel = compressionLevel, encodeDefaults = encodeDefaults, ignoreUnknownKeys = ignoreUnknownKeys, - classDiscriminator = classDiscriminator + classDiscriminator = classDiscriminator, + nameRootClasses = nameRootClasses ), serializersModule = serializersModule, ) diff --git a/src/commonMain/kotlin/NbtConfiguration.kt b/src/commonMain/kotlin/NbtConfiguration.kt index 29641e4b..358c8cca 100644 --- a/src/commonMain/kotlin/NbtConfiguration.kt +++ b/src/commonMain/kotlin/NbtConfiguration.kt @@ -7,6 +7,7 @@ public class NbtConfiguration internal constructor( override val encodeDefaults: Boolean, override val ignoreUnknownKeys: Boolean, override val classDiscriminator: String, + override val nameRootClasses: Boolean, ) : NbtFormatConfiguration { override fun toString(): String = "NbtConfiguration(" + @@ -16,5 +17,6 @@ public class NbtConfiguration internal constructor( ", encodeDefaults=$encodeDefaults" + ", ignoreUnknownKeys=$ignoreUnknownKeys" + ", classDiscriminator='$classDiscriminator'" + + ", nameRootClasses=$nameRootClasses" + ")" } diff --git a/src/commonMain/kotlin/NbtFormat.kt b/src/commonMain/kotlin/NbtFormat.kt index 616260ad..e6e21342 100644 --- a/src/commonMain/kotlin/NbtFormat.kt +++ b/src/commonMain/kotlin/NbtFormat.kt @@ -1,6 +1,7 @@ package net.benwoodworth.knbt import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.internal.AbstractPolymorphicSerializer import kotlinx.serialization.modules.SerializersModule @@ -45,6 +46,32 @@ public sealed interface NbtFormatBuilder { */ public var classDiscriminator: String + /** + * Specifies whether classes serialized as the root NBT tag should be named with their + * [serial name][SerialDescriptor.serialName]. + * `true` by default. + * + * Specifically, a named tag is represented as a single entry in an NBT compound. Encoding root classes with names + * will nest them into a NBT compound using the serial name for the entry. This applies to serializers with + * [StructureKind.CLASS] and the [default polymorphic serializers][AbstractPolymorphicSerializer]. + * + * For example, based on the NBT spec's `test.nbt` file: + * ``` + * @Serializable + * @SerialName("hello world") + * class Test(val name: String) + * + * val test = Test(name = "Bananarama") + * + * // Encoding `test` with naming root classes: + * // {"hello world":{name:"Bananarama"}} + * + * // Encoding `test` without naming root classes: + * // {name:"Bananarama"} + * ``` + */ + public var nameRootClasses: Boolean + /** * Module with contextual and polymorphic serializers to be used in the resulting [NbtFormat] instance. */ @@ -68,8 +95,8 @@ public inline fun NbtFormat.decodeFromNbtTag(tag: NbtTag): T = @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) internal fun NbtFormat.encodeToNbtWriter(writer: NbtWriter, serializer: SerializationStrategy, value: T) { val rootSerializer = if ( - serializer.descriptor.kind == StructureKind.CLASS || - serializer is AbstractPolymorphicSerializer + configuration.nameRootClasses && + (serializer.descriptor.kind == StructureKind.CLASS || serializer is AbstractPolymorphicSerializer) ) { RootClassSerializer(serializer) } else { @@ -83,8 +110,8 @@ internal fun NbtFormat.encodeToNbtWriter(writer: NbtWriter, serializer: Seri @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) internal fun NbtFormat.decodeFromNbtReader(reader: NbtReader, deserializer: DeserializationStrategy): T { val rootDeserializer = if ( - deserializer.descriptor.kind == StructureKind.CLASS || - deserializer is AbstractPolymorphicSerializer + configuration.nameRootClasses && + (deserializer.descriptor.kind == StructureKind.CLASS || deserializer is AbstractPolymorphicSerializer) ) { RootClassDeserializer(deserializer) } else { diff --git a/src/commonMain/kotlin/NbtFormatConfiguration.kt b/src/commonMain/kotlin/NbtFormatConfiguration.kt index 15d14cf1..3f19905a 100644 --- a/src/commonMain/kotlin/NbtFormatConfiguration.kt +++ b/src/commonMain/kotlin/NbtFormatConfiguration.kt @@ -4,4 +4,5 @@ public sealed interface NbtFormatConfiguration { public val encodeDefaults: Boolean public val ignoreUnknownKeys: Boolean public val classDiscriminator: String + public val nameRootClasses: Boolean } diff --git a/src/commonMain/kotlin/StringifiedNbt.kt b/src/commonMain/kotlin/StringifiedNbt.kt index af8de299..f4c4cf80 100644 --- a/src/commonMain/kotlin/StringifiedNbt.kt +++ b/src/commonMain/kotlin/StringifiedNbt.kt @@ -23,7 +23,8 @@ public sealed class StringifiedNbt( ignoreUnknownKeys = false, prettyPrint = false, prettyPrintIndent = " ", - classDiscriminator = "type" + classDiscriminator = "type", + nameRootClasses = true ), serializersModule = EmptySerializersModule(), ) @@ -88,6 +89,8 @@ public class StringifiedNbtBuilder internal constructor(stringifiedNbt: Stringif override var classDiscriminator: String = stringifiedNbt.configuration.classDiscriminator + override var nameRootClasses: Boolean = stringifiedNbt.configuration.nameRootClasses + /** * Module with contextual and polymorphic serializers to be used in the resulting [StringifiedNbt] instance. */ @@ -113,7 +116,8 @@ public class StringifiedNbtBuilder internal constructor(stringifiedNbt: Stringif ignoreUnknownKeys = ignoreUnknownKeys, prettyPrint = prettyPrint, prettyPrintIndent = prettyPrintIndent, - classDiscriminator = classDiscriminator + classDiscriminator = classDiscriminator, + nameRootClasses = nameRootClasses ), serializersModule = serializersModule, ) diff --git a/src/commonMain/kotlin/StringifiedNbtConfiguration.kt b/src/commonMain/kotlin/StringifiedNbtConfiguration.kt index 59010160..437b0e5d 100644 --- a/src/commonMain/kotlin/StringifiedNbtConfiguration.kt +++ b/src/commonMain/kotlin/StringifiedNbtConfiguration.kt @@ -7,6 +7,7 @@ public class StringifiedNbtConfiguration internal constructor( @ExperimentalNbtApi public val prettyPrintIndent: String, override val classDiscriminator: String, + override val nameRootClasses: Boolean, ) : NbtFormatConfiguration { @OptIn(ExperimentalNbtApi::class) override fun toString(): String = @@ -16,5 +17,6 @@ public class StringifiedNbtConfiguration internal constructor( ", prettyPrint=$prettyPrint" + ", prettyPrintIndent='$prettyPrintIndent'" + ", classDiscriminator='$classDiscriminator'" + + ", nameRootClasses=$nameRootClasses" + ")" } diff --git a/src/commonTest/kotlin/NbtFormatConfigurationTest.kt b/src/commonTest/kotlin/NbtFormatConfigurationTest.kt index d3e54544..701f1722 100644 --- a/src/commonTest/kotlin/NbtFormatConfigurationTest.kt +++ b/src/commonTest/kotlin/NbtFormatConfigurationTest.kt @@ -1,6 +1,5 @@ package net.benwoodworth.knbt -import com.benwoodworth.parameterize.parameter import com.benwoodworth.parameterize.parameterOf import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -99,4 +98,26 @@ class NbtFormatConfigurationTest { assertEquals(classDiscriminatorValue, nbt.configuration.classDiscriminator) } + + @Test + fun name_root_classes_default_should_be_true() = parameterizeTest { + var actualDefault: Boolean? = null + + parameterizedNbtFormat { + actualDefault = nameRootClasses + } + + assertEquals(true, actualDefault) + } + + @Test + fun name_root_classes_should_apply_when_built() = parameterizeTest { + val nameRootClassesValue by parameterOf(true, false) + + val nbt = parameterizedNbtFormat { + nameRootClasses = nameRootClassesValue + } + + assertEquals(nameRootClassesValue, nbt.configuration.nameRootClasses) + } } diff --git a/src/commonTest/kotlin/NestRootClassesTest.kt b/src/commonTest/kotlin/NestRootClassesTest.kt new file mode 100644 index 00000000..d2eaadbd --- /dev/null +++ b/src/commonTest/kotlin/NestRootClassesTest.kt @@ -0,0 +1,124 @@ +package net.benwoodworth.knbt + +import com.benwoodworth.parameterize.parameter +import com.benwoodworth.parameterize.parameterOf +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.PolymorphicSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalSerializationApi::class) +class NestRootClassesTest { + private class TestCase( + val serializer: KSerializer, + val value: T, + val serializersModule: SerializersModule = EmptySerializersModule() + ) { + override fun toString(): String = + serializer.descriptor.serialName + + fun encodeToNbtTag(nbt: NbtFormat): NbtTag = + nbt.encodeToNbtTag(serializer, value) + + fun decodeFromNbtTag(nbt: NbtFormat, tag: NbtTag): T = + nbt.decodeFromNbtTag(serializer, tag) + } + + @Serializable + private data class Class(val element: String) + + private abstract class AbstractClass { + @Serializable + data class Impl(val element: String) : AbstractClass() + } + + private interface Interface { + @Serializable + data class Impl(val element: String) : Interface + } + + @Serializable + private sealed class SealedClass { + @Serializable + data class Impl(val element: String) : SealedClass() + } + + @Serializable + private sealed interface SealedInterface { + @Serializable + data class Impl(val element: String) : SealedInterface + } + + private val testCases = listOf( + TestCase( + Class.serializer(), + Class("value") + ), + TestCase( + PolymorphicSerializer(AbstractClass::class), + AbstractClass.Impl("value"), + SerializersModule { + polymorphic(AbstractClass::class, AbstractClass.Impl::class, AbstractClass.Impl.serializer()) + } + ), + TestCase( + PolymorphicSerializer(Interface::class), + Interface.Impl("value"), + SerializersModule { + polymorphic(Interface::class, Interface.Impl::class, Interface.Impl.serializer()) + } + ), + TestCase( + SealedClass.serializer(), + SealedClass.Impl("value") + ), + TestCase( + SealedInterface.serializer(), + SealedInterface.Impl("value") + ) + ) + + @Test + fun class_encoded_with_nesting_be_class_without_nesting_wrapped_in_serial_name() = parameterizeTest { + val testCase by parameter(testCases) + + val nbtWithoutNesting = parameterizedNbtFormat { + nameRootClasses = false + serializersModule = testCase.serializersModule + } + + val nbtWithNesting = NbtFormat(nbtWithoutNesting) { + nameRootClasses = true + } + + val tagWithoutNesting = testCase.encodeToNbtTag(nbtWithoutNesting) + val tagWithNesting = testCase.encodeToNbtTag(nbtWithNesting) + + val expectedTagWithNesting = buildNbtCompound { + put(testCase.serializer.descriptor.serialName, tagWithoutNesting) + } + + assertEquals(expectedTagWithNesting, tagWithNesting) + } + + @Test + fun should_correctly_serialize_class_with_name_root_classes_configured() = parameterizeTest { + val testCase by parameter(testCases) + + val nameRootClasses by parameterOf(true, false) + + val nbt = parameterizedNbtFormat { + this.nameRootClasses = nameRootClasses + serializersModule = testCase.serializersModule + } + + val encoded = testCase.encodeToNbtTag(nbt) + val decoded = testCase.decodeFromNbtTag(nbt, encoded) + + assertEquals(testCase.value, decoded, "Decoded tag") + } +} diff --git a/src/commonTest/kotlin/Util.kt b/src/commonTest/kotlin/Util.kt index 4738f4cc..452560d8 100644 --- a/src/commonTest/kotlin/Util.kt +++ b/src/commonTest/kotlin/Util.kt @@ -123,3 +123,9 @@ fun ParameterizeScope.parameterizedNbtFormat( return NbtFormat(builderAction) } + +fun NbtFormat(from: NbtFormat, builderAction: NbtFormatBuilder.() -> Unit): NbtFormat = + when (from) { + is Nbt -> Nbt(from, builderAction) + is StringifiedNbt -> StringifiedNbt(from, builderAction) + }