package nl.kallestruik.dlib.config import kotlinx.serialization.descriptors.* import kotlinx.serialization.json.JsonPrimitive import nl.kallestruik.dlib.config.annotations.* import nl.kallestruik.dlib.config.data.ConfigSchema import nl.kallestruik.dlib.config.data.ConfigValueConstraints import nl.kallestruik.dlib.config.data.ConfigValueType import nl.kallestruik.dlib.exceptions.ConfigSchemaGenerationException import kotlin.math.min import kotlin.math.max fun SerialDescriptor.toConfigSchema(annotations: List = listOf()): ConfigSchema { val allAnnotations = annotations + this.annotations return when (this.kind) { SerialKind.ENUM -> createEnumConfigSchema(this, allAnnotations) PrimitiveKind.BOOLEAN -> createBooleanConfigSchema(allAnnotations) PrimitiveKind.BYTE -> createIntegerConfigSchema(allAnnotations, Byte.MAX_VALUE.toLong(), Byte.MIN_VALUE.toLong()) PrimitiveKind.CHAR -> createStringConfigSchema(allAnnotations, maxLength=1) PrimitiveKind.SHORT -> createIntegerConfigSchema(allAnnotations, Short.MAX_VALUE.toLong(), Short.MIN_VALUE.toLong()) PrimitiveKind.INT -> createIntegerConfigSchema(allAnnotations, Int.MAX_VALUE.toLong(), Int.MIN_VALUE.toLong()) PrimitiveKind.LONG -> createIntegerConfigSchema(allAnnotations, Long.MAX_VALUE, Long.MIN_VALUE) PrimitiveKind.FLOAT -> createFloatConfigSchema(allAnnotations, Float.MAX_VALUE.toDouble(), -Float.MAX_VALUE.toDouble()) PrimitiveKind.DOUBLE -> createFloatConfigSchema(allAnnotations, Double.MAX_VALUE, -Double.MAX_VALUE) PrimitiveKind.STRING -> createStringConfigSchema(allAnnotations) StructureKind.LIST -> createListConfigSchema(this, allAnnotations) StructureKind.MAP -> createMapConfigSchema(this, allAnnotations) StructureKind.OBJECT, StructureKind.CLASS -> createObjectConfigSchema(this, allAnnotations) else -> throw ConfigSchemaGenerationException("Creating a schema for type ${this.kind} is not currently supported.") } } private fun createObjectConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val children = descriptor.elementDescriptors .mapIndexed { i, child -> descriptor.getElementName(i) to child.toConfigSchema(descriptor.getElementAnnotations(i)) }.toMap() return ConfigSchema( type = ConfigValueType.OBJECT, children = children, description = description, ) } private fun createListConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } return ConfigSchema( type = ConfigValueType.LIST, description = description, items = descriptor.elementDescriptors.first().toConfigSchema(), ) } private fun createMapConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val allowedKeys = annotations .filterIsInstance() .flatMap { annotation -> annotation.keys .map { listOf(it) } } val allowedKeysExclusive = annotations .filterIsInstance() .map { it.exclusiveKeys.toList() } if (descriptor.elementDescriptors.first().kind != PrimitiveKind.STRING) { throw ConfigSchemaGenerationException("Map keys have to be strings.") } return ConfigSchema( type = ConfigValueType.MAP, description = description, items = descriptor.elementDescriptors.last().toConfigSchema(), constraints = ConfigValueConstraints( allowedKeys = allowedKeys + allowedKeysExclusive, ), ) } private fun createStringConfigSchema(annotations: List, maxLength: Int? = null): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val allowedValues = annotations .filterIsInstance() .flatMap { it.values.toList() } val default = annotations .filterIsInstance() .firstOrNull() ?.let { JsonPrimitive(it.default) } return ConfigSchema( type = ConfigValueType.STRING, description = description, default = default, constraints = ConfigValueConstraints( enum = allowedValues.ifEmpty { null }, maxLength = maxLength, ), ) } private fun createEnumConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val allowedValues = descriptor.elementNames.toList() val default = annotations .filterIsInstance() .firstOrNull() ?.let { JsonPrimitive(it.default) } return ConfigSchema( type = ConfigValueType.STRING, description = description, default = default, constraints = ConfigValueConstraints( enum = allowedValues.ifEmpty { null }, ), ) } private fun createBooleanConfigSchema(annotations: List): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val default = annotations .filterIsInstance() .firstOrNull() ?.let { JsonPrimitive(it.default) } return ConfigSchema( type = ConfigValueType.BOOLEAN, description = description, default = default, ) } private fun createIntegerConfigSchema(annotations: List, maxValue: Long, minValue: Long): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val max = annotations .filterIsInstance() .map { it.max } .reduceOrNull { acc, next -> return@reduceOrNull max(acc, next) } ?: maxValue val min = annotations .filterIsInstance() .map { it.min } .reduceOrNull { acc, next -> min(acc, next) } ?: minValue val default = annotations .filterIsInstance() .firstOrNull() ?.let { JsonPrimitive(it.default) } return ConfigSchema( type = ConfigValueType.INTEGER, description = description, default = default, constraints = ConfigValueConstraints( maxValueInt = max, minValueInt = min, ), ) } private fun createFloatConfigSchema(annotations: List, maxValue: Double, minValue: Double): ConfigSchema { val description = annotations .filterIsInstance() .flatMap { it.lines.toList() } val max = annotations .filterIsInstance() .map { it.max } .reduceOrNull { acc, next -> return@reduceOrNull max(acc, next) } ?: maxValue val min = annotations .filterIsInstance() .map { it.min } .reduceOrNull { acc, next -> min(acc, next) } ?: minValue val default = annotations .filterIsInstance() .firstOrNull() ?.let { JsonPrimitive(it.default) } return ConfigSchema( type = ConfigValueType.FLOAT, description = description, default = default, constraints = ConfigValueConstraints( maxValueFloat = max, minValueFloat = min, ), ) }