Skip to content

Commit

Permalink
Add nameRootClasses configuration option
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
BenWoodworth committed May 7, 2024
1 parent ed86b22 commit 9ae8c57
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 8 deletions.
9 changes: 9 additions & 0 deletions api/knbt.api
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ 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
public final fun setCompression (Lnet/benwoodworth/knbt/NbtCompression;)V
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
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -347,17 +350,20 @@ 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
}

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 {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/commonMain/kotlin/Nbt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private object DefaultNbt : Nbt(
encodeDefaults = false,
ignoreUnknownKeys = false,
classDiscriminator = "type",
nameRootClasses = true,
),
serializersModule = EmptySerializersModule(),
)
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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,
)
Expand Down
2 changes: 2 additions & 0 deletions src/commonMain/kotlin/NbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(" +
Expand All @@ -16,5 +17,6 @@ public class NbtConfiguration internal constructor(
", encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", classDiscriminator='$classDiscriminator'" +
", nameRootClasses=$nameRootClasses" +
")"
}
35 changes: 31 additions & 4 deletions src/commonMain/kotlin/NbtFormat.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
*/
Expand All @@ -68,8 +95,8 @@ public inline fun <reified T> NbtFormat.decodeFromNbtTag(tag: NbtTag): T =
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
internal fun <T> NbtFormat.encodeToNbtWriter(writer: NbtWriter, serializer: SerializationStrategy<T>, 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 {
Expand All @@ -83,8 +110,8 @@ internal fun <T> NbtFormat.encodeToNbtWriter(writer: NbtWriter, serializer: Seri
@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
internal fun <T> NbtFormat.decodeFromNbtReader(reader: NbtReader, deserializer: DeserializationStrategy<T>): 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 {
Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/NbtFormatConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public sealed interface NbtFormatConfiguration {
public val encodeDefaults: Boolean
public val ignoreUnknownKeys: Boolean
public val classDiscriminator: String
public val nameRootClasses: Boolean
}
8 changes: 6 additions & 2 deletions src/commonMain/kotlin/StringifiedNbt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public sealed class StringifiedNbt(
ignoreUnknownKeys = false,
prettyPrint = false,
prettyPrintIndent = " ",
classDiscriminator = "type"
classDiscriminator = "type",
nameRootClasses = true
),
serializersModule = EmptySerializersModule(),
)
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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,
)
Expand Down
2 changes: 2 additions & 0 deletions src/commonMain/kotlin/StringifiedNbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -16,5 +17,6 @@ public class StringifiedNbtConfiguration internal constructor(
", prettyPrint=$prettyPrint" +
", prettyPrintIndent='$prettyPrintIndent'" +
", classDiscriminator='$classDiscriminator'" +
", nameRootClasses=$nameRootClasses" +
")"
}
23 changes: 22 additions & 1 deletion src/commonTest/kotlin/NbtFormatConfigurationTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
124 changes: 124 additions & 0 deletions src/commonTest/kotlin/NestRootClassesTest.kt
Original file line number Diff line number Diff line change
@@ -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<T>(
val serializer: KSerializer<T>,
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")
}
}
6 changes: 6 additions & 0 deletions src/commonTest/kotlin/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 9ae8c57

Please sign in to comment.