[CORDA-1778, CORDA-1835]: Decoupled configuration parsing mechanism (#4093)

This commit is contained in:
Michele Sollecito 2018-10-25 16:45:14 +01:00 committed by GitHub
parent 01799cfc2d
commit 28dd3ac873
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2391 additions and 2 deletions

6
.idea/compiler.xml generated
View File

@ -30,8 +30,10 @@
<module name="client_test" target="1.8" />
<module name="cliutils_main" target="1.8" />
<module name="cliutils_test" target="1.8" />
<module name="common_main" target="1.8" />
<module name="common_test" target="1.8" />
<module name="common-configuration-parsing_main" target="1.8" />
<module name="common-configuration-parsing_test" target="1.8" />
<module name="common-validation_main" target="1.8" />
<module name="common-validation_test" target="1.8" />
<module name="confidential-identities_main" target="1.8" />
<module name="confidential-identities_test" target="1.8" />
<module name="contracts-states_integrationTest" target="1.8" />

9
common/README.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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
}

View File

@ -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<TYPE> {
/**
* 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<VALUE> {
/**
* 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<VALUE>
}
}
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<TYPE> : Configuration.Property.Metadata, Configuration.Validator, Configuration.Value.Extractor<TYPE>, Configuration.Describer, Configuration.Value.Parser<TYPE> {
override fun isSpecifiedBy(configuration: Config): Boolean = configuration.hasPath(key)
/**
* Defines a required property, which must provide a value or produce an error.
*/
interface Required<TYPE> : Definition<TYPE> {
/**
* 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<TYPE?>
}
/**
* 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<TYPE> : Definition<TYPE> {
/**
* Returns a required property expecting multiple values for the relevant key.
*/
fun list(): Required<List<TYPE>>
}
/**
* Default property definition, required and single-value.
*/
interface Standard<TYPE> : Required<TYPE>, Single<TYPE> {
/**
* Passes the value to a validating mapping function, provided this is valid in the first place.
*/
fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Validated<MAPPED, Validation.Error>): Standard<MAPPED>
/**
* Passes the value to a non-validating mapping function, provided this is valid in the first place.
*/
fun <MAPPED : Any> map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard<MAPPED> = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
}
override fun parse(configuration: Config, options: Configuration.Validation.Options): Validated<TYPE, Validation.Error> {
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<Long> = LongProperty(key, sensitive)
/**
* Returns a [Configuration.Property.Definition.Standard] with value of type [Int].
*/
fun int(key: String, sensitive: Boolean = false): Standard<Int> = long(key, sensitive).mapValid { value ->
try {
valid(Math.toIntExact(value))
} catch (e: ArithmeticException) {
invalid<Int, Configuration.Validation.Error>(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<Boolean> = 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<Double> = 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<Float> = double(key, sensitive).mapValid { value ->
val floatValue = value.toFloat()
if (floatValue.isInfinite() || floatValue.isNaN()) {
invalid<Float, Configuration.Validation.Error>(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<String> = 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<Duration> = 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<ConfigObject> = 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 : Enum<ENUM>> enum(key: String, enumClass: KClass<ENUM>, sensitive: Boolean = false): Standard<ENUM> = 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<Property.Definition<*>>
companion object {
/**
* Constructs a schema with given name and properties.
*/
fun withProperties(name: String? = null, properties: Iterable<Property.Definition<*>>): 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<Property.Definition<*>>): 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<VALUE>(name: String?, private val prefix: String? = null) : Configuration.Schema, Configuration.Value.Parser<VALUE> {
private val mutableProperties = mutableSetOf<Property.Definition<*>>()
override val properties: Set<Property.Definition<*>> = 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<Long> = 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<Int> = 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<Boolean> = 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<Double> = 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<Float> = 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<String> = 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<Duration> = 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<ConfigObject> = 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 : Enum<ENUM>> enum(key: String? = null, enumClass: KClass<ENUM>, sensitive: Boolean = false): PropertyDelegate.Standard<ENUM> = 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<VALUE> = 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<VALUE>
}
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<String> = emptyList()) {
internal companion object {
private const val UNKNOWN = "<unknown>"
private fun contextualize(keyName: String, containingPath: List<String>): Pair<String, List<String>> {
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<String> 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = emptyList()) : Configuration.Validation.Error(keyName, typeName, message, containingPath) {
internal companion object {
internal fun of(message: String, keyName: String = UNKNOWN, typeName: String = UNKNOWN, containingPath: List<String> = 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<String> = 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<String> = 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<String> = 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<Int?> {
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<Config, Configuration.Validation.Error, Configuration.Validation.Options>
}

View File

@ -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<Long>(key, Long::class.javaObjectType.simpleName, Config::getLong, Config::getLongList, sensitive) {
override fun validate(target: Config, options: Configuration.Validation.Options?): Valid<Config> {
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<TYPE>(override val key: String, typeNameArg: String, private val extractSingleValue: (Config, String) -> TYPE, internal val extractListValue: (Config, String) -> List<TYPE>, override val isSensitive: Boolean = false, final override val schema: Configuration.Schema? = null) : Configuration.Property.Definition.Standard<TYPE> {
override fun valueIn(configuration: Config) = extractSingleValue.invoke(configuration, key)
override val typeName: String = schema?.let { "#${it.name ?: "Object@$key"}" } ?: typeNameArg
override fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Valid<MAPPED>): Configuration.Property.Definition.Standard<MAPPED> = FunctionalProperty(this, mappedTypeName, extractListValue, convert)
override fun optional(defaultValue: TYPE?): Configuration.Property.Definition<TYPE?> = OptionalProperty(this, defaultValue)
override fun list(): Configuration.Property.Definition.Required<List<TYPE>> = 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<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
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<TYPE>(delegate: StandardProperty<TYPE>) : RequiredDelegatedProperty<List<TYPE>, StandardProperty<TYPE>>(delegate) {
override val typeName: String = "List<${delegate.typeName}>"
override fun valueIn(configuration: Config): List<TYPE> = delegate.extractListValue.invoke(configuration, key)
override fun validate(target: Config, options: Configuration.Validation.Options?): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
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<TYPE>(delegate: Configuration.Property.Definition.Required<TYPE>, private val defaultValue: TYPE?) : DelegatedProperty<TYPE?, Configuration.Property.Definition.Required<TYPE>>(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<Config> {
val result = delegate.validate(target, options)
val error = result.errors.asSequence().filterIsInstance<Configuration.Validation.Error.MissingValue>().singleOrNull()
return when {
error != null -> if (result.errors.size > 1) result else valid(target)
else -> result
}
}
}
private class FunctionalProperty<TYPE, MAPPED : Any>(delegate: Configuration.Property.Definition.Standard<TYPE>, private val mappedTypeName: String, internal val extractListValue: (Config, String) -> List<TYPE>, private val convert: (TYPE) -> Valid<MAPPED>) : RequiredDelegatedProperty<MAPPED, Configuration.Property.Definition.Standard<TYPE>>(delegate), Configuration.Property.Definition.Standard<MAPPED> {
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 <M : Any> mapValid(mappedTypeName: String, convert: (MAPPED) -> Valid<M>): Configuration.Property.Definition.Standard<M> = FunctionalProperty(delegate, mappedTypeName, extractListValue, { target: TYPE -> this.convert.invoke(target).mapValid(convert) })
override fun list(): Configuration.Property.Definition.Required<List<MAPPED>> = FunctionalListProperty(this)
override fun validate(target: Config, options: Configuration.Validation.Options?): Valid<Config> {
val errors = mutableSetOf<Configuration.Validation.Error>()
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<RAW, TYPE : Any>(delegate: FunctionalProperty<RAW, TYPE>) : RequiredDelegatedProperty<List<TYPE>, FunctionalProperty<RAW, TYPE>>(delegate) {
override val typeName: String = "List<${super.typeName}>"
override fun valueIn(configuration: Config): List<TYPE> = 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<Config> {
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<TYPE, DELEGATE : Configuration.Property.Metadata>(protected val delegate: DELEGATE) : Configuration.Property.Metadata by delegate, Configuration.Property.Definition<TYPE> {
final override fun toString() = "\"$key\": \"$typeName\""
}
private abstract class RequiredDelegatedProperty<TYPE, DELEGATE : Configuration.Property.Definition.Required<*>>(delegate: DELEGATE) : DelegatedProperty<TYPE, DELEGATE>(delegate), Configuration.Property.Definition.Required<TYPE> {
final override fun optional(defaultValue: TYPE?): Configuration.Property.Definition<TYPE?> = 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<Configuration.Validation.Error> {
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) }

View File

@ -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.Property.Definition<*>>) : 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<Config> {
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()
}
}

View File

@ -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<TYPE> {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition<TYPE>>
interface Required<TYPE> {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Required<TYPE>>
fun optional(defaultValue: TYPE? = null): PropertyDelegate<TYPE?>
}
interface Single<TYPE> {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Single<TYPE>>
fun list(): Required<List<TYPE>>
}
interface Standard<TYPE> : Required<TYPE>, Single<TYPE> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Standard<TYPE>>
fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Valid<MAPPED>): Standard<MAPPED>
fun <MAPPED : Any> map(mappedTypeName: String, convert: (TYPE) -> MAPPED): Standard<MAPPED> = mapValid(mappedTypeName) { value -> valid(convert.invoke(value)) }
}
companion object {
internal fun long(key: String?, prefix: String?, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard<Long> = 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<Int> = 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<Boolean> = 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<Double> = 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<Float> = 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<String> = 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<Duration> = 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<ConfigObject> = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.nestedObject(k, schema, s) })
internal fun <ENUM : Enum<ENUM>> enum(key: String?, prefix: String?, enumClass: KClass<ENUM>, sensitive: Boolean, addProperty: (Configuration.Property.Definition<*>) -> Unit): Standard<ENUM> = PropertyDelegateImpl(key, prefix, sensitive, addProperty, { k, s -> Configuration.Property.Definition.enum(k, enumClass, s) })
}
}
private class PropertyDelegateImpl<TYPE>(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<TYPE>) : PropertyDelegate.Standard<TYPE> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Standard<TYPE>> {
val shortName = key ?: property.name
val prop = construct.invoke(prefix?.let { "$prefix.$shortName" } ?: shortName, sensitive).also(addToProperties)
return object : ReadOnlyProperty<Any?, Configuration.Property.Definition.Standard<TYPE>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Standard<TYPE> = prop
}
}
override fun list(): PropertyDelegate.Required<List<TYPE>> = ListPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).list() })
override fun optional(defaultValue: TYPE?): PropertyDelegate<TYPE?> = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) })
override fun <MAPPED : Any> mapValid(mappedTypeName: String, convert: (TYPE) -> Valid<MAPPED>): PropertyDelegate.Standard<MAPPED> = PropertyDelegateImpl(key, prefix, sensitive, addToProperties, { k, s -> construct.invoke(k, s).mapValid(mappedTypeName) { value -> convert.invoke(value) } })
}
private class OptionalPropertyDelegateImpl<TYPE>(private val key: String?, private val sensitive: Boolean = false, private val addToProperties: (Configuration.Property.Definition<*>) -> Unit, private val construct: (String, Boolean) -> Configuration.Property.Definition<TYPE?>) : PropertyDelegate<TYPE?> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition<TYPE?>> {
val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties)
return object : ReadOnlyProperty<Any?, Configuration.Property.Definition<TYPE?>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition<TYPE?> = prop
}
}
}
private class ListPropertyDelegateImpl<TYPE>(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<TYPE>) : PropertyDelegate.Required<TYPE> {
override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, Configuration.Property.Definition.Required<TYPE>> {
val prop = construct.invoke(key ?: property.name, sensitive).also(addToProperties)
return object : ReadOnlyProperty<Any?, Configuration.Property.Definition.Required<TYPE>> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Configuration.Property.Definition.Required<TYPE> = prop
}
}
override fun optional(defaultValue: TYPE?): PropertyDelegate<TYPE?> = OptionalPropertyDelegateImpl(key, sensitive, addToProperties, { k, s -> construct.invoke(k, s).optional(defaultValue) })
}

View File

@ -0,0 +1,53 @@
package net.corda.common.configuration.parsing.internal
import com.typesafe.config.*
import net.corda.common.validation.internal.Validated
inline fun <TYPE, reified MAPPED : Any> Configuration.Property.Definition.Standard<TYPE>.mapValid(noinline convert: (TYPE) -> Valid<MAPPED>): Configuration.Property.Definition.Standard<MAPPED> = mapValid(MAPPED::class.java.simpleName, convert)
inline fun <reified ENUM : Enum<ENUM>, VALUE : Any> Configuration.Specification<VALUE>.enum(key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard<ENUM> = enum(key, ENUM::class, sensitive)
inline fun <TYPE, reified MAPPED : Any> PropertyDelegate.Standard<TYPE>.mapValid(noinline convert: (TYPE) -> Valid<MAPPED>): PropertyDelegate.Standard<MAPPED> = mapValid(MAPPED::class.java.simpleName, convert)
inline fun <TYPE, reified MAPPED : Any> PropertyDelegate.Standard<TYPE>.map(noinline convert: (TYPE) -> MAPPED): PropertyDelegate.Standard<MAPPED> = map(MAPPED::class.java.simpleName, convert)
operator fun <TYPE> Config.get(property: Configuration.Property.Definition<TYPE>): TYPE = property.valueIn(this)
inline fun <reified NESTED : Any> Configuration.Specification<*>.nested(specification: Configuration.Specification<NESTED>, key: String? = null, sensitive: Boolean = false): PropertyDelegate.Standard<NESTED> = nestedObject(schema = specification, key = key, sensitive = sensitive).map(ConfigObject::toConfig).mapValid { value -> specification.parse(value) }
@Suppress("UNCHECKED_CAST")
internal fun configObject(vararg entries: Pair<String, Any?>): 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<String, Any?>).root())
} else {
entry
}
}
return configuration.root()
}
internal operator fun Config.plus(entry: Pair<String, Any?>): 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<TARGET> = Validated<TARGET, Configuration.Validation.Error>
internal fun <TYPE> valid(target: TYPE) = Validated.valid<TYPE, Configuration.Validation.Error>(target)

