mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
[CORDA-1778, CORDA-1835]: Decoupled configuration parsing mechanism (#4093)
This commit is contained in:
parent
01799cfc2d
commit
28dd3ac873
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@ -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
9
common/README.md
Normal 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.
|
23
common/configuration-parsing/README.md
Normal file
23
common/configuration-parsing/README.md
Normal 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
|
25
common/configuration-parsing/build.gradle
Normal file
25
common/configuration-parsing/build.gradle
Normal 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
|
||||
}
|
@ -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>
|
||||
}
|
@ -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) }
|
@ -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()
|
||||
}
|
||||
}
|
@ -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) })
|
||||
}
|
@ -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)
|
@ -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))
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package net.corda.common.configuration.parsing.internal
|
||||
|
||||
data class Addresses(val principal: Address, val admin: Address)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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\""))
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.") }
|
3
common/validation/README.md
Normal file
3
common/validation/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# common-validation
|
||||
|
||||
This module provides basic types to facilitate multi-step validation of typed values.
|
19
common/validation/build.gradle
Normal file
19
common/validation/build.gradle
Normal 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
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -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)) }
|
||||
|
@ -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'
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user