Skip to content

Commit

Permalink
Add nestRootClasses 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 Apr 11, 2024
1 parent ed86b22 commit 7fd010c
Show file tree
Hide file tree
Showing 10 changed files with 207 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 getNestRootClasses ()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 setNestRootClasses (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 getNestRootClasses ()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 getNestRootClasses ()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 setNestRootClasses (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 getNestRootClasses ()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 getNestRootClasses ()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 setNestRootClasses (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 getNestRootClasses ()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",
nestRootClasses = 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 nestRootClasses: Boolean = nbt.configuration.nestRootClasses

/**
* 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,
nestRootClasses = nestRootClasses
),
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 nestRootClasses: Boolean,
) : NbtFormatConfiguration {
override fun toString(): String =
"NbtConfiguration(" +
Expand All @@ -16,5 +17,6 @@ public class NbtConfiguration internal constructor(
", encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", classDiscriminator='$classDiscriminator'" +
", nestRootClasses=$nestRootClasses" +
")"
}
34 changes: 30 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,31 @@ public sealed interface NbtFormatBuilder {
*/
public var classDiscriminator: String

/**
* Whether classes should be nested under their [serial name][SerialDescriptor.serialName] when serialized as the
* root of the NBT.
* `true` by default.
*
* This applies to classes and interface serializers. Specifically with [StructureKind.CLASS] and the
* [default polymorphic serializers][AbstractPolymorphicSerializer] for abstract/sealed classes/interfaces.
*
* 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 nesting root classes:
* // {"hello world":{name:"Bananarama"}}
*
* // Encoding `test` without nesting root classes:
* // {name:"Bananarama"}
* ```
*/
public var nestRootClasses: Boolean

/**
* Module with contextual and polymorphic serializers to be used in the resulting [NbtFormat] instance.
*/
Expand All @@ -68,8 +94,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.nestRootClasses &&
(serializer.descriptor.kind == StructureKind.CLASS || serializer is AbstractPolymorphicSerializer)
) {
RootClassSerializer(serializer)
} else {
Expand All @@ -83,8 +109,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.nestRootClasses &&
(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 nestRootClasses: 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",
nestRootClasses = 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 nestRootClasses: Boolean = stringifiedNbt.configuration.nestRootClasses

/**
* 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,
nestRootClasses = nestRootClasses
),
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 nestRootClasses: 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'" +
", nestRootClasses=$nestRootClasses" +
")"
}
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 nest_root_classes_default_should_be_true() = parameterizeTest {
var actualDefault: Boolean? = null

parameterizedNbtFormat {
actualDefault = nestRootClasses
}

assertEquals(true, actualDefault)
}

@Test
fun nest_root_classes_should_apply_when_built() = parameterizeTest {
val nestRootClassesValue by parameterOf(true, false)

val nbt = parameterizedNbtFormat {
nestRootClasses = nestRootClassesValue
}

assertEquals(nestRootClassesValue, nbt.configuration.nestRootClasses)
}
}
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 {
nestRootClasses = false
serializersModule = testCase.serializersModule
}

val nbtWithNesting = NbtFormat(nbtWithoutNesting) {
nestRootClasses = 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_nest_root_classes_configured() = parameterizeTest {
val testCase by parameter(testCases)

val nestRootClasses by parameterOf(true, false)

val nbt = parameterizedNbtFormat {
this.nestRootClasses = nestRootClasses
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 7fd010c

Please sign in to comment.