From a5fb1a82f153ab3084045b0e368d727538682796 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Fri, 23 Nov 2018 15:11:59 +0000 Subject: [PATCH] [CORDA-2244]: Allow `map` and `mapValid` on entire lists wrt configuration parsing (#4280) --- .../parsing/internal/Configuration.kt | 34 ++++++--- .../parsing/internal/Properties.kt | 72 +++++++++++++------ .../configuration/parsing/internal/Schema.kt | 2 +- .../parsing/internal/Specification.kt | 46 +++++++++--- .../configuration/parsing/internal/Utils.kt | 18 +++-- .../parsing/internal/PropertyTest.kt | 63 ++++++++++++++++ .../internal/PropertyValidationTest.kt | 42 +++++++++++ .../parsing/internal/SpecificationTest.kt | 62 ++++++++++++++++ .../internal/config/ConfigUtilities.kt | 2 +- .../subcommands/ValidateConfigurationCli.kt | 2 +- 10 files changed, 293 insertions(+), 50 deletions(-) diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt index 71e8fbacc4..9169f18fad 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt @@ -1,10 +1,6 @@ package net.corda.common.configuration.parsing.internal -import com.typesafe.config.Config -import com.typesafe.config.ConfigException -import com.typesafe.config.ConfigObject -import com.typesafe.config.ConfigValue -import com.typesafe.config.ConfigValueFactory +import com.typesafe.config.* import net.corda.common.configuration.parsing.internal.versioned.VersionExtractor import net.corda.common.validation.internal.Validated import net.corda.common.validation.internal.Validated.Companion.invalid @@ -25,7 +21,7 @@ object Configuration { /** * Describes a [Config] hiding sensitive data. */ - fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue = { value -> ConfigValueFactory.fromAnyRef(value.toString()) }): ConfigValue? + fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue = { value -> ConfigValueFactory.fromAnyRef(value.toString()) }): ConfigValue? } object Value { @@ -131,6 +127,22 @@ object Configuration { fun optional(): Optional } + /** + * Defines a required property with a collection of values. + */ + interface RequiredList : Required> { + + /** + * Passes the value to a validating mapping function, provided this is valid in the first place. + */ + fun mapValid(mappedTypeName: String, convert: (List) -> Validated): Required + + /** + * Passes the value to a non-validating mapping function, provided this is valid in the first place. + */ + fun map(mappedTypeName: String, convert: (List) -> MAPPED): Required = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } + } + /** * Defines a property that must provide a single value or produce an error in case multiple values are specified for the relevant key. */ @@ -139,7 +151,7 @@ object Configuration { /** * Returns a required property expecting multiple values for the relevant key. */ - fun list(): Required> + fun list(): RequiredList } /** @@ -161,12 +173,12 @@ object Configuration { /** * Passes the value to a validating mapping function, provided this is valid in the first place. */ - fun mapValid(mappedTypeName: String, convert: (TYPE) -> Validated): Standard + fun mapValid(mappedTypeName: String, convert: (TYPE) -> Validated): Standard /** * Passes the value to a non-validating mapping function, provided this is valid in the first place. */ - fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } + fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } } override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated { @@ -268,7 +280,7 @@ object Configuration { */ fun validate(target: Config): Valid = validate(target, Configuration.Validation.Options.defaults) - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue): ConfigValue + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue companion object { @@ -358,7 +370,7 @@ object Configuration { override fun validate(target: Config, options: Validation.Options) = schema.validate(target, options) - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue) = schema.describe(configuration, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = schema.describe(configuration, serialiseValue) final override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid = validate(configuration, options).mapValid(::parseValid) diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt index 4892c3ecd2..4ce49fc5e6 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt @@ -1,10 +1,6 @@ package net.corda.common.configuration.parsing.internal -import com.typesafe.config.Config -import com.typesafe.config.ConfigException -import com.typesafe.config.ConfigObject -import com.typesafe.config.ConfigValue -import com.typesafe.config.ConfigValueFactory +import com.typesafe.config.* import net.corda.common.validation.internal.Validated import net.corda.common.validation.internal.Validated.Companion.invalid import net.corda.common.validation.internal.Validated.Companion.valid @@ -27,13 +23,13 @@ internal open class StandardProperty(override val key: String, typeN override val typeName: String = schema?.let { "#${it.name ?: "Object@$key"}" } ?: typeNameArg - override fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Configuration.Property.Definition.Standard = FunctionalProperty(this, mappedTypeName, extractListValue, convert) + override fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Configuration.Property.Definition.Standard = FunctionalProperty(this, mappedTypeName, extractListValue, convert) override fun optional(): Configuration.Property.Definition.Optional = OptionalDelegatedProperty(this) - override fun list(): Configuration.Property.Definition.Required> = ListProperty(this) + override fun list(): Configuration.Property.Definition.RequiredList = ListProperty(this) - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue): ConfigValue { + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { if (isSensitive) { return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue) @@ -61,7 +57,7 @@ internal open class StandardProperty(override val key: String, typeN override fun toString() = "\"$key\": \"$typeName\"" } -private class ListProperty(delegate: StandardProperty) : RequiredDelegatedProperty, StandardProperty>(delegate) { +private class ListProperty(delegate: StandardProperty) : RequiredDelegatedProperty, StandardProperty>(delegate), Configuration.Property.Definition.RequiredList { override val typeName: String = "List<${delegate.typeName}>" @@ -79,7 +75,9 @@ private class ListProperty(delegate: StandardProperty) : Requi return Validated.withResult(target, errors) } - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue): ConfigValue { + override fun mapValid(mappedTypeName: String, convert: (List) -> Validated): Configuration.Property.Definition.Required = ListMappingProperty(this, mappedTypeName, convert) + + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { if (isSensitive) { return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue) @@ -102,28 +100,28 @@ private class ListProperty(delegate: StandardProperty) : Requi } } -private class OptionalPropertyWithDefault(delegate: Configuration.Property.Definition.Optional, private val defaultValue: TYPE) : DelegatedProperty>(delegate) { +private class OptionalPropertyWithDefault(delegate: Configuration.Property.Definition.Optional, private val defaultValue: TYPE) : DelegatedProperty>(delegate) { override val isMandatory: Boolean = false override val typeName: String = delegate.typeName.removeSuffix("?") - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue) ?: valueDescription(if (isSensitive) Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER else defaultValue, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue) ?: valueDescription(if (isSensitive) Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER else defaultValue, serialiseValue) override fun valueIn(configuration: Config): TYPE = delegate.valueIn(configuration) ?: defaultValue override fun validate(target: Config, options: Configuration.Validation.Options): Valid = delegate.validate(target, options) } -private class FunctionalProperty(delegate: Configuration.Property.Definition.Standard, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List, private val convert: (TYPE) -> Valid) : RequiredDelegatedProperty>(delegate), Configuration.Property.Definition.Standard { +private class FunctionalProperty(delegate: Configuration.Property.Definition.Standard, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List, private val convert: (TYPE) -> Valid) : RequiredDelegatedProperty>(delegate), Configuration.Property.Definition.Standard { override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value() override val typeName: String = if (super.typeName == "#$mappedTypeName") super.typeName else "$mappedTypeName(${super.typeName})" - override fun mapValid(mappedTypeName: String, convert: (MAPPED) -> Valid): Configuration.Property.Definition.Standard = FunctionalProperty(delegate, mappedTypeName, extractListValue, { target: TYPE -> this.convert.invoke(target).mapValid(convert) }) + override fun mapValid(mappedTypeName: String, convert: (MAPPED) -> Valid): Configuration.Property.Definition.Standard = FunctionalProperty(delegate, mappedTypeName, extractListValue, { target: TYPE -> this.convert.invoke(target).mapValid(convert) }) - override fun list(): Configuration.Property.Definition.Required> = FunctionalListProperty(this) + override fun list(): Configuration.Property.Definition.RequiredList = FunctionalListProperty(this) override fun validate(target: Config, options: Configuration.Validation.Options): Valid { @@ -135,10 +133,10 @@ private class FunctionalProperty(delegate: Configuration.Pro return Validated.withResult(target, errors) } - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue) = delegate.describe(configuration, serialiseValue) + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = delegate.describe(configuration, serialiseValue) } -private class FunctionalListProperty(delegate: FunctionalProperty) : RequiredDelegatedProperty, FunctionalProperty>(delegate) { +private class FunctionalListProperty(delegate: FunctionalProperty) : RequiredDelegatedProperty, FunctionalProperty>(delegate), Configuration.Property.Definition.RequiredList { override val typeName: String = "List<${super.typeName}>" @@ -167,13 +165,15 @@ private class FunctionalListProperty(delegate: FunctionalProper } } - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue): ConfigValue { + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { if (isSensitive) { return valueDescription(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER, serialiseValue) } return delegate.schema?.let { schema -> valueDescription(valueIn(configuration).asSequence().map { element -> valueDescription(element, serialiseValue) }.map { it as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it, serialiseValue) }.toList(), serialiseValue) } ?: valueDescription(valueIn(configuration), serialiseValue) } + + override fun mapValid(mappedTypeName: String, convert: (List) -> Validated): Configuration.Property.Definition.Required = ListMappingProperty(this, mappedTypeName, convert) } private abstract class DelegatedProperty(protected val delegate: DELEGATE) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition { @@ -181,13 +181,13 @@ private abstract class DelegatedProperty(private val delegate: Configuration.Property.Definition) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition.Optional { +private class OptionalDelegatedProperty(private val delegate: Configuration.Property.Definition) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition.Optional { override val isMandatory: Boolean = false override val typeName: String = "${delegate.typeName}?" - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue) = if (isSpecifiedBy(configuration)) delegate.describe(configuration, serialiseValue) else null + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue) = if (isSpecifiedBy(configuration)) delegate.describe(configuration, serialiseValue) else null override fun valueIn(configuration: Config): TYPE? { @@ -214,11 +214,39 @@ private class OptionalDelegatedProperty(private val delegate: Config } -private abstract class RequiredDelegatedProperty>(delegate: DELEGATE) : DelegatedProperty(delegate), Configuration.Property.Definition.Required { +private abstract class RequiredDelegatedProperty>(delegate: DELEGATE) : DelegatedProperty(delegate), Configuration.Property.Definition.Required { final override fun optional(): Configuration.Property.Definition.Optional = OptionalDelegatedProperty(this) } +private class ListMappingProperty(private val delegate: Configuration.Property.Definition.RequiredList, private val mappedTypeName: String, private val convert: (List) -> Validated) : Configuration.Property.Definition.Required { + + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue? = delegate.describe(configuration, serialiseValue) + + override fun valueIn(configuration: Config) = convert.invoke(delegate.valueIn(configuration)).value() + + override fun optional(): Configuration.Property.Definition.Optional = OptionalDelegatedProperty(this) + + override fun validate(target: Config, options: Configuration.Validation.Options): Validated { + + val errors = mutableSetOf() + errors += delegate.validate(target, options).errors + if (errors.isEmpty()) { + errors += convert.invoke(delegate.valueIn(target)).mapErrors { error -> error.with(delegate.key, mappedTypeName) }.errors + } + return Validated.withResult(target, errors) + } + + override val typeName: String = mappedTypeName + + override val key = delegate.key + override val isMandatory = delegate.isMandatory + override val isSensitive = delegate.isSensitive + override val schema = delegate.schema + + override fun toString() = "\"$key\": \"$typeName\"" +} + fun ConfigException.toValidationError(keyName: String? = null, typeName: String): Configuration.Validation.Error { val toError = when (this) { @@ -249,4 +277,4 @@ private val expectedExceptionTypes = setOf(ConfigException.Missing::class, Confi private fun isErrorExpected(error: ConfigException) = expectedExceptionTypes.any { expected -> expected.isInstance(error) } -private fun valueDescription(value: Any, serialiseValue: (Any) -> ConfigValue) = serialiseValue.invoke(value) \ No newline at end of file +private fun valueDescription(value: Any?, serialiseValue: (Any?) -> ConfigValue) = serialiseValue.invoke(value) \ No newline at end of file diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt index e92214e5ca..348714fef5 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt @@ -47,7 +47,7 @@ internal class Schema(override val name: String?, unorderedProperties: Iterable< return description.toString() } - override fun describe(configuration: Config, serialiseValue: (Any) -> ConfigValue): ConfigValue { + override fun describe(configuration: Config, serialiseValue: (Any?) -> ConfigValue): ConfigValue { return properties.asSequence().map { it.key to it.describe(configuration, serialiseValue) }.filter { it.second != null }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) } } diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt index a7cfcedf23..a48fe47467 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt @@ -17,11 +17,20 @@ interface PropertyDelegate { fun optional(): PropertyDelegate.Optional } + interface RequiredList: Required> { + + override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> + + fun mapValid(mappedTypeName: String, convert: (List) -> Valid): Required + + fun map(mappedTypeName: String, convert: (List) -> MAPPED): Required = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } + } + interface Single { operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> - fun list(): Required> + fun list(): RequiredList } interface Optional { @@ -35,9 +44,9 @@ interface PropertyDelegate { override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> - fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Standard + fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Standard - fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } + fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } } companion object { @@ -74,11 +83,26 @@ private class PropertyDelegateImpl(private val key: String?, private val p } } - override fun list(): PropertyDelegate.Required> = ListPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).list() }) + override fun list(): PropertyDelegate.RequiredList = ListPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).list() }) override fun optional(): PropertyDelegate.Optional = OptionalPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional() }) - override fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): PropertyDelegate.Standard = PropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).mapValid(mappedTypeName) { value -> convert.invoke(value) } }) + override fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): PropertyDelegate.Standard = PropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).mapValid(mappedTypeName) { value -> convert.invoke(value) } }) +} + +private class RequiredPropertyDelegateImpl(private val key: String?, private val prefix: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition.Required) : PropertyDelegate.Required { + + override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { + + val shortName = key ?: property.name + val prop = construct.invoke(prefix?.let { "$prefix.$shortName" } ?: shortName, sensitive).also(addToProperties) + return object : ReadOnlyProperty> { + + override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Required = prop + } + } + + override fun optional(): PropertyDelegate.Optional = OptionalPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional() }) } private class OptionalPropertyDelegateImpl(private val key: String?, private val prefix: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition.Optional) : PropertyDelegate.Optional { @@ -109,17 +133,19 @@ private class OptionalWithDefaultPropertyDelegateImpl(private val key: Str } } -private class ListPropertyDelegateImpl(private val key: String?, private val prefix: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition.Required) : PropertyDelegate.Required { +private class ListPropertyDelegateImpl(private val key: String?, private val prefix: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition.RequiredList) : PropertyDelegate.RequiredList { - override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { + override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { val shortName = key ?: property.name val prop = construct.invoke(prefix?.let { "$prefix.$shortName" } ?: shortName, sensitive).also(addToProperties) - return object : ReadOnlyProperty> { + return object : ReadOnlyProperty> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Required = prop + override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.RequiredList = prop } } - override fun optional(): PropertyDelegate.Optional = OptionalPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional() }) + override fun optional(): PropertyDelegate.Optional> = OptionalPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional() }) + + override fun mapValid(mappedTypeName: String, convert: (List) -> Valid): PropertyDelegate.Required = RequiredPropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).mapValid(mappedTypeName) { value -> convert.invoke(value) } }) } \ No newline at end of file diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt index 5475e23b84..e1d83215a5 100644 --- a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt @@ -3,13 +3,23 @@ package net.corda.common.configuration.parsing.internal import com.typesafe.config.* import net.corda.common.validation.internal.Validated -inline fun Configuration.Property.Definition.Standard.mapValid(noinline convert: (TYPE) -> Valid): Configuration.Property.Definition.Standard = mapValid(MAPPED::class.java.simpleName, convert) - inline fun , VALUE : Any> Configuration.Specification.enum(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = enum(key, ENUM::class, sensitive) -inline fun PropertyDelegate.Standard.mapValid(noinline convert: (TYPE) -> Valid): PropertyDelegate.Standard = mapValid(MAPPED::class.java.simpleName, convert) +inline fun PropertyDelegate.Standard.mapValid(noinline convert: (TYPE) -> Valid): PropertyDelegate.Standard = mapValid(MAPPED::class.java.simpleName, convert) -inline fun PropertyDelegate.Standard.map(noinline convert: (TYPE) -> MAPPED): PropertyDelegate.Standard = map(MAPPED::class.java.simpleName, convert) +inline fun PropertyDelegate.Standard.map(noinline convert: (TYPE) -> MAPPED): PropertyDelegate.Standard = map(MAPPED::class.java.simpleName, convert) + +inline fun PropertyDelegate.RequiredList.mapValid(noinline convert: (List) -> Valid): PropertyDelegate.Required = mapValid(MAPPED::class.java.simpleName, convert) + +inline fun PropertyDelegate.RequiredList.map(noinline convert: (List) -> MAPPED): PropertyDelegate.Required = map(MAPPED::class.java.simpleName, convert) + +inline fun Configuration.Property.Definition.Standard.mapValid(noinline convert: (TYPE) -> Valid): Configuration.Property.Definition.Standard = mapValid(MAPPED::class.java.simpleName, convert) + +inline fun Configuration.Property.Definition.Standard.map(noinline convert: (TYPE) -> MAPPED): Configuration.Property.Definition.Standard = map(MAPPED::class.java.simpleName, convert) + +inline fun Configuration.Property.Definition.RequiredList.mapValid(noinline convert: (List) -> Valid): Configuration.Property.Definition.Required = mapValid(MAPPED::class.java.simpleName, convert) + +inline fun Configuration.Property.Definition.RequiredList.map(noinline convert: (List) -> MAPPED): Configuration.Property.Definition.Required = map(MAPPED::class.java.simpleName, convert) operator fun Config.get(property: Configuration.Property.Definition): TYPE = property.valueIn(this) diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt index cf01937b50..2be1c5639b 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt @@ -4,6 +4,7 @@ import com.typesafe.config.ConfigException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test +import java.util.concurrent.atomic.AtomicLong class PropertyTest { @@ -56,6 +57,68 @@ class PropertyTest { assertThat(property.valueIn(configuration)).isEqualTo(value) } + @Test + fun present_value_of_list_type_with_whole_list_mapping() { + + val key = "a.b.c" + val value = listOf(1L, 3L, 2L) + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.long(key).list().map { list -> list.max() } + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isTrue() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThat(property.valueIn(configuration)).isEqualTo(value.max()) + } + + @Test + fun absent_value_of_list_type_with_whole_list_mapping() { + + val key = "a.b.c" + val configuration = configObject().toConfig() + + val property = Configuration.Property.Definition.long(key).list().map { list -> list.max() }.optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThat(property.valueIn(configuration)).isEqualTo(null) + } + + @Test + fun present_value_of_list_type_with_single_element_and_whole_list_mapping() { + + val key = "a.b.c" + val value = listOf(1L, 3L, 2L) + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.long(key).map(::AtomicLong).list().map { list -> list.map(AtomicLong::get).max() } + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isTrue() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThat(property.valueIn(configuration)).isEqualTo(value.max()) + } + + @Test + fun absent_value_of_list_type_with_single_element_and_whole_list_mapping() { + + val key = "a.b.c" + val configuration = configObject().toConfig() + + val property = Configuration.Property.Definition.long(key).map(::AtomicLong).list().map { list -> list.map(AtomicLong::get).max() }.optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThat(property.valueIn(configuration)).isEqualTo(null) + } + @Test fun optional_present_value_of_list_type() { diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt index e4fe7b85f7..4d6ede7884 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt @@ -83,6 +83,48 @@ class PropertyValidationTest { } } + @Test + fun whole_list_validation_valid_value() { + + val key = "a.b.c" + val value = listOf(1L, 2L, 3L) + val configuration = configObject(key to value).toConfig() + + fun parseMax(list: List): Valid = valid(list.max()) + + val property = Configuration.Property.Definition.long(key).list().mapValid(::parseMax) + + assertThat(property.validate(configuration).errors).isEmpty() + } + + @Test + fun whole_list_validation_invalid_value() { + + val key = "a.b.c" + val value = listOf(1L, 2L, 3L) + val configuration = configObject(key to value).toConfig() + + fun parseMax(list: List): Valid { + + if (list.any { value -> value <= 1L }) { + return invalid(Configuration.Validation.Error.BadValue.of("All values must be greater than 1")) + } + return valid(list.max()) + } + + val property = Configuration.Property.Definition.long(key).list().mapValid(::parseMax) + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + @Test fun wrong_type() { diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt index c7f61f41e9..f176622cfe 100644 --- a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt @@ -1,8 +1,11 @@ package net.corda.common.configuration.parsing.internal import com.typesafe.config.Config +import net.corda.common.validation.internal.Validated +import net.corda.common.validation.internal.Validated.Companion.invalid import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.util.concurrent.atomic.AtomicLong class SpecificationTest { @@ -50,6 +53,28 @@ class SpecificationTest { } } + @Test + fun parse_list_aggregation() { + + val spec = object : Configuration.Specification("AtomicLong") { + + private val maxElement by long("elements").list().map { elements -> elements.max() } + + override fun parseValid(configuration: Config): Valid { + + return valid(AtomicLong(configuration[maxElement]!!)) + } + } + + val elements = listOf(1L, 10L, 2L) + val configuration = configObject("elements" to elements).toConfig() + + val result = spec.parse(configuration) + + assertThat(result.isValid).isTrue() + assertThat(result.value().get()).isEqualTo(elements.max()) + } + @Test fun validate() { @@ -68,6 +93,43 @@ class SpecificationTest { } } + @Test + fun validate_list_aggregation() { + + fun parseMax(elements: List): Valid { + + if (elements.isEmpty()) { + return invalid(Configuration.Validation.Error.BadValue.of("element list cannot be empty")) + } + if (elements.any { element -> element <= 1 }) { + return invalid(Configuration.Validation.Error.BadValue.of("elements cannot be less than or equal to 1")) + } + return valid(elements.max()!!) + } + + val spec = object : Configuration.Specification("AtomicLong") { + + private val maxElement by long("elements").list().mapValid(::parseMax) + + override fun parseValid(configuration: Config): Valid { + + return valid(AtomicLong(configuration[maxElement])) + } + } + + val elements = listOf(1L, 10L, 2L) + val configuration = configObject("elements" to elements).toConfig() + + val result = spec.parse(configuration) + + assertThat(result.errors).hasSize(1) + assertThat(result.errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error -> + + assertThat(error.path).containsExactly("elements") + assertThat(error.message).contains("elements cannot be less than or equal to 1") + } + } + @Test fun validate_with_domain_specific_errors() { 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 52484d6d75..fd38af42bb 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 @@ -224,7 +224,7 @@ private fun > enumBridge(clazz: Class, name: String): T { */ fun Any.toConfig(): Config = ConfigValueFactory.fromMap(toConfigMap()).toConfig() -fun Any.toConfigValue(): ConfigValue = if (this is ConfigValue) this else ConfigValueFactory.fromAnyRef(convertValue(this)) +fun Any?.toConfigValue(): ConfigValue = if (this is ConfigValue) this else if (this != null) ConfigValueFactory.fromAnyRef(convertValue(this)) else ConfigValueFactory.fromAnyRef(null) @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") // Reflect over the fields of the receiver and generate a value Map that can use to create Config object. diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt index 1f1b9371a9..fd264e4563 100644 --- a/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/ValidateConfigurationCli.kt @@ -26,7 +26,7 @@ internal class ValidateConfigurationCli : CliWrapperBase("validate-configuration return "for path: \"$pathAsString\": $message" } - internal fun logRawConfig(config: Config) = logger.debug("Actual configuration:\n${V1NodeConfigurationSpec.describe(config, Any::toConfigValue).render(configRenderingOptions)}") + internal fun logRawConfig(config: Config) = logger.debug("Actual configuration:\n${V1NodeConfigurationSpec.describe(config, Any?::toConfigValue).render(configRenderingOptions)}") } @Mixin