I'm creating a YAML parser and one of the features I want to implement is that properties of the domain class should be able to have names different from those used in the YAML representation. For example, a property in YAML may have the name city of birth, while in Kotlin, it might be named from. So, to address the mapping between properties with distinct names, I implemented a YamlArg annotation that can be used on the parameters of a domain class constructor, indicating the corresponding name in YAML (e.g., @YamlArg("city of birth"))
However, I can not for the life of me manage to get the annotations to have any kind of value. In line 95 of AbstractYamlParser I perform a search in the domain class constructor to find the properties whose name match the key from the yaml file. It is able to find the properties which match the name but not the one where I try to call the annotation class.
val paramType = type.memberProperties.find{ it.name == key || it.findAnnotation<YamlArg>()?.name == key }
So, while debugging I see that memberProperties has all the properties of Student class but obviously the name of the property where I call the annotation class doesn't match with the input YAML file.
So in summary, I'm not sure how exactly I can achieve passing a different name in the YAML file to the class file. I'll leave both the Student class, YamlArg annotation class and the test I am trying to run below. Will also leave both my parser files, any help is very much appreciated!
class Student @JvmOverloads constructor (
val name: String,
val nr: Int,
@YamlArg("city of birth") val from: String,
@YamlConvert(YamlToDate::class) val birth: LocalDate? = null,
val address: Address? = null,
val grades: List<Grade> = emptyList()
)
annotation class YamlArg(val name:String)
fun `Test YamlParserReflect with @YamlArg`() {
val yaml = """
name: John
nr: 123
city of birth: New York
""".trimIndent()
val student = YamlParserReflect.yamlParser(Student::class).parseObject(yaml.reader())
assertEquals("John", student.name)
assertEquals(123, student.nr)
assertEquals("New York", student.from)
}
import java.io.Reader
import kotlin.contracts.contract
import kotlin.reflect.KClass
import kotlin.reflect.full.*
abstract class AbstractYamlParser<T : Any>(val type: KClass<T>) : YamlParser<T> {
/**
* Used to get a parser for other Type using this same parsing approach.
*/
abstract fun <T : Any> yamlParser(type: KClass<T>) : AbstractYamlParser<T>
/**
* Creates a new instance of T through the first constructor
* that has all the mandatory parameters in the map and optional parameters for the rest.
*/
abstract fun newInstance(args: Map<String, Any>): T
final override fun parseObject(yaml: Reader): T {
// Mapa para guardar os parâmetros do Objeto a criar
val parameters: MutableMap<String, Any> = mutableMapOf()
// Variáveis para controlar a identação do ficheiro YAML
var count = 0
var canCount = true
// Ficheiro de texto
val lines = yaml.readLines()
var idx = 0
while ( idx < lines.size ){
val line = lines[idx]
// Se a linha for branca, não há nada a analizar
if(line.isBlank()) { idx++ }
else {
// Verificar o número de espaços devido à identação YAML
if(canCount){
for(char in line) if(char == ' ') count++ else break
canCount = false
}
// Novo objeto
if(line.trim().endsWith(":")){
// Lista para guardar os parâmetros do objeto
val newObject: MutableList<String> = mutableListOf()
// Nome do parâmetro
val key = line.trim().dropLast(1) // Elimina os ':'
// Verificamos se existe um parâmetro no construtor com o mesmo nome do parâmetro
val obj = type.memberProperties.find { it.name.lowercase() == key }
?: throw IllegalArgumentException("Object '$key' was not found in class '${type.simpleName}'.")
// Verificamos a classe do parâmetro
// Caso este seja uma lista, pretendemos criar objetos com o tipo dos parâmetros da lista
val objClass =
if(obj.returnType.classifier == List::class){ obj.returnType.arguments.first().type!!.classifier as KClass<*> }
else{ obj.returnType.classifier as KClass<T> }
idx++
// Percorremos pelos parâmetros do objeto e adicionamo-los à lista 'newObject'
while( idx < lines.size ){
// Quando a linha já não tiver um elemento do objeto atual
if(!lines[idx].drop(count).startsWith(" ")) break
// Adicionar o parâmetro ao objeto atual
newObject.add(lines[idx])
idx++
}
// Criamos o novo objeto chamando a função com o tipo pretendido e os parâmetros recolhidos
parameters[key] = YamlParserReflect.yamlParser(objClass).parseObject(newObject.joinToString("\n").reader())
}
else if (line.trim().startsWith("-")){
// Caso se trate de uma lista, chamamos o 'parseList'
return parseList(lines.joinToString("\n").reader()) as T
}
else {
val words = line.split(": ")
// Dividimos a linha para obter strings com o parâmetro 'key' e o seu valor 'value'
val key = words[0].drop(count)
val strValue = words[1]
// Procuramos no construtor da classe por um parâmetro com o mesmo nome da 'key'
val paramType = type.memberProperties.find{ it.name == key || it.findAnnotation<YamlArg>()?.name == key }
?: throw IllegalArgumentException("Parameter '$key' was not found in the class '${type.simpleName}'.")
// Dada a classe do parâmetro, alteramos a classe do 'value'
val value =
when(paramType.returnType.classifier as KClass<*>){
Int::class -> strValue.toInt()
Double::class -> strValue.toDouble()
Float::class -> strValue.toFloat()
Long::class -> strValue.toDouble()
Char::class -> strValue.toCharArray().first()
else -> strValue
}
// Adicionamos o novo parâmetro ao mapa 'parameters'
parameters[key] = value
idx++
}
}
}
return newInstance(parameters)
}
final override fun parseList(yaml: Reader): List<T> {
val returnList = mutableListOf<T>()
val lines = yaml.readLines()
var idx = 0
var count = 0
var countSpaces = true
while (idx < lines.size) {
if(lines[idx].trim().endsWith("-")){
if(countSpaces){
for (char in lines[idx]) if (char == ' ') count++ else break
countSpaces = false
}
val currentObject = mutableListOf<String>()
if(lines[0].split(": ").size == 2) {
val objTypeName = returnList.first().toString().split(":")[0].trimStart()
// Os dois espaços são necessários, de outra forma seria necessário ajeitar a variável 'count'
currentObject.add(" $objTypeName: $objTypeName")
}
idx++
while(idx < lines.size && lines[idx].isNotBlank() && !lines[idx].drop(count).startsWith("-")){
currentObject.add(lines[idx])
idx++
}
returnList.add(parseObject(currentObject.joinToString("\n").reader()))
}
else if(lines[idx].isNotBlank()){
// Tipos Primitivos da forma '- Tipo Primitivo' devem ser tratados logo em 'parseList'
val value = lines[idx].dropWhile { it == ' ' || it == '-' }
val valueType =
when (type) {
Char::class -> value.toCharArray().first()
Int::class -> value.toInt()
Long::class -> value.toLong()
Double::class -> value.toDouble()
else -> value
} as T
idx++
returnList.add(valueType)
}
else { idx++ }
}
return returnList
}
}
import java.security.spec.InvalidParameterSpecException
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.*
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.reflect
/**
* A YamlParser that uses reflection to parse objects.
*/
class YamlParserReflect<T : Any>(type: KClass<T>) : AbstractYamlParser<T>(type) {
companion object {
/**
*Internal cache of YamlParserReflect instances.
*/
private val yamlParsers: MutableMap<KClass<*>, YamlParserReflect<*>> = mutableMapOf()
/**
* Creates a YamlParser for the given type using reflection if it does not already exist.
* Keep it in an internal cache of YamlParserReflect instances.
*/
fun <T : Any> yamlParser(type: KClass<T>): AbstractYamlParser<T> {
return yamlParsers.getOrPut(type) { YamlParserReflect(type) } as YamlParserReflect<T>
}
}
/**
* Used to get a parser for other Type using the same parsing approach.
*/
override fun <T : Any> yamlParser(type: KClass<T>) = YamlParserReflect.yamlParser(type)
/**
* Creates a new instance of T through the first constructor
* that has all the mandatory parameters in the map and optional parameters for the rest.
*/
override fun newInstance(args: Map<String, Any>): T {
val params: MutableMap<KParameter, Any?> = mutableMapOf()
// Get the constructor of the class
val constructor = type.primaryConstructor
?: throw IllegalArgumentException("Class ${type.simpleName} does not have a primary constructor.")
for (parameter in constructor.parameters) {
// Check if the parameter has a custom converter specified
val convertAnnotation = parameter.findAnnotation<YamlConvert>()
val argValue = args[parameter.name]
val value = when {
// Use custom converter if specified
convertAnnotation != null && argValue is String -> {
val converterInstance = convertAnnotation.converter.createInstance()
// Ensure the converter has the required function
val convertFunction = converterInstance::class.functions.find {
it.name == "convertYamlToObject" && it.parameters.size == 2
} ?: throw InvalidParameterSpecException("Custom converter must have a function 'convertYamlToObject(yaml: String)'")
convertFunction.call(converterInstance, argValue)
}
else -> argValue
}
if (value == null && !parameter.isOptional) {
throw IllegalArgumentException("The parameter ${parameter.name} was not assigned a value.")
}
// Special handling for 'grades' property
if (parameter.name == "grades" && value == null) {
params[parameter] = emptyList<T>() // or any default value you want
} else {
params[parameter] = value
}
}
return constructor.callBy(params)
}
}
Solved by accessing the parameters through the constructor
val paramType = type.constructors.first().parameters.find{ it.name == key || it.findAnnotation<YamlArg>()?.name == key }
then attributing the paramType.name property to a new key
var keyA = paramType.name
and finally by passing it in case it exists
if(keyA != null){
parameters[keyA] = value
keyA == null
}
else parameters[key] = value