View File

@ -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<Int?> {
return spec.parse(configuration)
}
private class Spec(versionKey: String, versionDefaultValue: Int?) : Configuration.Specification<Int?>("Version") {
private val version by int(key = versionKey).optional(versionDefaultValue)
override fun parseValid(configuration: Config) = valid(version.valueIn(configuration))
}
}

View File

@ -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<VALUE> private constructor(private val versionFromConfig: (Config) -> Valid<Int>, private val specifications: Map<Int, Configuration.Specification<VALUE>>) : (Config) -> Valid<Configuration.Specification<VALUE>> {
companion object {
fun <V> mapping(versionParser: Configuration.Value.Parser<Int>, specifications: Map<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications)
fun <V> mapping(versionParser: Configuration.Value.Parser<Int>, vararg specifications: Pair<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry({ config -> versionParser.parse(config) }, specifications.toMap())
fun <V> mapping(versionParser: (Config) -> Valid<Int>, specifications: Map<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry(versionParser, specifications)
fun <V> mapping(versionParser: (Config) -> Valid<Int>, vararg specifications: Pair<Int, Configuration.Specification<V>>) = VersionedSpecificationRegistry(versionParser, specifications.toMap())
}
override fun invoke(configuration: Config): Valid<Configuration.Specification<VALUE>> {
return versionFromConfig.invoke(configuration).mapValid { version ->
val value = specifications[version]
value?.let { valid(it) } ?: invalid<Configuration.Specification<VALUE>, Configuration.Validation.Error>(Configuration.Validation.Error.UnsupportedVersion.of(version))
}
}
}

View File

@ -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 <ERROR> validFromRawValue(rawValue: String, mapError: (String) -> ERROR): Validated<Address, ERROR> {
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 \"<host(String)>:<port:(Int)>\"").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))
}
}
}

