diff --git a/examples/DYNAMIC_TAG_NAMES.md b/examples/DYNAMIC_TAG_NAMES.md index 64164ac9b..4ca562c26 100644 --- a/examples/DYNAMIC_TAG_NAMES.md +++ b/examples/DYNAMIC_TAG_NAMES.md @@ -43,22 +43,10 @@ object CompatContainerSerializer: CommonContainerSerializer() { [ContainerSerializer](src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/ContainerSerializer.kt) ```kotlin -/** - * This version of the serializer uses the new delegateFormat method on [XML.XmlInput] and [XML.XmlOutput] - * to inherit configuration and serializerModules. - */ -object ContainerSerializer: CommonContainerSerializer() { - override fun delegateFormat(decoder: Decoder) = (decoder as XML.XmlInput).delegateFormat() - override fun delegateFormat(encoder: Encoder) = (encoder as XML.XmlOutput).delegateFormat() -} -``` - -[CommonContainerSerializer](src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/CommonContainerSerializer.kt) -```kotlin /** * A common base class that contains the actual code needed to serialize/deserialize the container. */ -abstract class CommonContainerSerializer: KSerializer { +class ContainerSerializer : XmlSerializer { /** We need to have the serializer for the elements */ private val elementSerializer = serializer() @@ -69,93 +57,105 @@ abstract class CommonContainerSerializer: KSerializer { override fun deserialize(decoder: Decoder): Container { // XmlInput is designed as an interface to test for to allow custom serializers - if (decoder is XML.XmlInput) { // We treat XML different, using a separate method for clarity - return deserializeDynamic(decoder, decoder.input) - } else { // Simple default decoder implementation that delegates parsing the data to the ListSerializer - val data = decoder.decodeStructure(descriptor) { - decodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer)) - } - return Container(data) + val data = decoder.decodeStructure(descriptor) { + decodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer)) } + // Simple default decoder implementation that delegates parsing the data to the ListSerializer + return Container(data) } - /** - * This function is the meat to deserializing the container with dynamic tag names. Note that - * because we use xml there is no point in going through the (anonymous) list dance. Doing that - * would be an additional complication. - */ - fun deserializeDynamic(decoder: Decoder, reader: XmlReader): Container { - val xml = delegateFormat(decoder) // get the format for deserializing + override fun serialize(encoder: Encoder, value: Container) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer), value.data) + } + } + + override fun deserializeXML( + decoder: Decoder, + input: XmlReader, + previousValue: Container?, + isValueChild: Boolean + ): Container { + // This delegate format allows for reusing the settings from the outer format. + val xml = (decoder as XML.XmlInput).delegateFormat() // We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is // its (only) child. val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0) - // A list to collect the data val dataList = mutableListOf() - decoder.decodeStructure(descriptor) { // Finding the children is actually not left to the serialization framework, but // done by "hand" - while (reader.next() != EventType.END_ELEMENT) { - when (reader.eventType) { + while (input.next() != EventType.END_ELEMENT) { + when (input.eventType) { EventType.COMMENT, EventType.IGNORABLE_WHITESPACE -> { // Comments and whitespace are just ignored } - EventType.ENTITY_REF, - EventType.TEXT -> if (reader.text.isNotBlank()) { - // Some parsers can return whitespace as text instead of ignorable whitespace - // Use the handler from the configuration to throw the exception. - xml.config.unknownChildHandler(reader, InputKind.Text, null, emptyList()) + EventType.ENTITY_REF, + EventType.TEXT -> { + if (input.text.isNotBlank()) { + // Some parsers can return whitespace as text instead of ignorable whitespace + + // Use the handler from the configuration to throw the exception. + @OptIn(ExperimentalXmlUtilApi::class) + xml.config.policy.handleUnknownContentRecovering( + input, + InputKind.Text, + elementXmlDescriptor, + null, + emptyList() + ) + } } // It's best to still check the name before parsing - EventType.START_ELEMENT -> if(reader.namespaceURI.isEmpty() && reader.localName.startsWith("Test_")) { - // When reading the child tag we use the DynamicTagReader to present normalized XML to the - // deserializer for elements - val filter = DynamicTagReader(reader, elementXmlDescriptor) - - // The test element can now be decoded as normal (with the filter applied) - val testElement = xml.decodeFromReader(elementSerializer, filter) - dataList.add(testElement) - } else { // handling unexpected tags - xml.config.unknownChildHandler(reader, InputKind.Element, reader.name, listOf("Test_??")) + EventType.START_ELEMENT -> { + if (input.namespaceURI.isEmpty() && input.localName.startsWith("Test_")) { + // When reading the child tag we use the DynamicTagReader to present normalized XML to the + // deserializer for elements + val filter = DynamicTagReader(input, elementXmlDescriptor) + + // The test element can now be decoded as normal (with the filter applied) + val testElement = xml.decodeFromReader(elementSerializer, filter) + dataList.add(testElement) + } else { // handling unexpected tags + @OptIn(ExperimentalXmlUtilApi::class) + xml.config.policy.handleUnknownContentRecovering( + input, + InputKind.Element, + elementXmlDescriptor, + input.name, + (0 until elementXmlDescriptor.elementsCount).map { + val e = elementXmlDescriptor.getElementDescriptor(it) + PolyInfo(e.tagName, it, e) + } + ) + } } - else -> { // other content that shouldn't happen + + else -> // other content that shouldn't happen throw XmlException("Unexpected tag content") - } } } } return Container(dataList) } - override fun serialize(encoder: Encoder, value: Container) { - if (encoder is XML.XmlOutput) { // When we are using the xml format use the serializeDynamic method - return serializeDynamic(encoder, encoder.target, value.data) - } else { // Otherwise just manually do the encoding that would have been generated - encoder.encodeStructure(descriptor) { - encodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer), value.data) - } - } - } - - /** - * This function provides the actual dynamic serialization - */ - fun serializeDynamic(encoder: Encoder, target: XmlWriter, data: List) { - val xml = delegateFormat(encoder) // get the format for deserializing + override fun serializeXML(encoder: Encoder, output: XmlWriter, value: Container, isValueChild: Boolean) { + val xml = + (encoder as XML.XmlOutput).delegateFormat() // This format keeps the settings from the outer serializer, this allows for + // serializing the children // We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is // its (only) child. val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0) - encoder.encodeStructure(descriptor) { // create the structure (will write the tags of Container) - for (element in data) { // write each element + for (element in value.data) { // write each element // We need a writer that does the renaming from the normal format to the dynamic format // It is passed the string of the id to add. - val writer = DynamicTagWriter(target, elementXmlDescriptor, element.id.toString()) + val writer = DynamicTagWriter(output, elementXmlDescriptor, element.id.toString()) // Normal delegate writing of the element xml.encodeToWriter(writer, elementSerializer, element) @@ -163,9 +163,6 @@ abstract class CommonContainerSerializer: KSerializer { } } - // These functions abstract away getting the delegate format in improved or compat way. - abstract fun delegateFormat(decoder: Decoder): XML - abstract fun delegateFormat(encoder: Encoder): XML } ``` @@ -181,13 +178,11 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : private var initDepth = reader.depth private val filterDepth: Int /** - * We want to be safe so only handle the content at relative depth 0. The way that depth is determined - * means that the depth is the depth after the tag (and end tags are thus one level lower than the tag (and its - * content). We correct for that here. + * We want to be safe so only handle the content at relative depth 0. */ get() = when (eventType) { - EventType.END_ELEMENT -> delegate.depth - initDepth + 1 - else -> delegate.depth - initDepth + EventType.END_ELEMENT -> delegate.depth - initDepth + else -> delegate.depth - initDepth } /** @@ -226,9 +221,10 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : */ override fun getAttributeNamespace(index: Int): String = when (filterDepth) { 0 -> when (index) { - 0 -> idAttrName.namespaceURI + 0 -> idAttrName.namespaceURI else -> super.getAttributeNamespace(index - 1) } + else -> super.getAttributeNamespace(index) } @@ -238,9 +234,10 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : */ override fun getAttributePrefix(index: Int): String = when (filterDepth) { 0 -> when (index) { - 0 -> idAttrName.prefix + 0 -> idAttrName.prefix else -> super.getAttributePrefix(index - 1) } + else -> super.getAttributePrefix(index) } @@ -250,9 +247,10 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : */ override fun getAttributeLocalName(index: Int): String = when (filterDepth) { 0 -> when (index) { - 0 -> idAttrName.localPart + 0 -> idAttrName.localPart else -> super.getAttributeLocalName(index - 1) } + else -> super.getAttributeLocalName(index) } @@ -262,9 +260,10 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : */ override fun getAttributeValue(index: Int): String = when (filterDepth) { 0 -> when (index) { - 0 -> idValue + 0 -> idValue else -> super.getAttributeValue(index - 1) } + else -> super.getAttributeValue(index) } @@ -276,7 +275,8 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : filterDepth == 0 && (nsUri ?: "") == idAttrName.namespaceURI && localName == idAttrName.localPart -> idValue - else -> super.getAttributeValue(nsUri, localName) + + else -> super.getAttributeValue(nsUri, localName) } /** @@ -440,4 +440,4 @@ private fun newExample(testElements: List) { println("Deserialized container:\n $deserializedData") } -``` \ No newline at end of file +``` diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 0eaed9196..01d6ef0e8 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -36,4 +36,5 @@ val autoModuleName = "net.devrieze.serialexamples" dependencies { implementation(projects.serialization) implementation(projects.serialutil) + testImplementation(projects.testutil) } diff --git a/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/ContainerSerializer.kt b/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/ContainerSerializer.kt index ae5438559..7d2cd6974 100644 --- a/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/ContainerSerializer.kt +++ b/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/ContainerSerializer.kt @@ -20,7 +20,6 @@ package net.devrieze.serialization.examples.dynamictagnames -import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor @@ -37,7 +36,7 @@ import nl.adaptivity.xmlutil.serialization.XML /** * A common base class that contains the actual code needed to serialize/deserialize the container. */ -class ContainerSerializer : KSerializer { +class ContainerSerializer : XmlSerializer { /** We need to have the serializer for the elements */ private val elementSerializer = serializer() @@ -48,40 +47,38 @@ class ContainerSerializer : KSerializer { override fun deserialize(decoder: Decoder): Container { // XmlInput is designed as an interface to test for to allow custom serializers - return when (decoder) { - is XML.XmlInput -> // We treat XML different, using a separate method for clarity - deserializeDynamic(decoder, decoder.input) + val data = decoder.decodeStructure(descriptor) { + decodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer)) + } + // Simple default decoder implementation that delegates parsing the data to the ListSerializer + return Container(data) + } - else -> { // Simple default decoder implementation that delegates parsing the data to the ListSerializer - val data = decoder.decodeStructure(descriptor) { - decodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer)) - } - Container(data) - } + override fun serialize(encoder: Encoder, value: Container) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer), value.data) } } - /** - * This function is the meat to deserializing the container with dynamic tag names. Note that - * because we use xml there is no point in going through the (anonymous) list dance. Doing that - * would be an additional complication. - */ - private fun deserializeDynamic(decoder: D, reader: XmlReader): Container where D : Decoder, D : XML.XmlInput { - val xml = - decoder.delegateFormat() // This delegate format allows for reusing the settings from the outer format. + override fun deserializeXML( + decoder: Decoder, + input: XmlReader, + previousValue: Container?, + isValueChild: Boolean + ): Container { + // This delegate format allows for reusing the settings from the outer format. + val xml = (decoder as XML.XmlInput).delegateFormat() // We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is // its (only) child. val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0) - // A list to collect the data val dataList = mutableListOf() - decoder.decodeStructure(descriptor) { // Finding the children is actually not left to the serialization framework, but // done by "hand" - while (reader.next() != EventType.END_ELEMENT) { - when (reader.eventType) { + while (input.next() != EventType.END_ELEMENT) { + when (input.eventType) { EventType.COMMENT, EventType.IGNORABLE_WHITESPACE -> { // Comments and whitespace are just ignored @@ -89,13 +86,13 @@ class ContainerSerializer : KSerializer { EventType.ENTITY_REF, EventType.TEXT -> { - if (reader.text.isNotBlank()) { + if (input.text.isNotBlank()) { // Some parsers can return whitespace as text instead of ignorable whitespace // Use the handler from the configuration to throw the exception. @OptIn(ExperimentalXmlUtilApi::class) xml.config.policy.handleUnknownContentRecovering( - reader, + input, InputKind.Text, elementXmlDescriptor, null, @@ -105,10 +102,10 @@ class ContainerSerializer : KSerializer { } // It's best to still check the name before parsing EventType.START_ELEMENT -> { - if (reader.namespaceURI.isEmpty() && reader.localName.startsWith("Test_")) { + if (input.namespaceURI.isEmpty() && input.localName.startsWith("Test_")) { // When reading the child tag we use the DynamicTagReader to present normalized XML to the // deserializer for elements - val filter = DynamicTagReader(reader, elementXmlDescriptor) + val filter = DynamicTagReader(input, elementXmlDescriptor) // The test element can now be decoded as normal (with the filter applied) val testElement = xml.decodeFromReader(elementSerializer, filter) @@ -116,10 +113,10 @@ class ContainerSerializer : KSerializer { } else { // handling unexpected tags @OptIn(ExperimentalXmlUtilApi::class) xml.config.policy.handleUnknownContentRecovering( - reader, + input, InputKind.Element, elementXmlDescriptor, - reader.name, + input.name, (0 until elementXmlDescriptor.elementsCount).map { val e = elementXmlDescriptor.getElementDescriptor(it) PolyInfo(e.tagName, it, e) @@ -136,38 +133,19 @@ class ContainerSerializer : KSerializer { return Container(dataList) } - override fun serialize(encoder: Encoder, value: Container) { - when (encoder) { - is XML.XmlOutput -> // When we are using the xml format use the serializeDynamic method - return serializeDynamic(encoder, encoder.target, value.data) - - else -> // Otherwise just manually do the encoding that would have been generated - encoder.encodeStructure(descriptor) { - encodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer), value.data) - } - } - } - - /** - * This function provides the actual dynamic serialization - */ - private fun serializeDynamic( - encoder: E, - target: XmlWriter, - data: List - ) where E : Encoder, E : XML.XmlOutput { - val xml = encoder.delegateFormat() // This format keeps the settings from the outer serializer, this allows for + override fun serializeXML(encoder: Encoder, output: XmlWriter, value: Container, isValueChild: Boolean) { + val xml = + (encoder as XML.XmlOutput).delegateFormat() // This format keeps the settings from the outer serializer, this allows for // serializing the children // We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is // its (only) child. val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0) - encoder.encodeStructure(descriptor) { // create the structure (will write the tags of Container) - for (element in data) { // write each element + for (element in value.data) { // write each element // We need a writer that does the renaming from the normal format to the dynamic format // It is passed the string of the id to add. - val writer = DynamicTagWriter(target, elementXmlDescriptor, element.id.toString()) + val writer = DynamicTagWriter(output, elementXmlDescriptor, element.id.toString()) // Normal delegate writing of the element xml.encodeToWriter(writer, elementSerializer, element) diff --git a/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicTagReader.kt b/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicTagReader.kt index 2120ba864..a065904fc 100644 --- a/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicTagReader.kt +++ b/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicTagReader.kt @@ -33,12 +33,10 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : private var initDepth = reader.depth private val filterDepth: Int /** - * We want to be safe so only handle the content at relative depth 0. The way that depth is determined - * means that the depth is the depth after the tag (and end tags are thus one level lower than the tag (and its - * content). We correct for that here. + * We want to be safe so only handle the content at relative depth 0. */ get() = when (eventType) { - EventType.END_ELEMENT -> delegate.depth - initDepth + 1 + EventType.END_ELEMENT -> delegate.depth - initDepth else -> delegate.depth - initDepth } @@ -141,18 +139,22 @@ internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : * When we are at relative depth 0 we return the synthetic name rather than the original. */ override val namespaceURI: String - get() = when (filterDepth) { - 0 -> elementName.namespaceURI - else -> super.namespaceURI + get() { + return when (filterDepth) { + 0 -> elementName.namespaceURI + else -> super.namespaceURI + } } /** * When we are at relative depth 0 we return the synthetic name rather than the original. */ override val localName: String - get() = when (filterDepth) { - 0 -> elementName.localPart - else -> super.localName + get() { + return when (filterDepth) { + 0 -> elementName.localPart + else -> super.localName + } } /** diff --git a/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/main.kt b/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/main.kt index 0e65183bf..3f2578cac 100644 --- a/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/main.kt +++ b/examples/src/main/kotlin/net/devrieze/serialization/examples/dynamictagnames/main.kt @@ -52,7 +52,9 @@ private fun newExample(testElements: List) { val serializer = serializer() // use the default serializer // Create the configuration for (de)serialization - val xml = XML { indent = 2 } + val xml = XML { + indent = 2 + } // Encode and print the output of serialization val string = xml.encodeToString(serializer, data) diff --git a/examples/src/test/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicContainerSerializerTest.kt b/examples/src/test/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicContainerSerializerTest.kt new file mode 100644 index 000000000..c8b8adc66 --- /dev/null +++ b/examples/src/test/kotlin/net/devrieze/serialization/examples/dynamictagnames/DynamicContainerSerializerTest.kt @@ -0,0 +1,39 @@ +package net.devrieze.serialization.examples.dynamictagnames + +import io.github.pdvrieze.xmlutil.testutil.assertXmlEquals +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import nl.adaptivity.xmlutil.serialization.XML +import kotlin.test.Test +import kotlin.test.assertEquals + +class DynamicContainerSerializerTest { + + val xml = XML { indent = 2 } + + @Test + fun testEncode() { + val actual = xml.encodeToString(ktData) + + assertXmlEquals(XML_DATA, actual) + } + + @Test + fun testDecode() { + val decoded = xml.decodeFromString(XML_DATA) + assertEquals(ktData, decoded) + } + + companion object { + const val XML_DATA = + "someDatamoreData" + + val ktData = Container( + listOf( + TestElement(123, 42, "someData"), + TestElement(456, 71, "moreData") + ) + ) + + } +} diff --git a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt index fa508b3ec..d0796333f 100644 --- a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt +++ b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt @@ -116,6 +116,7 @@ internal open class XmlDecoderBase internal constructor( private inline fun handleParseError(body: () -> R): R { try { + val initialLocation = input.extLocationInfo return body() } catch (e: XmlSerialException) { throw e