Custom config parser for more complex config structure (#3513)

* custom config parser for more complex config structure

* address PR issues
This commit is contained in:
Patrick Kuo 2018-07-04 16:51:54 +01:00 committed by GitHub
parent d634cdcbac
commit 68d0826563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 1 deletions

View File

@ -25,6 +25,8 @@ import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.jvmErasure
@ -32,6 +34,17 @@ import kotlin.reflect.jvm.jvmErasure
@Target(AnnotationTarget.PROPERTY)
annotation class OldConfig(val value: String)
/**
* This annotation can be used to provide ConfigParser for the class,
* the [parseAs] method will use the provided parser instead of data class constructs to parse the object.
*/
@Target(AnnotationTarget.CLASS)
annotation class CustomConfigParser(val parser: KClass<out ConfigParser<*>>)
interface ConfigParser<T> {
fun parse(config: Config): T
}
const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
// TODO Move other config parsing to use parseAs and remove this
@ -40,7 +53,10 @@ operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T
}
fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, nestedPath: String? = null): T {
require(clazz.isData) { "Only Kotlin data classes can be parsed. Offending: ${clazz.qualifiedName}" }
// Use custom parser if provided, instead of treating the object as data class.
clazz.findAnnotation<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) }
require(clazz.isData) { "Only Kotlin data classes or class annotated with CustomConfigParser can be parsed. Offending: ${clazz.qualifiedName}" }
val constructor = clazz.primaryConstructor!!
val parameters = constructor.parameters
val parameterNames = parameters.flatMap { param ->

View File

@ -227,6 +227,36 @@ class ConfigParsingTest {
}
}
@Test
fun `parse with provided parser`() {
val type1Config = mapOf("type" to "1", "value" to "type 1 value")
val type2Config = mapOf("type" to "2", "value" to "type 2 value")
val configuration = config("values" to listOf(type1Config, type2Config))
val objects = configuration.parseAs<TestObjects>()
assertThat(objects.values).containsExactly(TestObject.Type1("type 1 value"), TestObject.Type2("type 2 value"))
}
class TestParser : ConfigParser<TestObject> {
override fun parse(config: Config): TestObject {
val type = config.getInt("type")
return when (type) {
1 -> config.parseAs<TestObject.Type1>(onUnknownKeys = UnknownConfigKeysPolicy.IGNORE::handle)
2 -> config.parseAs<TestObject.Type2>(onUnknownKeys = UnknownConfigKeysPolicy.IGNORE::handle)
else -> throw IllegalArgumentException("Unsupported Object type : '$type'")
}
}
}
data class TestObjects(val values: List<TestObject>)
@CustomConfigParser(TestParser::class)
sealed class TestObject {
data class Type1(val value: String) : TestObject()
data class Type2(val value: String) : TestObject()
}
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
value1: V,
value2: V,
@ -310,6 +340,7 @@ class ConfigParsingTest {
require(positive > 0) { "$positive is not positive" }
}
}
data class OldData(
@OldConfig("oldValue")
val newValue: String)