diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 6f25d0a850..51247be08d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -30,8 +30,10 @@ - - + + + + diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000000..1eecd8139b --- /dev/null +++ b/common/README.md @@ -0,0 +1,9 @@ +# Common libraries + +This directory contains modules representing libraries that are reusable in different areas of Corda. + +## Rules of the folder + +- No dependencies whatsoever on any modules that are not in this directory (no corda-core, test-utils, etc.). +- No active components, as in, nothing that has a main function in it. +- Think carefully before using non-internal packages in these libraries. \ No newline at end of file diff --git a/common/configuration-parsing/README.md b/common/configuration-parsing/README.md new file mode 100644 index 0000000000..3d0d2b5745 --- /dev/null +++ b/common/configuration-parsing/README.md @@ -0,0 +1,23 @@ +# configuration-parsing + +This module provides types and functions to facilitate using Typesafe configuration objects. + +## Features + +1. A multi-step, structured validation framework for Typesafe configurations, allowing to merge Typesafe and application-level rules. +2. A parsing framework, allowing to extract domain types from raw configuration objects in a versioned, type-safe fashion. +3. A configuration description framework, allowing to print the expected schema of a configuration object. +4. A configuration serialization framework, allowing to output the structure and values of a configuration object, potentially obfuscating sensitive data. + +## Concepts + +The main idea is to create a `Configuration.Specification` to model the expected structure of a Typesafe configuration. +The specification is then able to parse, validate, describe and serialize a raw Typesafe configuration. + +By using `VersionedConfigurationParser`, it is possible to map specific versions to `Configuration.Specification`s and to parse and validate a raw configuration object based on a version header. + +Refer to the following tests to gain an understanding of how the library works: + +- net.corda.common.configuration.parsing.internal.versioned.VersionedParsingExampleTest +- net.corda.common.configuration.parsing.internal.SpecificationTest +- net.corda.common.configuration.parsing.internal.SchemaTest \ No newline at end of file diff --git a/common/configuration-parsing/build.gradle b/common/configuration-parsing/build.gradle new file mode 100644 index 0000000000..3b44430643 --- /dev/null +++ b/common/configuration-parsing/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'kotlin' + +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' + +dependencies { + compile group: "org.jetbrains.kotlin", name: "kotlin-stdlib-jdk8", version: kotlin_version + compile group: "org.jetbrains.kotlin", name: "kotlin-reflect", version: kotlin_version + + compile group: "com.typesafe", name: "config", version: typesafe_config_version + + compile project(":common-validation") + + testCompile group: "org.jetbrains.kotlin", name: "kotlin-test", version: kotlin_version + testCompile group: "junit", name: "junit", version: junit_version + testCompile group: "org.assertj", name: "assertj-core", version: assertj_version +} + +jar { + baseName 'common-configuration-parsing' +} + +publish { + name jar.baseName +} \ No newline at end of file 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 new file mode 100644 index 0000000000..becab85429 --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Configuration.kt @@ -0,0 +1,547 @@ +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 net.corda.common.configuration.parsing.internal.versioned.VersionExtractor +import net.corda.common.validation.internal.Validated +import net.corda.common.validation.internal.Validated.Companion.invalid +import java.time.Duration +import kotlin.reflect.KClass + +/** + * Entry point for the [Config] parsing utilities. + */ +object Configuration { + + /** + * Able to describe a part of a [Config] as a [ConfigValue]. + * Implemented by [Configuration.Specification], [Configuration.Schema] and [Configuration.Property.Definition] to output values that are masked if declared as sensitive. + */ + interface Describer { + + /** + * Describes a [Config] hiding sensitive data. + */ + fun describe(configuration: Config): ConfigValue + } + + object Value { + + /** + * Defines functions able to extract values from a [Config] in a type-safe fashion. + */ + interface Extractor { + + /** + * Returns a value out of a [Config] if all is good. Otherwise, it throws an exception. + * + * @throws ConfigException.Missing if the [Config] does not specify the value. + * @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type. + * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules.. + */ + @Throws(ConfigException.Missing::class, ConfigException.WrongType::class, ConfigException.BadValue::class) + fun valueIn(configuration: Config): TYPE + + /** + * Returns whether the value is specified by the [Config]. + */ + fun isSpecifiedBy(configuration: Config): Boolean + + /** + * Returns a value out of a [Config] if all is good, or null if no value is present. Otherwise, it throws an exception. + * + * @throws ConfigException.WrongType if the [Config] specifies a value of the wrong type. + * @throws ConfigException.BadValue if the [Config] specifies a value of the correct type, but this in unacceptable according to application-level validation rules.. + */ + @Throws(ConfigException.WrongType::class, ConfigException.BadValue::class) + fun valueInOrNull(configuration: Config): TYPE? { + + return when { + isSpecifiedBy(configuration) -> valueIn(configuration) + else -> null + } + } + } + + /** + * Able to parse a value from a [Config] and [Configuration.Validation.Options], returning a [Valid] result containing either the value itself, or some [Configuration.Validation.Error]s. + */ + interface Parser { + + /** + * Returns a [Valid] wrapper either around a valid value extracted from the [Config], or around a set of [Configuration.Validation.Error] with details about what went wrong. + */ + fun parse(configuration: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults): Valid + } + } + + object Property { + + /** + * Configuration property metadata, as in the set of qualifying traits for a [Configuration.Property.Definition]. + */ + interface Metadata { + + /** + * Property key. + */ + val key: String + + /** + * Name of the type for this property.. + */ + val typeName: String + + /** + * Whether the absence of a value for this property will raise an error. + */ + val isMandatory: Boolean + + /** + * Whether the value for this property will be shown by [Configuration.Property.Definition.describe]. + */ + val isSensitive: Boolean + + val schema: Schema? + } + + /** + * Property definition, able to validate, describe and extract values from a [Config] object. + */ + interface Definition : Configuration.Property.Metadata, Configuration.Validator, Configuration.Value.Extractor, Configuration.Describer, Configuration.Value.Parser { + + override fun isSpecifiedBy(configuration: Config): Boolean = configuration.hasPath(key) + + /** + * Defines a required property, which must provide a value or produce an error. + */ + interface Required : Definition { + + /** + * Returns an optional property with given [defaultValue]. This property does not produce errors in case the value is unspecified, returning the [defaultValue] instead. + */ + fun optional(defaultValue: TYPE? = null): Definition + } + + /** + * Defines a property that must provide a single value or produce an error in case multiple values are specified for the relevant key. + */ + interface Single : Definition { + + /** + * Returns a required property expecting multiple values for the relevant key. + */ + fun list(): Required> + } + + /** + * Default property definition, required and single-value. + */ + interface Standard : Required, Single { + + /** + * Passes the value to a validating mapping function, provided this is valid in the first place. + */ + 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)) } + } + + override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated { + + return validate(configuration, options).mapValid { config -> valid(valueIn(config)) } + } + + companion object { + + const val SENSITIVE_DATA_PLACEHOLDER = "*****" + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [Long]. + */ + fun long(key: String, sensitive: Boolean = false): Standard = LongProperty(key, sensitive) + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [Int]. + */ + fun int(key: String, sensitive: Boolean = false): Standard = long(key, sensitive).mapValid { value -> + + try { + valid(Math.toIntExact(value)) + } catch (e: ArithmeticException) { + invalid(Configuration.Validation.Error.BadValue.of("Provided value exceeds Integer range [${Int.MIN_VALUE}, ${Int.MAX_VALUE}].", key, Int::class.javaObjectType.simpleName)) + } + } + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [Boolean]. + */ + fun boolean(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Boolean::class.javaObjectType.simpleName, Config::getBoolean, Config::getBooleanList, sensitive) + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [Double]. + */ + fun double(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Double::class.javaObjectType.simpleName, Config::getDouble, Config::getDoubleList, sensitive) + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [Float]. + */ + fun float(key: String, sensitive: Boolean = false): Standard = double(key, sensitive).mapValid { value -> + + val floatValue = value.toFloat() + if (floatValue.isInfinite() || floatValue.isNaN()) { + invalid(Configuration.Validation.Error.BadValue.of(key, Float::class.javaObjectType.simpleName, "Provided value exceeds Float range.")) + } else { + valid(value.toFloat()) + } + } + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [String]. + */ + fun string(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, String::class.java.simpleName, Config::getString, Config::getStringList, sensitive) + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [Duration]. + */ + fun duration(key: String, sensitive: Boolean = false): Standard = StandardProperty(key, Duration::class.java.simpleName, Config::getDuration, Config::getDurationList, sensitive) + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [ConfigObject]. + * It supports an optional [Configuration.Schema], which is used for validation and more when provided. + */ + fun nestedObject(key: String, schema: Schema? = null, sensitive: Boolean = false): Standard = StandardProperty(key, ConfigObject::class.java.simpleName, Config::getObject, Config::getObjectList, sensitive, schema) + + /** + * Returns a [Configuration.Property.Definition.Standard] with value of type [ENUM]. + * This property expects the exact [ENUM] value specified as text for the relevant key. + */ + fun > enum(key: String, enumClass: KClass, sensitive: Boolean = false): Standard = StandardProperty(key, enumClass.java.simpleName, { conf: Config, propertyKey: String -> conf.getEnum(enumClass.java, propertyKey) }, { conf: Config, propertyKey: String -> conf.getEnumList(enumClass.java, propertyKey) }, sensitive) + } + } + } + + /** + * A definition of the expected structure of a [Config] object, able to validate it and describe it while preventing sensitive values from being revealed. + */ + interface Schema : Configuration.Validator, Configuration.Describer { + + /** + * Name of the schema. + */ + val name: String? + + /** + * A description of the schema definition, with references to nested types. + */ + fun description(): String + + /** + * All properties defining this schema. + */ + val properties: Set> + + companion object { + + /** + * Constructs a schema with given name and properties. + */ + fun withProperties(name: String? = null, properties: Iterable>): Schema = Schema(name, properties) + + /** + * @see [withProperties]. + */ + fun withProperties(vararg properties: Property.Definition<*>, name: String? = null): Schema = withProperties(name, properties.toSet()) + + /** + * Convenient way of creating an [Iterable] of [Property.Definition]s without having to reference the [Property.Definition.Companion] each time. + * @see [withProperties]. + */ + fun withProperties(name: String? = null, builder: Property.Definition.Companion.() -> Iterable>): Schema = withProperties(name, builder.invoke(Property.Definition)) + } + } + + /** + * A [Configuration.Schema] that is also able to parse a raw [Config] object into a [VALUE]. + * It is an abstract class to allow extension with delegated properties e.g., object Settings: Specification() { val address by string().optional("localhost:8080") }. + */ + abstract class Specification(name: String?, private val prefix: String? = null) : Configuration.Schema, Configuration.Value.Parser { + + private val mutableProperties = mutableSetOf>() + + override val properties: Set> = mutableProperties + + private val schema: Schema by lazy { Schema(name, properties) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Long]. + */ + fun long(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.long(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Int]. + */ + fun int(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.int(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Boolean]. + */ + fun boolean(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.boolean(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Double]. + */ + fun double(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.double(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Float]. + */ + fun float(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.float(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [String]. + */ + fun string(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.string(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [Duration]. + */ + fun duration(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.duration(key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [ConfigObject]. + * It supports an optional [Configuration.Schema], which is used for validation and more when provided. + */ + fun nestedObject(schema: Schema? = null, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.nestedObject(schema, key, prefix, sensitive) { mutableProperties.add(it) } + + /** + * Returns a delegate for a [Configuration.Property.Definition.Standard] of type [ENUM]. + * This property expects the exact [ENUM] value specified as text for the relevant key. + */ + fun > enum(key: String? = null, enumClass: KClass, sensitive: Boolean = false): PropertyDelegate.Standard = PropertyDelegate.enum(key, prefix, enumClass, sensitive) { mutableProperties.add(it) } + + override val name: String? get() = schema.name + + override fun description() = schema.description() + + override fun validate(target: Config, options: Validation.Options?) = schema.validate(target, options) + + override fun describe(configuration: Config) = schema.describe(configuration) + + final override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid = validate(configuration, options).mapValid(::parseValid) + + /** + * Implement to define further mapping and validation logic, assuming the underlying raw [Config] is correct in terms of this [Configuration.Specification]. + */ + protected abstract fun parseValid(configuration: Config): Valid + } + + object Validation { + + /** + * [Config] validation options. + * @property strict whether to raise unknown property keys as errors. + */ + data class Options(val strict: Boolean) { + + companion object { + + /** + * Default [Config] validation options, without [strict] parsing enabled. + */ + val defaults: Configuration.Validation.Options = Options(strict = false) + } + } + + /** + * Super-type for the errors raised by the parsing and validation of a [Config] object. + * + * @property keyName name of the property key this error refers to, if any. + * @property typeName name of the type of the property this error refers to, if any. + * @property message details about what went wrong during the processing. + * @property containingPath containing path of the error, excluding the [keyName]. + */ + sealed class Error constructor(open val keyName: String?, open val typeName: String?, open val message: String, val containingPath: List = emptyList()) { + + internal companion object { + + private const val UNKNOWN = "" + + private fun contextualize(keyName: String, containingPath: List): Pair> { + + val keyParts = keyName.split(".") + return when { + keyParts.size > 1 -> { + val fullContainingPath = containingPath + keyParts.subList(0, keyParts.size - 1) + val keySegment = keyParts.last() + keySegment to fullContainingPath + } + else -> keyName to containingPath + } + } + } + + /** + * Full path for nested property keys, including the [keyName]. + */ + val path: List get() = keyName?.let { containingPath + it } ?: containingPath + + /** + * [containingPath] joined by "." characters. + */ + val containingPathAsString: String = containingPath.joinToString(".") + + /** + * [pathstr] joined by "." characters. + */ + val pathAsString: String = path.joinToString(".") + + internal abstract fun withContainingPath(vararg containingPath: String): Error + + internal abstract fun with(keyName: String = this.keyName ?: UNKNOWN, typeName: String = this.typeName ?: UNKNOWN): Configuration.Validation.Error + + override fun toString(): String { + + return "(keyName='$keyName', typeName='$typeName', path=$path, message='$message')" + } + + /** + * Raised when a value was found for the relevant [keyName], but the value did not match the declared one for the property. + */ + class WrongType private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) { + + internal companion object { + + internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): WrongType = contextualize(keyName, containingPath).let { (key, path) -> WrongType(key, typeName, message, path) } + } + + override fun withContainingPath(vararg containingPath: String) = WrongType(keyName, typeName, message, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): WrongType = WrongType.of(message, keyName, typeName, containingPath) + } + + /** + * Raised when no value was found for the relevant [keyName], and the property is [Configuration.Property.Definition.Required]. + */ + class MissingValue private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) { + + internal companion object { + + internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): MissingValue = contextualize(keyName, containingPath).let { (key, path) -> MissingValue(key, typeName, message, path) } + } + + override fun withContainingPath(vararg containingPath: String) = MissingValue(keyName, typeName, message, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): MissingValue = MissingValue.of(message, keyName, typeName, containingPath) + } + + /** + * Raised when a value was found for the relevant [keyName], it matched the declared raw type for the property, but its value is unacceptable due to application-level validation rules. + */ + class BadValue private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) { + + internal companion object { + + internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): BadValue = contextualize(keyName, containingPath).let { (key, path) -> BadValue(key, typeName, message, path) } + } + + override fun withContainingPath(vararg containingPath: String) = BadValue(keyName, typeName, message, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): BadValue = BadValue.of(message, keyName, typeName, containingPath) + } + + /** + * Raised when the [Config] contains a malformed path. + */ + class BadPath private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) { + + internal companion object { + + internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): BadPath = contextualize(keyName, containingPath).let { (key, path) -> BadPath(key, typeName, message, path) } + } + + override fun withContainingPath(vararg containingPath: String) = BadPath(keyName, typeName, message, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): BadPath = BadPath.of(message, keyName, typeName, containingPath) + } + + /** + * Raised when the [Config] is malformed and cannot be parsed. + */ + class MalformedStructure private constructor(override val keyName: String, override val typeName: String, message: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) { + + internal companion object { + + internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List = emptyList()): MalformedStructure = contextualize(keyName, containingPath).let { (key, path) -> MalformedStructure(key, typeName, message, path) } + } + + override fun withContainingPath(vararg containingPath: String) = MalformedStructure(keyName, typeName, message, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): MalformedStructure = MalformedStructure.of(message, keyName, typeName, containingPath) + } + + /** + * Raised when a key-value pair appeared in the [Config] object without a matching property in the [Configuration.Schema], and [Configuration.Validation.Options.strict] was enabled. + */ + class Unknown private constructor(override val keyName: String, containingPath: List = emptyList()) : Configuration.Validation.Error(keyName, null, message(keyName), containingPath) { + + internal companion object { + + private fun message(keyName: String) = "Unknown property \"$keyName\"." + + internal fun of(keyName: String = UNKNOWN, containingPath: List = emptyList()): Unknown = contextualize(keyName, containingPath).let { (key, path) -> Unknown(key, path) } + } + + override val message = message(pathAsString) + + override fun withContainingPath(vararg containingPath: String) = Unknown(keyName, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): Unknown = Unknown.of(keyName, containingPath) + } + + /** + * Raised when the specification version found in the [Config] object did not match any known [Configuration.Specification]. + */ + class UnsupportedVersion private constructor(val version: Int, containingPath: List = emptyList()) : Configuration.Validation.Error(null, null, "Unknown configuration version $version.", containingPath) { + + internal companion object { + + internal fun of(version: Int): UnsupportedVersion = UnsupportedVersion(version) + } + + override fun withContainingPath(vararg containingPath: String) = UnsupportedVersion(version, containingPath.toList() + this.containingPath) + + override fun with(keyName: String, typeName: String): UnsupportedVersion = this + } + } + } + + object Version { + + /** + * Defines the contract from extracting a specification version from a [Config] object. + */ + interface Extractor : Configuration.Value.Parser { + + companion object { + + const val DEFAULT_VERSION_VALUE = 1 + + /** + * Returns a [Configuration.Version.Extractor] that reads the value from given [versionKey], defaulting to [versionDefaultValue] when [versionKey] is unspecified. + */ + fun fromKey(versionKey: String, versionDefaultValue: Int? = DEFAULT_VERSION_VALUE): Configuration.Version.Extractor = VersionExtractor(versionKey, versionDefaultValue) + } + } + } + + /** + * Defines the ability to validate a [Config] object, producing a valid [Config] or a set of [Configuration.Validation.Error]. + */ + interface Validator : net.corda.common.validation.internal.Validator +} \ No newline at end of file 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 new file mode 100644 index 0000000000..17b9fe73a1 --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Properties.kt @@ -0,0 +1,205 @@ +package net.corda.common.configuration.parsing.internal + +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 + +internal class LongProperty(key: String, sensitive: Boolean = false) : StandardProperty(key, Long::class.javaObjectType.simpleName, Config::getLong, Config::getLongList, sensitive) { + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + val validated = super.validate(target, options) + if (validated.isValid && target.getValue(key).unwrapped().toString().contains(".")) { + return invalid(ConfigException.WrongType(target.origin(), key, Long::class.javaObjectType.simpleName, Double::class.javaObjectType.simpleName).toValidationError(key, typeName)) + } + return validated + } +} + +internal open class StandardProperty(override val key: String, typeNameArg: String, private val extractSingleValue: (Config, String) -> TYPE, internal val extractListValue: (Config, String) -> List, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard { + + override fun valueIn(configuration: Config) = extractSingleValue.invoke(configuration, key) + + 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 optional(defaultValue: TYPE?): Configuration.Property.Definition = OptionalProperty(this, defaultValue) + + override fun list(): Configuration.Property.Definition.Required> = ListProperty(this) + + override fun describe(configuration: Config): ConfigValue { + + if (isSensitive) { + return ConfigValueFactory.fromAnyRef(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER) + } + return schema?.describe(configuration.getConfig(key)) ?: ConfigValueFactory.fromAnyRef(valueIn(configuration)) + } + + override val isMandatory = true + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + val errors = mutableSetOf() + errors += errorsWhenExtractingValue(target) + if (errors.isEmpty()) { + schema?.let { nestedSchema -> + val nestedConfig: Config? = target.getConfig(key) + nestedConfig?.let { + errors += nestedSchema.validate(nestedConfig, options).errors.map { error -> error.withContainingPath(*key.split(".").toTypedArray()) } + } + } + } + return Validated.withResult(target, errors) + } + + override fun toString() = "\"$key\": \"$typeName\"" +} + +private class ListProperty(delegate: StandardProperty) : RequiredDelegatedProperty, StandardProperty>(delegate) { + + override val typeName: String = "List<${delegate.typeName}>" + + override fun valueIn(configuration: Config): List = delegate.extractListValue.invoke(configuration, key) + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + val errors = mutableSetOf() + errors += errorsWhenExtractingValue(target) + if (errors.isEmpty()) { + delegate.schema?.let { schema -> + errors += valueIn(target).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).mapIndexed { index, targetConfig -> schema.validate(targetConfig, options).errors.map { error -> error.withContainingPath(key, "[$index]") } }.reduce { one, other -> one + other } + } + } + return Validated.withResult(target, errors) + } + + override fun describe(configuration: Config): ConfigValue { + + if (isSensitive) { + return ConfigValueFactory.fromAnyRef(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER) + } + return delegate.schema?.let { schema -> ConfigValueFactory.fromAnyRef(valueIn(configuration).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it) }.toList()) } ?: ConfigValueFactory.fromAnyRef(valueIn(configuration)) + } +} + +private class OptionalProperty(delegate: Configuration.Property.Definition.Required, private val defaultValue: TYPE?) : DelegatedProperty>(delegate) { + + override val isMandatory: Boolean = false + + override val typeName: String = "${super.typeName}?" + + override fun describe(configuration: Config) = delegate.describe(configuration) + + override fun valueIn(configuration: Config): TYPE? { + + return when { + isSpecifiedBy(configuration) -> delegate.valueIn(configuration) + else -> defaultValue + } + } + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + val result = delegate.validate(target, options) + val error = result.errors.asSequence().filterIsInstance().singleOrNull() + return when { + error != null -> if (result.errors.size > 1) result else valid(target) + else -> result + } + } +} + +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)).valueOrThrow() + + 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 list(): Configuration.Property.Definition.Required> = FunctionalListProperty(this) + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + 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 fun describe(configuration: Config) = delegate.describe(configuration) +} + +private class FunctionalListProperty(delegate: FunctionalProperty) : RequiredDelegatedProperty, FunctionalProperty>(delegate) { + + override val typeName: String = "List<${super.typeName}>" + + override fun valueIn(configuration: Config): List = delegate.extractListValue.invoke(configuration, key).asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.map(ConfigObject::toConfig).map(delegate::valueIn).toList() + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + val list = try { + delegate.extractListValue.invoke(target, key) + } catch (e: ConfigException) { + if (isErrorExpected(e)) { + return invalid(e.toValidationError(key, typeName)) + } else { + throw e + } + } + val errors = list.asSequence().map { configObject(key to ConfigValueFactory.fromAnyRef(it)) }.mapIndexed { index, value -> delegate.validate(value.toConfig(), options).errors.map { error -> error.withContainingPath(key, "[$index]") } }.reduce { one, other -> one + other }.toSet() + return Validated.withResult(target, errors) + } + + override fun describe(configuration: Config): ConfigValue { + + if (isSensitive) { + return ConfigValueFactory.fromAnyRef(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER) + } + return delegate.schema?.let { schema -> ConfigValueFactory.fromAnyRef(valueIn(configuration).asSequence().map { element -> element as ConfigObject }.map(ConfigObject::toConfig).map { schema.describe(it) }.toList()) } ?: ConfigValueFactory.fromAnyRef(valueIn(configuration)) + } +} + +private abstract class DelegatedProperty(protected val delegate: DELEGATE) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition { + + final override fun toString() = "\"$key\": \"$typeName\"" +} + +private abstract class RequiredDelegatedProperty>(delegate: DELEGATE) : DelegatedProperty(delegate), Configuration.Property.Definition.Required { + + final override fun optional(defaultValue: TYPE?): Configuration.Property.Definition = OptionalProperty(this, defaultValue) +} + +private fun ConfigException.toValidationError(keyName: String, typeName: String): Configuration.Validation.Error { + + val toError = when (this) { + is ConfigException.Missing -> Configuration.Validation.Error.MissingValue.Companion::of + is ConfigException.WrongType -> Configuration.Validation.Error.WrongType.Companion::of + is ConfigException.BadValue -> Configuration.Validation.Error.BadValue.Companion::of + is ConfigException.BadPath -> Configuration.Validation.Error.BadPath.Companion::of + is ConfigException.Parse -> Configuration.Validation.Error.MalformedStructure.Companion::of + else -> throw IllegalStateException("Unsupported ConfigException of type ${this::class.java.name}", this) + } + return toError.invoke(message!!, keyName, typeName, emptyList()) +} + +private fun Configuration.Property.Definition<*>.errorsWhenExtractingValue(target: Config): Set { + + try { + valueIn(target) + return emptySet() + } catch (exception: ConfigException) { + if (isErrorExpected(exception)) { + return setOf(exception.toValidationError(key, typeName)) + } + throw exception + } +} + +private val expectedExceptionTypes = setOf(ConfigException.Missing::class, ConfigException.WrongType::class, ConfigException.BadValue::class, ConfigException.BadPath::class, ConfigException.Parse::class) + +private fun isErrorExpected(error: ConfigException) = expectedExceptionTypes.any { expected -> expected.isInstance(error) } \ 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 new file mode 100644 index 0000000000..06e90d34cd --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Schema.kt @@ -0,0 +1,75 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.Config +import com.typesafe.config.ConfigValue +import com.typesafe.config.ConfigValueFactory +import net.corda.common.validation.internal.Validated + +internal class Schema(override val name: String?, unorderedProperties: Iterable>) : Configuration.Schema { + + override val properties = unorderedProperties.sortedBy(Configuration.Property.Definition<*>::key).toSet() + + init { + val invalid = properties.groupBy(Configuration.Property.Definition<*>::key).mapValues { entry -> entry.value.size }.filterValues { propertiesForKey -> propertiesForKey > 1 } + if (invalid.isNotEmpty()) { + throw IllegalArgumentException("More than one property was found for keys ${invalid.keys.joinToString(", ", "[", "]")}.") + } + } + + override fun validate(target: Config, options: Configuration.Validation.Options?): Valid { + + val propertyErrors = properties.flatMap { property -> property.validate(target, options).errors }.toMutableSet() + if (options?.strict == true) { + val unknownKeys = target.root().keys - properties.map(Configuration.Property.Definition<*>::key) + propertyErrors += unknownKeys.map { Configuration.Validation.Error.Unknown.of(it) } + } + return Validated.withResult(target, propertyErrors) + } + + override fun description(): String { + + val description = StringBuilder() + val root = properties.asSequence().map { it.key to ConfigValueFactory.fromAnyRef(it.typeName) }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) } + + description.append(root.toConfig().serialize()) + + val nestedProperties = (properties + properties.flatMap { it.schema?.properties ?: emptySet() }).asSequence().distinctBy(Configuration.Property.Definition<*>::schema) + nestedProperties.forEach { property -> + property.schema?.let { + description.append(System.lineSeparator()) + description.append("${property.typeName}: ") + description.append(it.description()) + description.append(System.lineSeparator()) + } + } + return description.toString() + } + + override fun describe(configuration: Config): ConfigValue { + + return properties.asSequence().map { it.key to it.describe(configuration) }.fold(configObject()) { config, (key, value) -> config.withValue(key, value) } + } + + override fun equals(other: Any?): Boolean { + + if (this === other) { + return true + } + if (javaClass != other?.javaClass) { + return false + } + + other as Schema + + if (properties != other.properties) { + return false + } + + return true + } + + override fun hashCode(): Int { + + return properties.hashCode() + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..919263dc16 --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Specification.kt @@ -0,0 +1,101 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.ConfigObject +import java.time.Duration +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +interface PropertyDelegate { + + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> + + interface Required { + + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> + + fun optional(defaultValue: TYPE? = null): PropertyDelegate + } + + interface Single { + + operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> + + fun list(): Required> + } + + interface Standard : Required, Single { + + override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> + + fun mapValid(mappedTypeName: String, convert: (TYPE) -> Valid): Standard + + fun map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) } + } + + companion object { + + internal fun long(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::long) + + internal fun int(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::int) + + internal fun boolean(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::boolean) + + internal fun double(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::double) + + internal fun float(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::float) + + internal fun string(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::string) + + internal fun duration(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, Configuration.Property.Definition.Companion::duration) + + internal fun nestedObject(schema: Configuration.Schema?, key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.nestedObject(k, schema, s) }) + + internal fun > enum(key: String?, prefix: String?, enumClass: KClass, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.enum(k, enumClass, s) }) + } +} + +private class PropertyDelegateImpl(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.Standard) : PropertyDelegate.Standard { + + 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.Standard = prop + } + } + + override fun list(): PropertyDelegate.Required> = ListPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).list() }) + + override fun optional(defaultValue: TYPE?): PropertyDelegate = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) }) + + 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 OptionalPropertyDelegateImpl(private val key: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition) : PropertyDelegate { + + override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty> { + + val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties) + return object : ReadOnlyProperty> { + + override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition = prop + } + } +} + +private class ListPropertyDelegateImpl(private val key: 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 prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties) + return object : ReadOnlyProperty> { + + override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Required = prop + } + } + + override fun optional(defaultValue: TYPE?): PropertyDelegate = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) }) +} \ 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 new file mode 100644 index 0000000000..d75a2b1d08 --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/Utils.kt @@ -0,0 +1,53 @@ +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.map(noinline convert: (TYPE) -> MAPPED): PropertyDelegate.Standard = map(MAPPED::class.java.simpleName, convert) + +operator fun Config.get(property: Configuration.Property.Definition): TYPE = property.valueIn(this) + +inline fun Configuration.Specification<*>.nested(specification: Configuration.Specification, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard = nestedObject(schema = specification, key = key, sensitive = sensitive).map(ConfigObject::toConfig).mapValid { value -> specification.parse(value) } + +@Suppress("UNCHECKED_CAST") +internal fun configObject(vararg entries: Pair): ConfigObject { + + var configuration = ConfigFactory.empty() + entries.forEach { entry -> + val value = entry.second + configuration += if (value is Pair<*, *> && value.first is String) { + (entry.first to (ConfigFactory.empty() + value as Pair).root()) + } else { + entry + } + } + return configuration.root() +} + +internal operator fun Config.plus(entry: Pair): Config { + + var value = entry.second ?: return this - entry.first + if (value is Config) { + value = value.root() + } + return withValue(entry.first, ConfigValueFactory.fromAnyRef(value)) +} + +internal operator fun Config.minus(key: String): Config { + + return withoutPath(key) +} + +internal fun Config.serialize(options: ConfigRenderOptions = ConfigRenderOptions.concise().setFormatted(true).setJson(true)): String = root().serialize(options) + +internal fun ConfigValue.serialize(options: ConfigRenderOptions = ConfigRenderOptions.concise().setFormatted(true).setJson(true)): String = render(options) + +internal typealias Valid = Validated + +internal fun valid(target: TYPE) = Validated.valid(target) \ No newline at end of file diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt new file mode 100644 index 0000000000..cdba00e7c7 --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractor.kt @@ -0,0 +1,23 @@ +package net.corda.common.configuration.parsing.internal.versioned + +import com.typesafe.config.Config +import net.corda.common.configuration.parsing.internal.Configuration +import net.corda.common.configuration.parsing.internal.Valid +import net.corda.common.configuration.parsing.internal.valid + +internal class VersionExtractor(versionKey: String, versionDefaultValue: Int?) : Configuration.Version.Extractor { + + private val spec = Spec(versionKey, versionDefaultValue) + + override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid { + + return spec.parse(configuration) + } + + private class Spec(versionKey: String, versionDefaultValue: Int?) : Configuration.Specification("Version") { + + private val version by int(key = versionKey).optional(versionDefaultValue) + + override fun parseValid(configuration: Config) = valid(version.valueIn(configuration)) + } +} \ No newline at end of file diff --git a/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedSpecificationRegistry.kt b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedSpecificationRegistry.kt new file mode 100644 index 0000000000..11cff499d8 --- /dev/null +++ b/common/configuration-parsing/src/main/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedSpecificationRegistry.kt @@ -0,0 +1,30 @@ +package net.corda.common.configuration.parsing.internal.versioned + +import com.typesafe.config.Config +import net.corda.common.configuration.parsing.internal.Configuration +import net.corda.common.configuration.parsing.internal.Valid +import net.corda.common.configuration.parsing.internal.valid +import net.corda.common.validation.internal.Validated.Companion.invalid + +class VersionedSpecificationRegistry private constructor(private val versionFromConfig: (Config) -> Valid, private val specifications: Map>) : (Config) -> Valid> { + + companion object { + + fun mapping(versionParser: Configuration.Value.Parser, specifications: Map>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications) + + fun mapping(versionParser: Configuration.Value.Parser, vararg specifications: Pair>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications.toMap()) + + fun mapping(versionParser: (Config) -> Valid, specifications: Map>) = VersionedSpecificationRegistry(versionParser, specifications) + + fun mapping(versionParser: (Config) -> Valid, vararg specifications: Pair>) = VersionedSpecificationRegistry(versionParser, specifications.toMap()) + } + + override fun invoke(configuration: Config): Valid> { + + return versionFromConfig.invoke(configuration).mapValid { version -> + + val value = specifications[version] + value?.let { valid(it) } ?: invalid, Configuration.Validation.Error>(Configuration.Validation.Error.UnsupportedVersion.of(version)) + } + } +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Address.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Address.kt new file mode 100644 index 0000000000..254d50740d --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Address.kt @@ -0,0 +1,29 @@ +package net.corda.common.configuration.parsing.internal + +import net.corda.common.validation.internal.Validated + +data class Address(val host: String, val port: Int) { + + init { + require(host.isNotBlank()) + require(port > 0) + } + + companion object { + + fun validFromRawValue(rawValue: String, mapError: (String) -> ERROR): Validated { + + val parts = rawValue.split(":") + if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank() || parts[1].toIntOrNull() == null) { + return Validated.invalid(sequenceOf("Value format is \":\"").map(mapError).toSet()) + } + val host = parts[0] + val port = parts[1].toInt() + if (port <= 0) { + return Validated.invalid(sequenceOf("Port value must be greater than zero").map(mapError).toSet()) + } + + return Validated.valid(Address(host, port)) + } + } +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Addresses.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Addresses.kt new file mode 100644 index 0000000000..6c60fc796a --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/Addresses.kt @@ -0,0 +1,3 @@ +package net.corda.common.configuration.parsing.internal + +data class Addresses(val principal: Address, val admin: Address) \ No newline at end of file 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 new file mode 100644 index 0000000000..d2d35e5de1 --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyTest.kt @@ -0,0 +1,184 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.ConfigException +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.Test + +class PropertyTest { + + @Test + fun present_value_with_correct_type() { + + val key = "a.b.c" + val value = 1L + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.long(key) + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isTrue() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThat(property.valueIn(configuration)).isEqualTo(value) + assertThat(configuration[property]).isEqualTo(value) + } + + @Test + fun present_value_with_wrong_type() { + + val key = "a.b.c" + val value = 1 + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.boolean(key) + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isTrue() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java) + } + + @Test + fun present_value_of_list_type() { + + val key = "a.b.c" + val value = listOf(1L, 2L, 3L) + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.long(key).list() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isTrue() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThat(property.valueIn(configuration)).isEqualTo(value) + } + + @Test + fun optional_present_value_of_list_type() { + + val key = "a.b.c" + val value = listOf(1L, 2L, 3L) + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.long(key).list().optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThat(property.valueIn(configuration)).isEqualTo(value) + } + + @Test + fun optional_absent_value_of_list_type() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val property = Configuration.Property.Definition.long(key).list().optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThat(property.valueIn(configuration)).isNull() + + } + + @Test + fun optional_absent_value_of_list_type_with_default_value() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val defaultValue = listOf(1L, 2L, 3L) + val property = Configuration.Property.Definition.long(key).list().optional(defaultValue) + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThat(property.valueIn(configuration)).isEqualTo(defaultValue) + } + + @Test + fun absent_value() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val property = Configuration.Property.Definition.long(key) + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isTrue() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.Missing::class.java) + } + + @Test + fun optional_present_value_with_correct_type() { + + val key = "a.b.c" + val value = 1L + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.long(key).optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThat(property.valueIn(configuration)).isEqualTo(value) + } + + @Test + fun optional_present_value_with_wrong_type() { + + val key = "a.b.c" + val value = 1 + val configuration = configObject(key to value).toConfig() + + val property = Configuration.Property.Definition.boolean(key).optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isTrue() + assertThatThrownBy { property.valueIn(configuration) }.isInstanceOf(ConfigException.WrongType::class.java) + } + + @Test + fun optional_absent_value() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val property = Configuration.Property.Definition.long(key).optional() + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThat(property.valueIn(configuration)).isNull() + } + + @Test + fun optional_absent_with_default_value() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val defaultValue = 23L + val property = Configuration.Property.Definition.long(key).optional(defaultValue) + println(property) + + assertThat(property.key).isEqualTo(key) + assertThat(property.isMandatory).isFalse() + assertThat(property.isSpecifiedBy(configuration)).isFalse() + assertThat(property.valueIn(configuration)).isEqualTo(defaultValue) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b26aca0abb --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/PropertyValidationTest.kt @@ -0,0 +1,337 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.Config +import net.corda.common.validation.internal.Validated.Companion.invalid +import net.corda.common.validation.internal.Validated.Companion.valid +import net.corda.common.validation.internal.Validator +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class PropertyValidationTest { + + @Test + fun absent_value() { + + val key = "a.b.c" + val configuration = configObject().toConfig() + + val property: Validator = Configuration.Property.Definition.long(key) + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun missing_value() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val property: Validator = Configuration.Property.Definition.long(key) + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun absent_list_value() { + + val key = "a.b.c" + val configuration = configObject().toConfig() + + val property: Validator = Configuration.Property.Definition.long(key).list() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun missing_list_value() { + + val key = "a.b.c" + val configuration = configObject(key to null).toConfig() + + val property: Validator = Configuration.Property.Definition.long(key).list() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun wrong_type() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.long(key) + + val configuration = configObject(key to false).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun wrong_floating_numeric_type_when_integer_expected() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.long(key) + + val configuration = configObject(key to 1.2).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun integer_numeric_type_when_floating_expected_works() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.double(key) + + val configuration = configObject(key to 1).toConfig() + + assertThat(property.validate(configuration).isValid).isTrue() + } + + @Test + fun wrong_element_type_for_list() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.long(key).list() + + val configuration = configObject(key to listOf(false, true)).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun list_type_when_declared_single() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.long(key) + + val configuration = configObject(key to listOf(1, 2, 3)).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun single_type_when_declared_list() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.long(key).list() + + val configuration = configObject(key to 1).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> + + assertThat(error.keyName).isEqualTo(key.split(".").last()) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray()) + } + } + } + + @Test + fun wrong_type_in_nested_property() { + + val key = "a.b.c" + + val nestedKey = "d" + val nestedPropertySchema = Configuration.Schema.withProperties(Configuration.Property.Definition.long(nestedKey)) + + val property: Validator = Configuration.Property.Definition.nestedObject(key, nestedPropertySchema) + + val configuration = configObject(key to configObject(nestedKey to false)).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.WrongType::class.java) { error -> + + assertThat(error.keyName).isEqualTo(nestedKey) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray(), nestedKey) + } + } + } + + @Test + fun absent_value_in_nested_property() { + + val key = "a.b.c" + + val nestedKey = "d" + val nestedPropertySchema = Configuration.Schema.withProperties(Configuration.Property.Definition.long(nestedKey)) + + val property: Validator = Configuration.Property.Definition.nestedObject(key, nestedPropertySchema) + + val configuration = configObject(key to configObject()).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(nestedKey) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray(), nestedKey) + } + } + } + + @Test + fun missing_value_in_nested_property() { + + val key = "a.b.c" + + val nestedKey = "d" + val nestedPropertySchema = Configuration.Schema.withProperties(Configuration.Property.Definition.long(nestedKey)) + + val property: Validator = Configuration.Property.Definition.nestedObject(key, nestedPropertySchema) + + val configuration = configObject(key to configObject(nestedKey to null)).toConfig() + + assertThat(property.validate(configuration).errors).satisfies { errors -> + + assertThat(errors).hasSize(1) + assertThat(errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.keyName).isEqualTo(nestedKey) + assertThat(error.path).containsExactly(*key.split(".").toTypedArray(), nestedKey) + } + } + } + + @Test + fun nested_property_without_schema_does_not_validate() { + + val key = "a.b.c" + + val nestedKey = "d" + + val property: Validator = Configuration.Property.Definition.nestedObject(key) + + val configuration = configObject(key to configObject(nestedKey to false)).toConfig() + + assertThat(property.validate(configuration).isValid).isTrue() + } + + @Test + fun valid_mapped_property() { + + val key = "a" + + val property: Validator = Configuration.Property.Definition.string(key).mapValid(::parseAddress) + + val host = "localhost" + val port = 8080 + val value = "$host:$port" + + val configuration = configObject(key to value).toConfig() + + assertThat(property.validate(configuration).isValid).isTrue() + } + + @Test + fun invalid_mapped_property() { + + val key = "a.b.c" + + val property: Validator = Configuration.Property.Definition.string(key).mapValid(::parseAddress) + + val host = "localhost" + val port = 8080 + // No ":" separating the 2 parts. + val value = "$host$port" + + val configuration = configObject(key to value).toConfig() + + val result = property.validate(configuration) + + assertThat(result.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()) + } + } + } + + private fun parseAddress(value: String): Valid
{ + + return try { + val parts = value.split(":") + val host = parts[0].also { require(it.isNotBlank()) } + val port = parts[1].toInt().also { require(it > 0) } + valid(Address(host, port)) + } catch (e: Exception) { + return invalid(Configuration.Validation.Error.BadValue.of("Value must be of format \"host(String):port(Int > 0)\" e.g., \"127.0.0.1:8080\"")) + } + } +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt new file mode 100644 index 0000000000..d468e68361 --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SchemaTest.kt @@ -0,0 +1,203 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.ConfigObject +import com.typesafe.config.ConfigValueFactory +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class SchemaTest { + + @Test + fun validation_with_nested_properties() { + + val prop1 = "prop1" + val prop1Value = "value1" + + val prop2 = "prop2" + val prop2Value = 3L + + val prop3 = "prop3" + val prop4 = "prop4" + val prop4Value = true + val prop5 = "prop5" + val prop5Value = -17.3 + val prop3Value = configObject(prop4 to prop4Value, prop5 to prop5Value) + + val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig() + println(configuration.serialize()) + + val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), double("prop5")) } + val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } + + val result = barConfigSchema.validate(configuration) + println(barConfigSchema.description()) + + assertThat(result.isValid).isTrue() + } + + @Test + fun validation_with_unknown_properties() { + + val prop1 = "prop1" + val prop1Value = "value1" + + val prop2 = "prop2" + val prop2Value = 3L + + val prop3 = "prop3" + val prop4 = "prop4" + val prop4Value = true + val prop5 = "prop5" + val prop5Value = -17.3 + // Here "prop6" is not known to the schema. + val prop3Value = configObject(prop4 to prop4Value, "prop6" to "value6", prop5 to prop5Value) + + // Here "prop4" is not known to the schema. + val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value, "prop4" to "value4").toConfig() + println(configuration.serialize()) + + val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) } + val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } + + val strictErrors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = true)).errors + + assertThat(strictErrors).hasSize(2) + assertThat(strictErrors.filter { error -> error.keyName == "prop4" }).hasSize(1) + assertThat(strictErrors.filter { error -> error.keyName == "prop6" }).hasSize(1) + + val errors = barConfigSchema.validate(configuration, Configuration.Validation.Options(strict = false)).errors + + assertThat(errors).isEmpty() + + val errorsWithDefaultOptions = barConfigSchema.validate(configuration).errors + + assertThat(errorsWithDefaultOptions).isEmpty() + } + + @Test + fun validation_with_unknown_properties_non_strict() { + + val prop1 = "prop1" + val prop1Value = "value1" + + val prop2 = "prop2" + val prop2Value = 3L + + val prop3 = "prop3" + val prop4 = "prop4" + val prop4Value = true + val prop5 = "prop5" + val prop5Value = -17.3 + // Here "prop6" is not known to the schema, but it is not in strict mode. + val prop3Value = configObject(prop4 to prop4Value, "prop6" to "value6", prop5 to prop5Value) + + // Here "prop4" is not known to the schema, but it is not in strict mode. + val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value, "prop4" to "value4").toConfig() + println(configuration.serialize()) + + val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) } + val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } + + val result = barConfigSchema.validate(configuration) + + assertThat(result.isValid).isTrue() + } + + @Test + fun validation_with_wrong_nested_properties() { + + val prop1 = "prop1" + val prop1Value = "value1" + + val prop2 = "prop2" + // This value is wrong, should be an Int. + val prop2Value = false + + val prop3 = "prop3" + val prop4 = "prop4" + // This value is wrong, should be a Boolean. + val prop4Value = 44444 + val prop5 = "prop5" + val prop5Value = -17.3 + val prop3Value = configObject(prop4 to prop4Value, prop5 to prop5Value) + + val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig() + println(configuration.serialize()) + + val fooConfigSchema = Configuration.Schema.withProperties { setOf(boolean("prop4"), double("prop5")) } + val barConfigSchema = Configuration.Schema.withProperties { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } + + val errors = barConfigSchema.validate(configuration).errors + errors.forEach(::println) + + assertThat(errors).hasSize(2) + } + + @Test + fun describe_with_nested_properties_does_not_show_sensitive_values() { + + val prop1 = "prop1" + val prop1Value = "value1" + + val prop2 = "prop2" + val prop2Value = 3L + + val prop3 = "prop3" + val prop4 = "prop4" + val prop4Value = true + val prop5 = "prop5" + val prop5Value = "sensitive!" + val prop3Value = configObject(prop4 to prop4Value, prop5 to prop5Value) + + val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig() + + val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) } + val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema)) } + + val printedConfiguration = barConfigSchema.describe(configuration) + + val description = printedConfiguration.serialize().also { println(it) } + + val descriptionObj = (printedConfiguration as ConfigObject).toConfig() + + assertThat(descriptionObj.getAnyRef("prop3.prop5")).isEqualTo(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER) + assertThat(description).doesNotContain(prop5Value) + } + + @Test + fun describe_with_nested_properties_list_does_not_show_sensitive_values() { + + val prop1 = "prop1" + val prop1Value = "value1" + + val prop2 = "prop2" + val prop2Value = 3L + + val prop3 = "prop3" + val prop4 = "prop4" + val prop4Value = true + val prop5 = "prop5" + val prop5Value = "sensitive!" + val prop3Value = ConfigValueFactory.fromIterable(listOf(configObject(prop4 to prop4Value, prop5 to prop5Value), configObject(prop4 to prop4Value, prop5 to prop5Value))) + + val configuration = configObject(prop1 to prop1Value, prop2 to prop2Value, prop3 to prop3Value).toConfig() + + val fooConfigSchema = Configuration.Schema.withProperties(name = "Foo") { setOf(boolean("prop4"), string("prop5", sensitive = true)) } + val barConfigSchema = Configuration.Schema.withProperties(name = "Bar") { setOf(string(prop1), long(prop2), nestedObject("prop3", fooConfigSchema).list()) } + + val printedConfiguration = barConfigSchema.describe(configuration) + + val description = printedConfiguration.serialize().also { println(it) } + + val descriptionObj = (printedConfiguration as ConfigObject).toConfig() + + assertThat(descriptionObj.getObjectList("prop3")).satisfies { objects -> + + objects.forEach { obj -> + + assertThat(obj.toConfig().getString("prop5")).isEqualTo(Configuration.Property.Definition.SENSITIVE_DATA_PLACEHOLDER) + } + } + assertThat(description).doesNotContain(prop5Value) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a94489c99e --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/SpecificationTest.kt @@ -0,0 +1,113 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.Config +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class SpecificationTest { + + private object RpcSettingsSpec : Configuration.Specification("RpcSettings") { + + private object AddressesSpec : Configuration.Specification("Addresses") { + + val principal by string().mapValid(::parseAddress) + val admin by string().mapValid(::parseAddress) + + override fun parseValid(configuration: Config) = valid(Addresses(configuration[principal], configuration[admin])) + + private fun parseAddress(rawValue: String): Valid
{ + + return Address.validFromRawValue(rawValue) { error -> Configuration.Validation.Error.BadValue.of(error) } + } + } + + val useSsl by boolean() + val addresses by nested(AddressesSpec) + + override fun parseValid(configuration: Config) = valid(RpcSettingsImpl(configuration[addresses], configuration[useSsl])) + } + + @Test + fun parse() { + + val useSslValue = true + val principalAddressValue = Address("localhost", 8080) + val adminAddressValue = Address("127.0.0.1", 8081) + val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}") + val configuration = configObject("useSsl" to useSslValue, "addresses" to addressesValue).toConfig() + + val rpcSettings = RpcSettingsSpec.parse(configuration) + + assertThat(rpcSettings.isValid).isTrue() + assertThat(rpcSettings.valueOrThrow()).satisfies { value -> + + assertThat(value.useSsl).isEqualTo(useSslValue) + assertThat(value.addresses).satisfies { addresses -> + + assertThat(addresses.principal).isEqualTo(principalAddressValue) + assertThat(addresses.admin).isEqualTo(adminAddressValue) + } + } + } + + @Test + fun validate() { + + val principalAddressValue = Address("localhost", 8080) + val adminAddressValue = Address("127.0.0.1", 8081) + val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}") + // Here "useSsl" shouldn't be `null`, hence causing the validation to fail. + val configuration = configObject("useSsl" to null, "addresses" to addressesValue).toConfig() + + val rpcSettings = RpcSettingsSpec.parse(configuration) + + assertThat(rpcSettings.errors).hasSize(1) + assertThat(rpcSettings.errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.MissingValue::class.java) { error -> + + assertThat(error.path).containsExactly("useSsl") + } + } + + @Test + fun validate_with_domain_specific_errors() { + + val useSslValue = true + val principalAddressValue = Address("localhost", 8080) + val adminAddressValue = Address("127.0.0.1", 8081) + // Here, for the "principal" property, the value is incorrect, as the port value is unacceptable. + val addressesValue = configObject("principal" to "${principalAddressValue.host}:-10", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}") + val configuration = configObject("useSsl" to useSslValue, "addresses" to addressesValue).toConfig() + + val rpcSettings = RpcSettingsSpec.parse(configuration) + + assertThat(rpcSettings.errors).hasSize(1) + assertThat(rpcSettings.errors.first()).isInstanceOfSatisfying(Configuration.Validation.Error.BadValue::class.java) { error -> + + assertThat(error.path).containsExactly("addresses", "principal") + assertThat(error.keyName).isEqualTo("principal") + assertThat(error.typeName).isEqualTo(Address::class.java.simpleName) + } + } + + @Test + fun chained_delegated_properties_are_not_added_multiple_times() { + + val spec = object : Configuration.Specification?>("Test") { + + @Suppress("unused") + val myProp by string().list().optional() + + override fun parseValid(configuration: Config) = valid(configuration[myProp]) + } + + assertThat(spec.properties).hasSize(1) + } + + private interface RpcSettings { + + val addresses: Addresses + val useSsl: Boolean + } + + private data class RpcSettingsImpl(override val addresses: Addresses, override val useSsl: Boolean) : RpcSettings +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt new file mode 100644 index 0000000000..bf2c2f92bf --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/TestUtils.kt @@ -0,0 +1,19 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.Config +import net.corda.common.validation.internal.Validated + +internal val extractMissingVersion: Configuration.Value.Parser = extractVersion(null) + +internal fun extractVersion(value: Int?) = extractValidValue(value) + +internal fun extractPresentVersion(value: Int) = extractValidValue(value) + +internal fun extractValidValue(value: VALUE) = extractValue(Validated.valid(value)) + +internal fun extractValueWithErrors(errors: Set) = extractValue(Validated.invalid(errors)) + +internal fun extractValue(value: Valid) = object : Configuration.Value.Parser { + + override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid = value +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/UtilsTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/UtilsTest.kt new file mode 100644 index 0000000000..936db1a789 --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/UtilsTest.kt @@ -0,0 +1,25 @@ +package net.corda.common.configuration.parsing.internal + +import com.typesafe.config.ConfigFactory +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class UtilsTest { + + @Test + fun serialize_deserialize_configuration() { + + var rawConfiguration = ConfigFactory.empty() + + rawConfiguration += "key" to "value" + rawConfiguration += "key1.key2" to configObject("key3" to "value2", "key4" to configObject("key5" to -2.0, "key6" to false)) + rawConfiguration += "key7" to listOf("Hey!", true, 17, 0.0, configObject("key8" to listOf(-12.0, "HH", false), "key9" to "blah")) + + val serialized = rawConfiguration.serialize() + println(serialized) + + val deserialized = ConfigFactory.parseString(serialized) + + assertThat(deserialized).isEqualTo(rawConfiguration) + } +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt new file mode 100644 index 0000000000..aa2e6e7139 --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionExtractorTest.kt @@ -0,0 +1,63 @@ +package net.corda.common.configuration.parsing.internal.versioned + +import com.typesafe.config.Config +import net.corda.common.configuration.parsing.internal.Configuration +import net.corda.common.configuration.parsing.internal.Valid +import net.corda.common.configuration.parsing.internal.configObject +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class VersionExtractorTest { + + private val versionExtractor = Configuration.Version.Extractor.fromKey("configuration.metadata.version") + private val extractVersion: (Config) -> Valid = { config -> versionExtractor.parse(config) } + + @Test + fun version_header_extraction_present() { + + val versionValue = Configuration.Version.Extractor.DEFAULT_VERSION_VALUE + 1 + val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to versionValue), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() + + val version = extractVersion.invoke(rawConfiguration).valueOrThrow() + assertThat(version).isEqualTo(versionValue) + } + + @Test + fun version_header_extraction_no_metadata() { + + val rawConfiguration = configObject("configuration" to configObject("node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() + + val version = extractVersion.invoke(rawConfiguration).valueOrThrow() + assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) + } + + @Test + fun version_header_extraction_no_key() { + + val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject(), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() + + val version = extractVersion.invoke(rawConfiguration).valueOrThrow() + + assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) + } + + @Test + fun version_header_extraction_no_value() { + + val rawConfiguration = configObject("configuration" to configObject("metadata" to configObject("version" to null), "node" to configObject("p2pAddress" to "localhost:8080"))).toConfig() + + val version = extractVersion.invoke(rawConfiguration).valueOrThrow() + + assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) + } + + @Test + fun version_header_extraction_no_configuration() { + + val rawConfiguration = configObject().toConfig() + + val version = extractVersion.invoke(rawConfiguration).valueOrThrow() + + assertThat(version).isEqualTo(Configuration.Version.Extractor.DEFAULT_VERSION_VALUE) + } +} \ No newline at end of file diff --git a/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt new file mode 100644 index 0000000000..60b35480e5 --- /dev/null +++ b/common/configuration-parsing/src/test/kotlin/net/corda/common/configuration/parsing/internal/versioned/VersionedParsingExampleTest.kt @@ -0,0 +1,131 @@ +package net.corda.common.configuration.parsing.internal.versioned + +import com.typesafe.config.Config +import net.corda.common.configuration.parsing.internal.* +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 + +class VersionedParsingExampleTest { + + @Test + fun correct_parsing_function_is_used_for_present_version() { + + val versionParser = Configuration.Version.Extractor.fromKey("configuration.metadata.version", null) + val extractVersion: (Config) -> Valid = { config -> versionParser.parseRequired(config) } + val parseConfiguration = VersionedSpecificationRegistry.mapping(extractVersion, 1 to RpcSettingsSpec.V1, 2 to RpcSettingsSpec.V2) + + val principalAddressValue = Address("localhost", 8080) + val adminAddressValue = Address("127.0.0.1", 8081) + + val configurationV1 = configObject("configuration.metadata.version" to 1, "principalHost" to principalAddressValue.host, "principalPort" to principalAddressValue.port, "adminHost" to adminAddressValue.host, "adminPort" to adminAddressValue.port).toConfig().also { println(it.serialize()) } + val rpcSettingsFromVersion1Conf = parseConfiguration.invoke(configurationV1).mapValid { it.parse(configurationV1) } + + assertResult(rpcSettingsFromVersion1Conf, principalAddressValue, adminAddressValue) + + val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}") + val configurationV2 = configObject("configuration.metadata.version" to 2, "configuration.value.addresses" to addressesValue).toConfig().also { println(it.serialize()) } + val rpcSettingsFromVersion2Conf = parseConfiguration.invoke(configurationV2).mapValid { it.parse(configurationV2) } + + assertResult(rpcSettingsFromVersion2Conf, principalAddressValue, adminAddressValue) + } + + @Test + fun default_value_is_used_for_absent_version() { + + val defaultVersion = 2 + val versionParser = Configuration.Version.Extractor.fromKey("configuration.metadata.version", defaultVersion) + val extractVersion: (Config) -> Valid = { config -> versionParser.parseRequired(config) } + val parseConfiguration = VersionedSpecificationRegistry.mapping(extractVersion, 1 to RpcSettingsSpec.V1, 2 to RpcSettingsSpec.V2) + + val principalAddressValue = Address("localhost", 8080) + val adminAddressValue = Address("127.0.0.1", 8081) + + val addressesValue = configObject("principal" to "${principalAddressValue.host}:${principalAddressValue.port}", "admin" to "${adminAddressValue.host}:${adminAddressValue.port}") + val configurationV2 = configObject("configuration.value.addresses" to addressesValue).toConfig().also { println(it.serialize()) } + val rpcSettingsFromVersion2Conf = parseConfiguration.invoke(configurationV2).mapValid { it.parse(configurationV2) } + + assertResult(rpcSettingsFromVersion2Conf, principalAddressValue, adminAddressValue) + } + + private fun assertResult(result: Valid, principalAddressValue: Address, adminAddressValue: Address) { + + assertThat(result.isValid).isTrue() + assertThat(result.valueOrThrow()).satisfies { value -> + + assertThat(value.principal).isEqualTo(principalAddressValue) + assertThat(value.admin).isEqualTo(adminAddressValue) + } + } + + private data class RpcSettings(val principal: Address, val admin: Address) + + private object RpcSettingsSpec { + + private fun addressFor(host: String, port: Int): Valid
{ + + return try { + require(host.isNotBlank()) + require(port > 0) + Validated.valid(Address(host, port)) + } catch (e: Exception) { + return Validated.invalid(Configuration.Validation.Error.BadValue.of(host, Address::class.java.simpleName, "Value must be of format \"host(String):port(Int > 0)\" e.g., \"127.0.0.1:8080\"")) + } + } + + object V1 : Configuration.Specification("RpcSettings") { + + private val principalHost by string() + private val principalPort by int() + + private val adminHost by string() + private val adminPort by int() + + override fun parseValid(configuration: Config): Valid { + + val principalHost = configuration[principalHost] + val principalPort = configuration[principalPort] + + val adminHost = configuration[adminHost] + val adminPort = configuration[adminPort] + + val principalAddress = addressFor(principalHost, principalPort) + val adminAddress = addressFor(adminHost, adminPort) + + return if (principalAddress.isValid && adminAddress.isValid) { + return valid(RpcSettings(principalAddress.value, adminAddress.value)) + } else { + invalid(principalAddress.errors + adminAddress.errors) + } + } + } + + object V2 : Configuration.Specification("RpcSettings", prefix = "configuration.value") { + + private object AddressesSpec : Configuration.Specification("Addresses") { + + val principal by string().mapValid(::parseAddress) + + val admin by string().mapValid(::parseAddress) + + override fun parseValid(configuration: Config) = valid(Addresses(configuration[principal],configuration[admin])) + + private fun parseAddress(rawValue: String): Valid
{ + + return Address.validFromRawValue(rawValue) { error -> Configuration.Validation.Error.BadValue.of(error) } + } + } + + private val addresses by nested(AddressesSpec) + + override fun parseValid(configuration: Config): Valid { + + val addresses = configuration[addresses] + return valid(RpcSettings(addresses.principal, addresses.admin)) + } + } + } +} + +private fun Configuration.Version.Extractor.parseRequired(config: Config, options: Configuration.Validation.Options = Configuration.Validation.Options.defaults) = parse(config, options).map { it ?: throw IllegalStateException("Absent version value.") } \ No newline at end of file diff --git a/common/validation/README.md b/common/validation/README.md new file mode 100644 index 0000000000..ea47dda1bb --- /dev/null +++ b/common/validation/README.md @@ -0,0 +1,3 @@ +# common-validation + +This module provides basic types to facilitate multi-step validation of typed values. \ No newline at end of file diff --git a/common/validation/build.gradle b/common/validation/build.gradle new file mode 100644 index 0000000000..17333abe48 --- /dev/null +++ b/common/validation/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'kotlin' + +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + testCompile project(":test-utils") +} + +jar { + baseName 'common-validation' +} + +publish { + name jar.baseName +} \ No newline at end of file diff --git a/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt b/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt new file mode 100644 index 0000000000..7a2d813f70 --- /dev/null +++ b/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validated.kt @@ -0,0 +1,140 @@ +package net.corda.common.validation.internal + +import java.util.Collections.emptySet + +/** + * A monad, providing information about validation result. + * It wraps either a valid [TARGET] or a set of [ERROR]. + */ +interface Validated { + + /** + * The valid [TARGET] value. + * + * @throws IllegalStateException if accessed in presence of validation errors. + */ + val value: TARGET + + /** + * The errors produced during validation, if any. + */ + val errors: Set + + /** + * Whether a valid [TARGET] value is present. + */ + val isValid: Boolean get() = errors.isEmpty() + + /** + * Whether there were validation errors. + */ + val isInvalid: Boolean get() = !isValid + + /** + * Returns a valid [TARGET] if no validation errors are present. Otherwise, it throws the exception produced by [exceptionOnErrors], defaulting to [IllegalStateException]. + * + * @throws IllegalStateException or the result of [exceptionOnErrors] if there are errors. + */ + fun valueOrThrow(exceptionOnErrors: (Set) -> Exception = { errors -> IllegalStateException(errors.joinToString(System.lineSeparator())) }): TARGET + + /** + * Applies the [convert] function to the [TARGET] value, if valid. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set. + */ + fun map(convert: (TARGET) -> MAPPED): Validated + + /** + * Applies the [convert] function to the [TARGET] value, if valid, returning its [Validated] output. Otherwise, returns a [Validated] monad with a [MAPPED] generic type and the current errors set. + */ + fun mapValid(convert: (TARGET) -> Validated): Validated + + /** + * Applies the [convertError] function to the errors set, if not empty. Otherwise, returns a [Validated] wrapper with a [MAPPED_ERROR] generic type. + */ + fun mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated + + companion object { + + /** + * Constructs a [Validated] wrapper with given valid [target] value and no errors. + */ + fun valid(target: T): Validated.Result = Validated.Result.Successful(target) + + /** + * Constructs an invalid [Validated] wrapper with given errors and no value. + */ + fun invalid(errors: Set): Validated.Result = Validated.Result.Unsuccessful(errors) + + /** + * @see invalid + */ + fun invalid(vararg errors: E): Validated.Result = invalid(errors.toSet()) + + /** + * Constructs a [Validated] wrapper with valid [T] value if [errors] is empty. Otherwise, it constructs an invalid wrapper with no value. + * + * @see valid + * @see invalid + */ + fun withResult(target: T, errors: Set): Validated = if (errors.isEmpty()) valid(target) else invalid(errors) + } + + /** + * Models the result of validating a [TARGET] value, producing [ERROR]s if rules are violated. + */ + sealed class Result : Validated { + + /** + * A successful validation result, containing a valid [TARGET] value and no [ERROR]s. + */ + class Successful(override val value: TARGET) : Result(), Validated { + + override val errors: Set = emptySet() + + override fun valueOrThrow(exceptionOnErrors: (Set) -> Exception) = value + + override fun map(convert: (TARGET) -> MAPPED): Validated { + + return valid(convert.invoke(value)) + } + + override fun mapValid(convert: (TARGET) -> Validated): Validated { + + return convert.invoke(value) + } + + override fun mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated { + + return valid(value) + } + } + + /** + * An unsuccessful validation result, containing [ERROR]s and no valid [TARGET] value. + */ + class Unsuccessful(override val errors: Set) : Result(), Validated { + + init { + require(errors.isNotEmpty()) + } + + override val value: TARGET get() = throw IllegalStateException("Invalid state.") + + override fun valueOrThrow(exceptionOnErrors: (Set) -> Exception) = throw exceptionOnErrors.invoke(errors) + + override fun map(convert: (TARGET) -> MAPPED): Validated { + + return invalid(errors) + } + + override fun mapValid(convert: (TARGET) -> Validated): Validated { + + return invalid(errors) + } + + override fun mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated { + + return invalid(errors.asSequence().map(convertError).toSet()) + } + } + } +} \ No newline at end of file diff --git a/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validator.kt b/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validator.kt new file mode 100644 index 0000000000..2ba54acdcb --- /dev/null +++ b/common/validation/src/main/kotlin/net/corda/common/validation/internal/Validator.kt @@ -0,0 +1,12 @@ +package net.corda.common.validation.internal + +/** + * Defines validation behaviour for [TARGET] value and given [OPTIONS], raising [ERROR]s if rules are violated. + */ +interface Validator { + + /** + * Validates [target] using given [options], producing a [Validated] monad wrapping either a valid [target] or a set of [ERROR]s. + */ + fun validate(target: TARGET, options: OPTIONS? = null): Validated +} \ No newline at end of file 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 7286fe8abc..8ba2cc6b88 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 @@ -53,6 +53,12 @@ operator fun Config.getValue(receiver: Any, metadata: KProperty<*>): T return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle) } +// Problems: +// - Forces you to have a primary constructor with all fields of name and type matching the configuration file structure. +// - Encourages weak bean-like types. +// - Cannot support a many-to-one relationship between configuration file structures and configuration domain type. This is essential for versioning of the configuration files. +// - It's complicated and based on reflection, meaning problems with it are typically found at runtime. +// - It doesn't support validation errors in a structured way. If something goes wrong, it throws exceptions, which doesn't support good usability practices like displaying all the errors at once. fun Config.parseAs(clazz: KClass, onUnknownKeys: ((Set, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle, nestedPath: String? = null): T { // Use custom parser if provided, instead of treating the object as data class. clazz.findAnnotation()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) } diff --git a/settings.gradle b/settings.gradle index 118033c379..51a5677ce4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,6 +62,14 @@ include 'samples:cordapp-configuration' include 'samples:network-verifier' include 'serialization' +// Common libraries - start +include 'common-validation' +project(":common-validation").projectDir = new File("$settingsDir/common/validation") + +include 'common-configuration-parsing' +project(":common-configuration-parsing").projectDir = new File("$settingsDir/common/configuration-parsing") +// Common libraries - end + apply from: 'buildCacheSettings.gradle' if (JavaVersion.current() == JavaVersion.VERSION_1_8) { @@ -71,3 +79,4 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_8) { include 'core-deterministic:testing:verifier' include 'serialization-deterministic' } +