import org.junit.Test
import kotlin.reflect.KClass
import kotlin.reflect.full.*
import kotlin.test.assertEquals
fun serializeToXml(value: Any): String = valueToXml(value)
private fun objectToXml(any: Any): String {
val reference = any::class
val classNameMapper = reference
.findAnnotation<SerializationNameMapper>()
?.let(::createMapper)
val simpleName = reference.simpleName.orEmpty()
val className = classNameMapper?.map(simpleName)
?: simpleName
val ignoreNulls = reference
.hasAnnotation<SerializationIgnoreNulls>()
return reference
.memberProperties
.filterNot { it.hasAnnotation<SerializationIgnore>()}
.mapNotNull { prop ->
val annotationName = prop
.findAnnotation<SerializationName>()
val mapper = prop
.findAnnotation<SerializationNameMapper>()
?.let(::createMapper)
val name = annotationName?.name
?: mapper?.map(prop.name)
?: classNameMapper?.map(prop.name)
?: prop.name
val value = prop.call(any)
if (ignoreNulls && value == null) {
return@mapNotNull null
}
"<$name>${valueToXml(value)}</$name>"
}
.joinToString(
separator = "",
prefix = "<$className>",
postfix = "</$className>",
)
}
private fun valueToXml(value: Any?): String = when (value) {
null, is Number, is Boolean, is String,
is Char, is Enum<*> -> "$value"
is Iterable<*> -> iterableToJson(value)
is Map<*, *> -> mapToJson(value)
else -> objectToXml(value)
}
private fun iterableToJson(any: Iterable<*>): String = any
.joinToString(
separator = "",
transform = ::valueToXml
)
private fun mapToJson(any: Map<*, *>) = any.toList()
.joinToString(
separator = "",
transform = { (name, value) ->
"<$name>${valueToXml(value)}</$name>"
}
)
private fun createMapper(
annotation: SerializationNameMapper
): NameMapper =
annotation.mapper.objectInstance
?: createWithNoargConstructor(annotation)
?: error("Cannot create mapper")
private fun createWithNoargConstructor(
annotation: SerializationNameMapper
): NameMapper? =
annotation.mapper
.constructors
.find { it.parameters.isEmpty() }
?.call()
fun main() {
data class SampleDataClass(
val externalTxnId: String,
val merchantTxnId: String,
val reference: String
)
val data = SampleDataClass(
externalTxnId = "07026984141550752666",
merchantTxnId = "07026984141550752666",
reference = "MERCHPAY"
)
println(serializeToXml(data))
// <SampleDataClass>
// <externalTxnId>07026984141550752666</externalTxnId>
// <merchantTxnId>07026984141550752666</merchantTxnId>
// <reference>MERCHPAY</reference>
// </SampleDataClass>
@SerializationNameMapper(UpperSnakeCaseName::class)
@SerializationIgnoreNulls
class Book(
val title: String,
val author: String,
@SerializationName("YEAR")
val publicationYear: Int,
val isbn: String?,
@SerializationIgnore
val price: Double,
)
@SerializationNameMapper(UpperSnakeCaseName::class)
class Library(
val catalog: List<Book>
)
val library = Library(
catalog = listOf(
Book(
title = "The Hobbit",
author = "J. R. R. Tolkien",
publicationYear = 1937,
isbn = "978-0-261-10235-4",
price = 9.99,
),
Book(
title = "The Witcher",
author = "Andrzej Sapkowski",
publicationYear = 1993,
isbn = "978-0-575-09404-2",
price = 7.99,
),
Book(
title = "Antifragile",
author = "Nassim Nicholas Taleb",
publicationYear = 2012,
isbn = null,
price = 12.99,
)
)
)
println(serializeToXml(library))
// <LIBRARY>
// <CATALOG>
// <BOOK>
// <AUTHOR>J. R. R. Tolkien</AUTHOR>
// <ISBN>978-0-261-10235-4</ISBN>
// <YEAR>1937</YEAR>
// <TITLE>The Hobbit</TITLE>
// </BOOK>
// <BOOK>
// <AUTHOR>Andrzej Sapkowski</AUTHOR>
// <ISBN>978-0-575-09404-2</ISBN>
// <YEAR>1993</YEAR>
// <TITLE>The Witcher</TITLE>
// </BOOK>
// <BOOK>
// <AUTHOR>Nassim Nicholas Taleb</AUTHOR>
// <YEAR>2012</YEAR>
// <TITLE>Antifragile</TITLE>
// </BOOK>
// </CATALOG>
// </LIBRARY>
}
@Target(AnnotationTarget.PROPERTY)
annotation class SerializationName(val name: String)
@Target(AnnotationTarget.PROPERTY)
annotation class SerializationIgnore
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
annotation class SerializationNameMapper(val mapper: KClass<out NameMapper>)
@Target(AnnotationTarget.CLASS)
annotation class SerializationIgnoreNulls
interface NameMapper {
fun map(name: String): String
}
object LowerCaseName : NameMapper {
override fun map(name: String): String = name.lowercase()
}
class SnakeCaseName : NameMapper {
val pattern = "(?<=.)[A-Z]".toRegex()
override fun map(name: String): String =
name.replace(pattern, "_$0").lowercase()
}
object UpperSnakeCaseName : NameMapper {
val pattern = "(?<=.)[A-Z]".toRegex()
override fun map(name: String): String =
name.replace(pattern, "_$0").uppercase()
}
class XmlSerializerTest {
@Test
fun `should serialize object with string`() {
class ExampleClass(val s1: String, val s2: String)
assertEquals(
"<ExampleClass><s1>ABC</s1><s2>DEF</s2></ExampleClass>",
serializeToXml(ExampleClass("ABC", "DEF"))
)
}
@Test
fun `should serialize nested objects`() {
class Name(val value: String)
class Box(val name: Name)
assertEquals(
"<Box><name><Name><value>ABC</value></Name></name></Box>",
serializeToXml(Box(Name("ABC")))
)
}
@Test
fun `should serialize list`() {
class ExampleClass(val names: List<String>, val grades: List<Int>)
assertEquals(
"<ExampleClass><grades>343</grades><names>ABC</names></ExampleClass>",
serializeToXml(ExampleClass(listOf("A", "B", "C"), listOf(3, 4, 3)))
)
}
@Test
fun `should serialize map`() {
class ExampleClass(val grades: Map<String, Int>)
assertEquals(
"<ExampleClass><grades><Alex>5</Alex><Beatrice>1</Beatrice></grades></ExampleClass>",
serializeToXml(ExampleClass(mapOf("Alex" to 5, "Beatrice" to 1)))
)
}
@Test
fun `should serialize complex object`() {
class Creature(
val name: String,
val attack: Int,
val defence: Int,
val traits: List<Trait>,
val cost: Map<Element, Int>,
)
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4,
traits = listOf(Trait.FLYING),
cost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
)
)
assertEquals(
"<Creature><attack>2</attack><cost><ANY>3</ANY><FOREST>2</FOREST></cost><defence>4</defence><name>Cockatrice</name><traits>FLYING</traits></Creature>",
serializeToXml(creature)
)
}
@Test
fun `should ignore properties`() {
class Creature(
@SerializationIgnore
val name: String,
val attack: Int,
val defence: Int,
val traits: List<Trait>,
val cost: Map<Element, Int>,
)
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4,
traits = listOf(Trait.FLYING),
cost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
)
)
assertEquals(
"<Creature><attack>2</attack><cost><ANY>3</ANY><FOREST>2</FOREST></cost><defence>4</defence><traits>FLYING</traits></Creature>",
serializeToXml(creature)
)
}
@Test
fun `should use different property names`() {
class Creature(
val name: String,
@SerializationName("att") val attack: Int,
@SerializationName("def") val defence: Int,
val traits: List<Trait>,
val cost: Map<Element, Int>,
)
val creature = Creature(
name = "Cockatrice",
attack = 2,
defence = 4,
traits = listOf(Trait.FLYING),
cost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
)
)
assertEquals(
"<Creature><att>2</att><cost><ANY>3</ANY><FOREST>2</FOREST></cost><def>4</def><name>Cockatrice</name><traits>FLYING</traits></Creature>",
serializeToXml(creature)
)
}
@Test
fun `should use class mapper`() {
@SerializationNameMapper(SnakeCaseName::class)
class Creature(
val longName: String,
val traitsList: List<Trait>,
val elementCost: Map<Element, Int>,
val isSpecial: Boolean,
var isUserAlready: Boolean = false,
)
val creature = Creature(
longName = "Cockatrice",
traitsList = listOf(Trait.FLYING),
elementCost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
),
isSpecial = true,
)
assertEquals(
"<creature><element_cost><ANY>3</ANY><FOREST>2</FOREST></element_cost><is_special>true</is_special><is_user_already>false</is_user_already><long_name>Cockatrice</long_name><traits_list>FLYING</traits_list></creature>",
serializeToXml(creature)
)
}
@Test
fun `should use property mapper`() {
class Creature(
@SerializationNameMapper(UpperSnakeCaseName::class)
val longName: String,
@SerializationNameMapper(SnakeCaseName::class)
val traitsList: List<Trait>,
@SerializationNameMapper(UpperSnakeCaseName::class)
val elementCost: Map<Element, Int>,
@SerializationNameMapper(SnakeCaseName::class)
val isSpecial: Boolean,
@SerializationNameMapper(UpperSnakeCaseName::class)
var isUserAlready: Boolean = false,
)
val creature = Creature(
longName = "Cockatrice",
traitsList = listOf(Trait.FLYING),
elementCost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
),
isSpecial = true,
)
assertEquals(
"<Creature><ELEMENT_COST><ANY>3</ANY><FOREST>2</FOREST></ELEMENT_COST><is_special>true</is_special><IS_USER_ALREADY>false</IS_USER_ALREADY><LONG_NAME>Cockatrice</LONG_NAME><traits_list>FLYING</traits_list></Creature>",
serializeToXml(creature)
)
}
@Test
fun `should override class mapper with property mapper`() {
@SerializationNameMapper(UpperSnakeCaseName::class)
class Creature(
val longName: String,
@SerializationNameMapper(SnakeCaseName::class)
val traitsList: List<Trait>,
val elementCost: Map<Element, Int>,
@SerializationNameMapper(SnakeCaseName::class)
val isSpecial: Boolean,
var isUsedAlready: Boolean = false,
)
val creature = Creature(
longName = "Cockatrice",
traitsList = listOf(Trait.FLYING),
elementCost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
),
isSpecial = true,
)
assertEquals(
"<CREATURE><ELEMENT_COST><ANY>3</ANY><FOREST>2</FOREST></ELEMENT_COST><is_special>true</is_special><IS_USED_ALREADY>false</IS_USED_ALREADY><LONG_NAME>Cockatrice</LONG_NAME><traits_list>FLYING</traits_list></CREATURE>",
serializeToXml(creature)
)
}
@Test
fun `should override mappers with property name`() {
@SerializationNameMapper(UpperSnakeCaseName::class)
class Creature(
@SerializationName("name")
val longName: String,
val traitsList: List<Trait>,
val elementCost: Map<Element, Int>,
@SerializationName("special")
val isSpecial: Boolean,
var isUserAlready: Boolean = false,
)
val creature = Creature(
longName = "Cockatrice",
traitsList = listOf(Trait.FLYING),
elementCost = mapOf(
Element.ANY to 3,
Element.FOREST to 2
),
isSpecial = true,
)
assertEquals(
"<CREATURE><ELEMENT_COST><ANY>3</ANY><FOREST>2</FOREST></ELEMENT_COST><special>true</special><IS_USER_ALREADY>false</IS_USER_ALREADY><name>Cockatrice</name><TRAITS_LIST>FLYING</TRAITS_LIST></CREATURE>",
serializeToXml(creature)
)
}
@Test
fun `should ignore nulls if annotation used`() {
@SerializationIgnoreNulls
class CreatureIgnoringNulls(
val name: String,
val attack: Int?,
val defence: Int?,
val extraDetails: String?,
)
class Creature(
val name: String,
val attack: Int?,
val defence: Int?,
val extraDetails: String?,
)
val creatureIgnoring = CreatureIgnoringNulls(
name = "Cockatrice",
attack = null,
defence = 4,
extraDetails = null,
)
assertEquals(
"<CreatureIgnoringNulls><defence>4</defence><name>Cockatrice</name></CreatureIgnoringNulls>",
serializeToXml(creatureIgnoring)
)
val creature = Creature(
name = "Cockatrice",
attack = null,
defence = 4,
extraDetails = null,
)
assertEquals(
"<Creature><attack>null</attack><defence>4</defence><extraDetails>null</extraDetails><name>Cockatrice</name></Creature>",
serializeToXml(creature)
)
}
@Test
fun `should produce correct result for sampe data class`() {
data class SampleDataClass(
val externalTxnId: String,
val merchantTxnId: String,
val reference: String
)
val data = SampleDataClass(
externalTxnId = "07026984141550752666",
merchantTxnId = "07026984141550752666",
reference = "MERCHPAY"
)
assertEquals(
"<SampleDataClass><externalTxnId>07026984141550752666</externalTxnId><merchantTxnId>07026984141550752666</merchantTxnId><reference>MERCHPAY</reference></SampleDataClass>",
serializeToXml(data)
)
}
@SerializationNameMapper(UpperSnakeCaseName::class)
class Book(
val title: String,
val author: String,
@SerializationName("YEAR")
val publicationYear: Int,
val isbn: String,
@SerializationIgnore
val price: Double,
)
@SerializationNameMapper(UpperSnakeCaseName::class)
class Library(
val catalog: List<Book>
)
@Test
fun `should produce correct result for library salmpe`() {
val library = Library(
catalog = listOf(
Book(
title = "The Hobbit",
author = "J. R. R. Tolkien",
publicationYear = 1937,
isbn = "978-0-261-10235-4",
price = 9.99,
),
Book(
title = "The Witcher",
author = "Andrzej Sapkowski",
publicationYear = 1993,
isbn = "978-0-575-09404-2",
price = 7.99,
),
)
)
assertEquals(
"<LIBRARY><CATALOG><BOOK><AUTHOR>J. R. R. Tolkien</AUTHOR><ISBN>978-0-261-10235-4</ISBN><YEAR>1937</YEAR><TITLE>The Hobbit</TITLE></BOOK><BOOK><AUTHOR>Andrzej Sapkowski</AUTHOR><ISBN>978-0-575-09404-2</ISBN><YEAR>1993</YEAR><TITLE>The Witcher</TITLE></BOOK></CATALOG></LIBRARY>",
serializeToXml(library)
)
}
enum class Element {
FOREST, ANY,
}
enum class Trait {
FLYING
}
}