View File

@ -0,0 +1,3 @@
package net.corda.common.configuration.parsing.internal
data class Addresses(val principal: Address, val admin: Address)

View File

@ -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)
}
}

View File

@ -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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Config, Configuration.Validation.Error, Configuration.Validation.Options> = 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<Address> {
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\""))
}
}
}

View File

@ -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)
}
}

View File

@ -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>("RpcSettings") {
private object AddressesSpec : Configuration.Specification<Addresses>("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<Address> {
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<RpcSettings>(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<List<String>?>("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
}

View File

@ -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<Int?> = extractVersion(null)
internal fun extractVersion(value: Int?) = extractValidValue(value)
internal fun extractPresentVersion(value: Int) = extractValidValue(value)
internal fun <VALUE> extractValidValue(value: VALUE) = extractValue(Validated.valid(value))
internal fun <VALUE> extractValueWithErrors(errors: Set<Configuration.Validation.Error>) = extractValue<VALUE>(Validated.invalid(errors))
internal fun <VALUE> extractValue(value: Valid<VALUE>) = object : Configuration.Value.Parser<VALUE> {
override fun parse(configuration: Config, options: Configuration.Validation.Options): Valid<VALUE> = value
}

View File

@ -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)
}
}

View File

@ -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<Int?> = { 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)
}
}

