diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt index f3b78acd4e..b3ee7f06ee 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/config/ConfigUtilities.kt @@ -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>) + +interface ConfigParser { + 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 Config.getValue(receiver: Any, metadata: KProperty<*>): T } fun Config.parseAs(clazz: KClass, onUnknownKeys: ((Set, 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()?.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 -> diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt index 688e6d53f9..07698b83c3 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/config/ConfigParsingTest.kt @@ -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() + + assertThat(objects.values).containsExactly(TestObject.Type1("type 1 value"), TestObject.Type2("type 2 value")) + } + + class TestParser : ConfigParser { + override fun parse(config: Config): TestObject { + val type = config.getInt("type") + return when (type) { + 1 -> config.parseAs(onUnknownKeys = UnknownConfigKeysPolicy.IGNORE::handle) + 2 -> config.parseAs(onUnknownKeys = UnknownConfigKeysPolicy.IGNORE::handle) + else -> throw IllegalArgumentException("Unsupported Object type : '$type'") + } + } + } + + data class TestObjects(val values: List) + + @CustomConfigParser(TestParser::class) + sealed class TestObject { + data class Type1(val value: String) : TestObject() + data class Type2(val value: String) : TestObject() + } + private inline fun , reified L : ListData, 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)