View File

@ -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<Int> = { 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<Int> = { 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<RpcSettings>, 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<Address> {
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>("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<RpcSettings> {
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>("RpcSettings", prefix = "configuration.value") {
private object AddressesSpec : Configuration.Specification<Addresses>("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<Address> {
return Address.validFromRawValue(rawValue) { error -> Configuration.Validation.Error.BadValue.of(error) }
}
}
private val addresses by nested(AddressesSpec)
override fun parseValid(configuration: Config): Valid<RpcSettings> {
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.") }

View File

@ -0,0 +1,3 @@
# common-validation
This module provides basic types to facilitate multi-step validation of typed values.

View File

@ -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
}

View File

@ -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<TARGET, ERROR> {
/**
* 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<ERROR>
/**
* 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<ERROR>) -> 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 <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR>
/**
* 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 <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR>
/**
* Applies the [convertError] function to the errors set, if not empty. Otherwise, returns a [Validated] wrapper with a [MAPPED_ERROR] generic type.
*/
fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR>
companion object {
/**
* Constructs a [Validated] wrapper with given valid [target] value and no errors.
*/
fun <T, E> valid(target: T): Validated.Result<T, E> = Validated.Result.Successful(target)
/**
* Constructs an invalid [Validated] wrapper with given errors and no value.
*/
fun <T, E> invalid(errors: Set<E>): Validated.Result<T, E> = Validated.Result.Unsuccessful(errors)
/**
* @see invalid
*/
fun <T, E> invalid(vararg errors: E): Validated.Result<T, E> = 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 <T, E> withResult(target: T, errors: Set<E>): Validated<T, E> = 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<TARGET, ERROR> : Validated<TARGET, ERROR> {
/**
* A successful validation result, containing a valid [TARGET] value and no [ERROR]s.
*/
class Successful<TARGET, ERROR>(override val value: TARGET) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
override val errors: Set<ERROR> = emptySet<ERROR>()
override fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = value
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
return valid(convert.invoke(value))
}
override fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR> {
return convert.invoke(value)
}
override fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR> {
return valid(value)
}
}
/**
* An unsuccessful validation result, containing [ERROR]s and no valid [TARGET] value.
*/
class Unsuccessful<TARGET, ERROR>(override val errors: Set<ERROR>) : Result<TARGET, ERROR>(), Validated<TARGET, ERROR> {
init {
require(errors.isNotEmpty())
}
override val value: TARGET get() = throw IllegalStateException("Invalid state.")
override fun valueOrThrow(exceptionOnErrors: (Set<ERROR>) -> Exception) = throw exceptionOnErrors.invoke(errors)
override fun <MAPPED> map(convert: (TARGET) -> MAPPED): Validated<MAPPED, ERROR> {
return invalid(errors)
}
override fun <MAPPED> mapValid(convert: (TARGET) -> Validated<MAPPED, ERROR>): Validated<MAPPED, ERROR> {
return invalid(errors)
}
override fun <MAPPED_ERROR> mapErrors(convertError: (ERROR) -> MAPPED_ERROR): Validated<TARGET, MAPPED_ERROR> {
return invalid(errors.asSequence().map(convertError).toSet())
}
}
}
}

View File

@ -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<TARGET : Any, ERROR : Any, OPTIONS> {
/**
* 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<TARGET, ERROR>
}

View File

@ -53,6 +53,12 @@ operator fun <T : Any> 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 <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, 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<CustomConfigParser>()?.let { return uncheckedCast(it.parser.createInstance().parse(this)) }

View File

@ -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'
}