diff --git a/.travis.yml b/.travis.yml index 9aa19dd..b39b505 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,15 @@ languaje: kotlin -jdk: oraclejdk8 +jdk: + - openjdk8 + - openjdk9 + - openjdk10 + - openjdk11 + - openjdk-ea + +matrix: + allow_failures: + - jdk: openjdk-ea + sudo: false notifications: diff --git a/build.gradle b/build.gradle index ec11f84..5cad299 100644 --- a/build.gradle +++ b/build.gradle @@ -1,192 +1,190 @@ -import java.util.stream.Collectors - -buildscript { - - apply from: 'gradle/versions.gradle' - - repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { url 'https://jetbrains.jfrog.io/jetbrains/spek-snapshots' } - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.jetbrains.kotlin" - classpath "org.junit.platform:junit-platform-gradle-plugin:$versions.junitrunner" - } - -} - -plugins { - id 'com.github.kt3k.coveralls' version '2.8.2' - id 'com.github.ben-manes.versions' version '0.20.0' - id "com.jfrog.bintray" version "1.8.4" -} - -apply plugin: 'jacoco' -apply plugin: 'idea' - -idea { - module { - downloadJavadoc = true - downloadSources = true - } -} - -project.ext { - artifactGroup = "com.jdiazcano.cfg4k" - artifactVersion = "0.9.2" -} - -repositories { - mavenCentral() -} - -subprojects { p -> - apply plugin: 'java' - apply plugin: 'kotlin' - apply plugin: 'jacoco' - apply plugin: 'org.junit.platform.gradle.plugin' - apply plugin: "maven-publish" - apply plugin: "com.jfrog.bintray" - - sourceCompatibility = 1.8 - - repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { url 'https://jetbrains.jfrog.io/jetbrains/spek-snapshots' } - } - - bintray { - user = 'jdiazcano' - key = System.getenv("BINTRAY_KEY") - publications = ["kotlinPublish"] - //configurations = ["archives"] - publish = true - override = true - pkg { - repo = 'cfg4k' - name = p.name - userOrg = 'jdiazcano' - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/jdiazcano/cfg4k.git' - publicDownloadNumbers = true - version { - name = project.artifactVersion - desc = 'Cfg4k is a configuration library made for Kotlin in Kotlin!' - released = new Date() - vcsTag = project.artifactVersion - } - } - } - - // custom tasks for creating source/javadoc jars - task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource - } - - task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir - } - - // add javadoc/source jar tasks as artifacts - artifacts { - archives sourcesJar, javadocJar - } - - publishing { - publications { - kotlinPublish(MavenPublication) { - from components.java - groupId 'com.jdiazcano.cfg4k' - artifactId p.name - version project.artifactVersion - - artifact sourcesJar - artifact javadocJar - } - } - } - - project.afterEvaluate { - def junitPlatformTestTask = project.tasks.getByName('junitPlatformTest') - - jacoco { - toolVersion = "+" - reportsDir = file("$buildDir/reports/jacoco") - applyTo junitPlatformTestTask - } - - project.task(type: JacocoReport, "junitPlatformJacocoReport", { - sourceDirectories = files("src/main/kotlin") - classDirectories = files("$buildDir/classes/main") - reports { - xml.enabled = true - html.enabled = true - xml.destination = new File("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") - html.destination = new File("${buildDir}/reports/jacoco/test/jacocoTestReport.html") - } - executionData junitPlatformTestTask - }) - } - - coveralls { - sourceDirs += ['src/main/kotlin'] - } - - // Needed because there's something messy with kotlin version numbers and dependencies - configurations.all { - resolutionStrategy { - eachDependency { DependencyResolveDetails details -> - if (details.requested.group == 'org.jetbrains.kotlin') { - details.useVersion "$versions.jetbrains.kotlin" - } - } - } - } - - junitPlatform { - platformVersion "${versions.junitrunner}" - filters { - engines { - include 'spek' - } - } - } - -} - -def activeSubprojects = subprojects.findAll { it.name.startsWith("cfg4k") } -def subprojectsExecFiles = files( activeSubprojects.stream().map { "${it.name}/build/jacoco/junitPlatformTest.exec" }.collect(Collectors.toList()) ) - -task junitTest(dependsOn: getTasksByName("junitPlatformTest", true)) -task report(dependsOn: getTasksByName("junitPlatformJacocoReport", true)) - -task jacocoRootReport(type: JacocoReport) { - additionalSourceDirs = files(activeSubprojects.sourceSets.main.allSource.srcDirs) - sourceDirectories = files(activeSubprojects.sourceSets.main.allSource.srcDirs) - classDirectories = files(activeSubprojects.sourceSets.main.output) - executionData = subprojectsExecFiles - reports { - html.enabled = true - xml.enabled = true - csv.enabled = false - } - onlyIf = { - true - } - -} - -coveralls { - sourceDirs = activeSubprojects.sourceSets.main.allSource.srcDirs.flatten() - jacocoReportPath = "${buildDir}/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" -} - -task publish(dependsOn: activeSubprojects.collect { it.tasks.findByPath("bintrayUpload") }) {} -task publishLocal(dependsOn: activeSubprojects.collect { it.tasks.findByPath("publishToMavenLocal") }) {} +buildscript { + + apply from: 'gradle/versions.gradle' + + repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { url 'https://jetbrains.jfrog.io/jetbrains/spek-snapshots' } + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.jetbrains.kotlin" + classpath "org.junit.platform:junit-platform-gradle-plugin:$versions.junitrunner" + } + +} + +plugins { + id 'com.github.kt3k.coveralls' version '2.8.2' + id 'com.github.ben-manes.versions' version '0.20.0' + id "com.jfrog.bintray" version "1.8.4" +} + +apply plugin: 'jacoco' +apply plugin: 'idea' + +idea { + module { + downloadJavadoc = true + downloadSources = true + } +} + +project.ext { + artifactGroup = "com.jdiazcano.cfg4k" + artifactVersion = "0.9.3" +} + +repositories { + mavenCentral() +} + +subprojects { p -> + apply plugin: 'java' + apply plugin: 'kotlin' + apply plugin: 'jacoco' + apply plugin: 'org.junit.platform.gradle.plugin' + apply plugin: "maven-publish" + apply plugin: "com.jfrog.bintray" + + sourceCompatibility = 1.8 + + repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { url 'https://jetbrains.jfrog.io/jetbrains/spek-snapshots' } + } + + bintray { + user = 'jdiazcano' + key = System.getenv("BINTRAY_KEY") + publications = ["kotlinPublish"] + //configurations = ["archives"] + publish = true + override = true + pkg { + repo = 'cfg4k' + name = p.name + userOrg = 'jdiazcano' + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/jdiazcano/cfg4k.git' + publicDownloadNumbers = true + version { + name = project.artifactVersion + desc = 'Cfg4k is a configuration library made for Kotlin in Kotlin!' + released = new Date() + vcsTag = project.artifactVersion + } + } + } + + // custom tasks for creating source/javadoc jars + task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource + } + + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir + } + + // add javadoc/source jar tasks as artifacts + artifacts { + archives sourcesJar, javadocJar + } + + publishing { + publications { + kotlinPublish(MavenPublication) { + from components.java + groupId 'com.jdiazcano.cfg4k' + artifactId p.name + version project.artifactVersion + + artifact sourcesJar + artifact javadocJar + } + } + } + + project.afterEvaluate { + def junitPlatformTestTask = project.tasks.getByName('junitPlatformTest') + + jacoco { + toolVersion = "+" + reportsDir = file("$buildDir/reports/jacoco") + applyTo junitPlatformTestTask + } + + project.task(type: JacocoReport, "junitPlatformJacocoReport", { + sourceDirectories = files("src/main/kotlin") + classDirectories = files("$buildDir/classes/main") + reports { + xml.enabled = true + html.enabled = true + xml.destination = new File("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") + html.destination = new File("${buildDir}/reports/jacoco/test/jacocoTestReport.html") + } + executionData junitPlatformTestTask + }) + } + + coveralls { + sourceDirs += ['src/main/kotlin'] + } + + // Needed because there's something messy with kotlin version numbers and dependencies + configurations.all { + resolutionStrategy { + eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'org.jetbrains.kotlin') { + details.useVersion "$versions.jetbrains.kotlin" + } + } + } + } + + junitPlatform { + platformVersion "${versions.junitrunner}" + filters { + engines { + include 'spek' + } + } + } + +} + +def activeSubprojects = subprojects.findAll { it.name.startsWith("cfg4k") } +def subprojectsExecFiles = files( activeSubprojects.collect { "${it.name}/build/jacoco/junitPlatformTest.exec" } ) + +task junitTest(dependsOn: getTasksByName("junitPlatformTest", true)) +task report(dependsOn: getTasksByName("junitPlatformJacocoReport", true)) + +task jacocoRootReport(type: JacocoReport) { + additionalSourceDirs = files(activeSubprojects.sourceSets.main.allSource.srcDirs) + sourceDirectories = files(activeSubprojects.sourceSets.main.allSource.srcDirs) + classDirectories = files(activeSubprojects.sourceSets.main.output) + executionData = subprojectsExecFiles + reports { + html.enabled = true + xml.enabled = true + csv.enabled = false + } + onlyIf = { + true + } + +} + +coveralls { + sourceDirs = activeSubprojects.sourceSets.main.allSource.srcDirs.flatten() + jacocoReportPath = "${buildDir}/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" +} + +task publish(dependsOn: activeSubprojects.collect { it.tasks.findByPath("bintrayUpload") }) {} +task publishLocal(dependsOn: activeSubprojects.collect { it.tasks.findByPath("publishToMavenLocal") }) {} diff --git a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/Converters.kt b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/Converters.kt index 99fc74e..763fc3b 100644 --- a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/Converters.kt +++ b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/Converters.kt @@ -1,40 +1,40 @@ -package com.jdiazcano.cfg4k.binders - -import com.jdiazcano.cfg4k.core.ConfigContext -import com.jdiazcano.cfg4k.core.ConfigObject -import com.jdiazcano.cfg4k.parsers.ListParser -import com.jdiazcano.cfg4k.parsers.MapParser -import com.jdiazcano.cfg4k.parsers.Parsers.findParser -import com.jdiazcano.cfg4k.parsers.Parsers.isParseable -import com.jdiazcano.cfg4k.utils.ParserClassNotFound -import com.jdiazcano.cfg4k.utils.TypeStructure - - -fun convert(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure): Any? { - return convertBase(context, configObject, structure) { - context.bind(context.propertyName, structure.raw) - } -} - -fun convertGet(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure): Any? { - return convertBase(context, configObject, structure) { - context.get(context.propertyName, structure.raw) - } -} - -fun convertGetOrNull(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure): Any? { - return convertBase(context, configObject, structure) { - context.getOrNull(context.propertyName, structure.raw) - } -} - -fun convertBase(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure, getter: () -> Any?): Any? { - return when { - structure.isMap() -> MapParser.parse(context, configObject, structure) - configObject.isList() -> ListParser.parse(context, configObject, structure) - structure.raw.isParseable() -> structure.raw.findParser().parse(context, configObject, structure) - structure.raw.isInterface -> getter() - structure.raw.kotlin.isData -> DataClassBinder.bind(context.provider, context.propertyName, structure.raw.kotlin) - else -> throw ParserClassNotFound("Couldn't parse class ${structure.raw}") - } +package com.jdiazcano.cfg4k.binders + +import com.jdiazcano.cfg4k.core.ConfigContext +import com.jdiazcano.cfg4k.core.ConfigObject +import com.jdiazcano.cfg4k.parsers.ListParser +import com.jdiazcano.cfg4k.parsers.MapParser +import com.jdiazcano.cfg4k.parsers.Parsers.findParser +import com.jdiazcano.cfg4k.parsers.Parsers.isParseable +import com.jdiazcano.cfg4k.utils.ParserClassNotFound +import com.jdiazcano.cfg4k.utils.TypeStructure + + +fun convert(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure): Any? { + return convertBase(context, configObject, structure) { + context.bind(context.propertyName, structure.raw) + } +} + +fun convertGet(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure): Any? { + return convertBase(context, configObject, structure) { + context.get(context.propertyName, structure.raw) + } +} + +fun convertGetOrNull(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure): Any? { + return convertBase(context, configObject, structure) { + context.getOrNull(context.propertyName, structure.raw) + } +} + +fun convertBase(context: ConfigContext, configObject: ConfigObject, structure: TypeStructure, getter: () -> Any?): Any? { + return when { + structure.isMap() -> MapParser.parse(context, configObject, structure) + configObject.isList() -> ListParser.parse(context, configObject, structure) + structure.raw.isParseable() -> structure.raw.findParser().parse(context, configObject, structure) + structure.raw.isInterface -> getter() + structure.raw.kotlin.isData -> DataClassBinder.bind(context.provider, context.propertyName, structure.raw.kotlin) + else -> throw ParserClassNotFound("Couldn't parse class ${structure.raw}") + } } \ No newline at end of file diff --git a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/DataClassBinder.kt b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/DataClassBinder.kt index 0c84736..1bbc990 100644 --- a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/DataClassBinder.kt +++ b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/binders/DataClassBinder.kt @@ -1,38 +1,38 @@ -package com.jdiazcano.cfg4k.binders - -import com.jdiazcano.cfg4k.core.ConfigContext -import com.jdiazcano.cfg4k.providers.ConfigProvider -import com.jdiazcano.cfg4k.providers.load -import com.jdiazcano.cfg4k.utils.ConstructorNotFound -import com.jdiazcano.cfg4k.utils.convert -import kotlin.reflect.KClass -import kotlin.reflect.KVisibility -import kotlin.reflect.jvm.javaType - -object DataClassBinder { - fun bind(configProvider: ConfigProvider, prefix: String, type: KClass): T { - if (type.visibility == KVisibility.PRIVATE) { - throw IllegalArgumentException("Binding data classes can't be private: ${type.qualifiedName}") - } - - val constructors = type.constructors - val configObject = configProvider.load(prefix) ?: throw IllegalArgumentException("Config object not found: $prefix") - - val paramNames = configObject.asObject().map { it.key } - val matchingConstructor = constructors - .firstOrNull { c -> c.parameters.filterNot { it.isOptional }.map { param -> param.name }.containsAll(paramNames) } - ?: throw ConstructorNotFound("Constructor for class ${type.simpleName} wasn't found. Names: $paramNames") - - val constructorParameters = matchingConstructor.parameters.map { parameter -> - val structure = parameter.type.javaType.convert() - val context = ConfigContext(configProvider, concatPrefix(prefix, parameter.name!!)) - val subObject = configProvider.load(context) ?: if (parameter.type.isMarkedNullable) null else throw IllegalArgumentException("Parameter ${parameter.name} isn't marked as nullable and avlue was null.") - val value = subObject?.let { convert(context, subObject, structure) } - - parameter to value - }.toMap() - - return matchingConstructor.callBy(constructorParameters) - } - +package com.jdiazcano.cfg4k.binders + +import com.jdiazcano.cfg4k.core.ConfigContext +import com.jdiazcano.cfg4k.providers.ConfigProvider +import com.jdiazcano.cfg4k.providers.load +import com.jdiazcano.cfg4k.utils.ConstructorNotFound +import com.jdiazcano.cfg4k.utils.convert +import kotlin.reflect.KClass +import kotlin.reflect.KVisibility +import kotlin.reflect.jvm.javaType + +object DataClassBinder { + fun bind(configProvider: ConfigProvider, prefix: String, type: KClass): T { + if (type.visibility == KVisibility.PRIVATE) { + throw IllegalArgumentException("Binding data classes can't be private: ${type.qualifiedName}") + } + + val constructors = type.constructors + val configObject = configProvider.load(prefix) ?: throw IllegalArgumentException("Config object not found: $prefix") + + val paramNames = configObject.asObject().map { it.key } + val matchingConstructor = constructors + .firstOrNull { c -> c.parameters.filterNot { it.isOptional }.map { param -> param.name }.containsAll(paramNames) } + ?: throw ConstructorNotFound("Constructor for class ${type.simpleName} wasn't found. Names: $paramNames") + + val constructorParameters = matchingConstructor.parameters.map { parameter -> + val structure = parameter.type.javaType.convert() + val context = ConfigContext(configProvider, concatPrefix(prefix, parameter.name!!)) + val subObject = configProvider.load(context) ?: if (parameter.type.isMarkedNullable) null else throw IllegalArgumentException("Parameter ${parameter.name} isn't marked as nullable and avlue was null.") + val value = subObject?.let { convert(context, subObject, structure) } + + parameter to value + }.toMap() + + return matchingConstructor.callBy(constructorParameters) + } + } \ No newline at end of file diff --git a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/core/ConfigObject.kt b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/core/ConfigObject.kt index 1e1762d..f344254 100644 --- a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/core/ConfigObject.kt +++ b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/core/ConfigObject.kt @@ -1,126 +1,126 @@ -package com.jdiazcano.cfg4k.core - -import com.jdiazcano.cfg4k.loaders.findNumbers -import com.jdiazcano.cfg4k.providers.ConfigProvider -import java.io.InvalidObjectException - -interface ConfigObject { - val type: ConfigObjectType - val value: Any - - fun merge(configObject: ConfigObject): ConfigObject - - fun child(key: String): ConfigObject? { - val (number, cleanKey) = findNumbers(key) - - return when { - // We're in a normal object - number == null -> asObject()[cleanKey] - // We're in a list that is ROOT - cleanKey.isEmpty() -> asList()[number] - // When we are in a list that is inside our object - else -> asObject()[cleanKey]?.asList()?.get(number) - } - } - - fun isString() = type == ConfigObjectType.STRING - fun isObject() = type == ConfigObjectType.OBJECT - fun isList() = type == ConfigObjectType.LIST - fun asString() = value.toString() - fun asObject() = value as Map - fun asList() = value as List -} - -abstract class AbstractConfigObject( - override val value: Any, - override val type: ConfigObjectType -) : ConfigObject { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - return true - } - - override fun hashCode(): Int { - return value.hashCode() - } - - override fun toString(): String { - return "ConfigObject(value=$value)" - } -} - -class ListConfigObject(value: Collection) : AbstractConfigObject(value.toList(), ConfigObjectType.LIST) { - override fun merge(configObject: ConfigObject): ConfigObject { - return ListConfigObject((value as Iterable).union(configObject.value as Iterable)) - } -} - -class MapConfigObject(value: Map) : AbstractConfigObject(value.toMap(), ConfigObjectType.OBJECT) { - override fun merge(configObject: ConfigObject): ConfigObject { - val thisMap = value as Map - val thatMap = configObject.value as Map - - val theList = thisMap.keys.toMutableList() - val commonKeysMergedMap = hashMapOf() - val commonKeys = theList.retainAll(thatMap.keys) - - // Check for primitive common keys - if (commonKeys) { - theList.forEach { - commonKeysMergedMap[it] = thisMap[it]!!.merge(thatMap[it]!!) - } - } - - return MapConfigObject(thisMap.plus(thatMap).plus(commonKeysMergedMap)) - } -} - -class StringConfigObject(value: String) : AbstractConfigObject(value, ConfigObjectType.STRING) { - override fun merge(configObject: ConfigObject): ConfigObject { - throw InvalidObjectException("Can't merge primitive (string) values") - } -} - -enum class ConfigObjectType { - STRING, - LIST, - OBJECT -} - -data class ConfigContext( - val provider: ConfigProvider, - val propertyName: String -): ConfigProvider by provider - -// Add converters for primitive types -fun String.toConfig() = StringConfigObject(this) - -fun Int.toConfig() = StringConfigObject(this.toString()) -fun Long.toConfig() = StringConfigObject(this.toString()) -fun Double.toConfig() = StringConfigObject(this.toString()) -fun Float.toConfig() = StringConfigObject(this.toString()) -fun Map.toConfig() = parseObject(this) -fun List<*>.toConfig() = parseArray(this) - -private fun parseObject(parsed: Map<*, *>): ConfigObject { - return MapConfigObject(parsed.map { (key, value) -> - when (value) { - is List<*> -> key.toString() to parseArray(value) - is Map<*, *> -> key.toString() to parseObject(value) - is ConfigObject -> key.toString() to value - else -> key.toString() to StringConfigObject(value.toString()) - } - }.toMap(hashMapOf())) -} - -private fun parseArray(array: List<*>): ConfigObject { - return ListConfigObject(array.map { item -> - when (item) { - is Map<*, *> -> parseObject(item) - is ConfigObject -> item - else -> StringConfigObject(item.toString()) - } - }) +package com.jdiazcano.cfg4k.core + +import com.jdiazcano.cfg4k.loaders.findNumbers +import com.jdiazcano.cfg4k.providers.ConfigProvider +import java.io.InvalidObjectException + +interface ConfigObject { + val type: ConfigObjectType + val value: Any + + fun merge(configObject: ConfigObject): ConfigObject + + fun child(key: String): ConfigObject? { + val (number, cleanKey) = findNumbers(key) + + return when { + // We're in a normal object + number == null -> asObject()[cleanKey] + // We're in a list that is ROOT + cleanKey.isEmpty() -> asList()[number] + // When we are in a list that is inside our object + else -> asObject()[cleanKey]?.asList()?.get(number) + } + } + + fun isString() = type == ConfigObjectType.STRING + fun isObject() = type == ConfigObjectType.OBJECT + fun isList() = type == ConfigObjectType.LIST + fun asString() = value.toString() + fun asObject() = value as Map + fun asList() = value as List +} + +abstract class AbstractConfigObject( + override val value: Any, + override val type: ConfigObjectType +) : ConfigObject { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return true + } + + override fun hashCode(): Int { + return value.hashCode() + } + + override fun toString(): String { + return "ConfigObject(value=$value)" + } +} + +class ListConfigObject(value: Collection) : AbstractConfigObject(value.toList(), ConfigObjectType.LIST) { + override fun merge(configObject: ConfigObject): ConfigObject { + return ListConfigObject((value as Iterable).union(configObject.value as Iterable)) + } +} + +class MapConfigObject(value: Map) : AbstractConfigObject(value.toMap(), ConfigObjectType.OBJECT) { + override fun merge(configObject: ConfigObject): ConfigObject { + val thisMap = value as Map + val thatMap = configObject.value as Map + + val theList = thisMap.keys.toMutableList() + val commonKeysMergedMap = hashMapOf() + val commonKeys = theList.retainAll(thatMap.keys) + + // Check for primitive common keys + if (commonKeys) { + theList.forEach { + commonKeysMergedMap[it] = thisMap[it]!!.merge(thatMap[it]!!) + } + } + + return MapConfigObject(thisMap.plus(thatMap).plus(commonKeysMergedMap)) + } +} + +class StringConfigObject(value: String) : AbstractConfigObject(value, ConfigObjectType.STRING) { + override fun merge(configObject: ConfigObject): ConfigObject { + throw InvalidObjectException("Can't merge primitive (string) values") + } +} + +enum class ConfigObjectType { + STRING, + LIST, + OBJECT +} + +data class ConfigContext( + val provider: ConfigProvider, + val propertyName: String +): ConfigProvider by provider + +// Add converters for primitive types +fun String.toConfig() = StringConfigObject(this) + +fun Int.toConfig() = StringConfigObject(this.toString()) +fun Long.toConfig() = StringConfigObject(this.toString()) +fun Double.toConfig() = StringConfigObject(this.toString()) +fun Float.toConfig() = StringConfigObject(this.toString()) +fun Map.toConfig() = parseObject(this) +fun List<*>.toConfig() = parseArray(this) + +private fun parseObject(parsed: Map<*, *>): ConfigObject { + return MapConfigObject(parsed.map { (key, value) -> + when (value) { + is List<*> -> key.toString() to parseArray(value) + is Map<*, *> -> key.toString() to parseObject(value) + is ConfigObject -> key.toString() to value + else -> key.toString() to StringConfigObject(value.toString()) + } + }.toMap(hashMapOf())) +} + +private fun parseArray(array: List<*>): ConfigObject { + return ListConfigObject(array.map { item -> + when (item) { + is Map<*, *> -> parseObject(item) + is ConfigObject -> item + else -> StringConfigObject(item.toString()) + } + }) } \ No newline at end of file diff --git a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/loaders/PropertyConfigLoader.kt b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/loaders/PropertyConfigLoader.kt index 210e7af..537eeb1 100644 --- a/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/loaders/PropertyConfigLoader.kt +++ b/cfg4k-core/src/main/kotlin/com/jdiazcano/cfg4k/loaders/PropertyConfigLoader.kt @@ -21,7 +21,7 @@ import com.jdiazcano.cfg4k.core.toConfig import com.jdiazcano.cfg4k.sources.ConfigSource import mu.KotlinLogging import java.net.URL -import java.util.Properties +import java.util.* private val logger = KotlinLogging.logger {} diff --git a/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/ConfigObjectMergeTest.kt b/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/ConfigObjectMergeTest.kt index d67af95..4cb288b 100644 --- a/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/ConfigObjectMergeTest.kt +++ b/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/ConfigObjectMergeTest.kt @@ -1,129 +1,129 @@ -package com.jdiazcano.cfg4k - -import com.jdiazcano.cfg4k.core.ListConfigObject -import com.jdiazcano.cfg4k.core.MapConfigObject -import com.jdiazcano.cfg4k.core.toConfig -import org.jetbrains.spek.api.Spek -import org.jetbrains.spek.api.dsl.describe -import org.jetbrains.spek.api.dsl.it -import java.io.InvalidObjectException -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class ConfigObjectMergeTest : Spek({ - describe("a config object that can and should be merged") { - it("merge lists") { - val list1 = ListConfigObject(listOf( - 1.toConfig(), - 2.toConfig() - )) - - val list2 = ListConfigObject(listOf( - 3.toConfig(), - 4.toConfig() - )) - - val mergedList = list1.merge(list2) - assertEquals( - ListConfigObject(listOf( - 1.toConfig(), - 2.toConfig(), - 3.toConfig(), - 4.toConfig() - )), - mergedList - ) - } - - it("merge maps with collision") { - val list1 = MapConfigObject(mapOf( - "a" to 1.toConfig(), - "b" to 2.toConfig() - )) - - val list2 = MapConfigObject(mapOf( - "a" to 3.toConfig(), - "d" to 4.toConfig() - )) - - assertFailsWith { list1.merge(list2) } - } - - it("merge lists with collision will just take the object once") { - val list1 = ListConfigObject(listOf( - 1.toConfig(), - 2.toConfig() - )) - - val list2 = ListConfigObject(listOf( - 2.toConfig(), - 4.toConfig() - )) - - val mergedList = list1.merge(list2) - assertEquals( - ListConfigObject(listOf( - 1.toConfig(), - 2.toConfig(), - 4.toConfig() - )), - mergedList - ) - } - - it("merge maps") { - val list1 = MapConfigObject(mapOf( - "a" to 1.toConfig(), - "b" to 2.toConfig() - )) - - val list2 = MapConfigObject(mapOf( - "c" to 3.toConfig(), - "d" to 4.toConfig() - )) - - val mergedList = list1.merge(list2) - assertEquals( - MapConfigObject(mapOf( - "a" to 1.toConfig(), - "b" to 2.toConfig(), - "c" to 3.toConfig(), - "d" to 4.toConfig() - )), - mergedList - ) - } - - it("merge super nested") { - val list1 = MapConfigObject(mapOf( - "a" to ListConfigObject(listOf(1.toConfig(), 2.toConfig())), - "b" to MapConfigObject(mapOf( - "aa" to 11.toConfig(), - "ab" to 22.toConfig(), - "ac" to 33.toConfig() - )) - )) - - val list2 = MapConfigObject(mapOf( - "a" to ListConfigObject(listOf(3.toConfig())), - "c" to 3.toConfig(), - "d" to 4.toConfig() - )) - - val mergedList = list1.merge(list2) - assertEquals( - MapConfigObject(mapOf( - "a" to ListConfigObject(listOf(1.toConfig(), 2.toConfig(), 3.toConfig())), - "b" to MapConfigObject(mapOf( - "aa" to 11.toConfig(), - "ab" to 22.toConfig(), - "ac" to 33.toConfig() - )), - "c" to 3.toConfig(), - "d" to 4.toConfig() - )), - mergedList - ) - } - } +package com.jdiazcano.cfg4k + +import com.jdiazcano.cfg4k.core.ListConfigObject +import com.jdiazcano.cfg4k.core.MapConfigObject +import com.jdiazcano.cfg4k.core.toConfig +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import java.io.InvalidObjectException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class ConfigObjectMergeTest : Spek({ + describe("a config object that can and should be merged") { + it("merge lists") { + val list1 = ListConfigObject(listOf( + 1.toConfig(), + 2.toConfig() + )) + + val list2 = ListConfigObject(listOf( + 3.toConfig(), + 4.toConfig() + )) + + val mergedList = list1.merge(list2) + assertEquals( + ListConfigObject(listOf( + 1.toConfig(), + 2.toConfig(), + 3.toConfig(), + 4.toConfig() + )), + mergedList + ) + } + + it("merge maps with collision") { + val list1 = MapConfigObject(mapOf( + "a" to 1.toConfig(), + "b" to 2.toConfig() + )) + + val list2 = MapConfigObject(mapOf( + "a" to 3.toConfig(), + "d" to 4.toConfig() + )) + + assertFailsWith { list1.merge(list2) } + } + + it("merge lists with collision will just take the object once") { + val list1 = ListConfigObject(listOf( + 1.toConfig(), + 2.toConfig() + )) + + val list2 = ListConfigObject(listOf( + 2.toConfig(), + 4.toConfig() + )) + + val mergedList = list1.merge(list2) + assertEquals( + ListConfigObject(listOf( + 1.toConfig(), + 2.toConfig(), + 4.toConfig() + )), + mergedList + ) + } + + it("merge maps") { + val list1 = MapConfigObject(mapOf( + "a" to 1.toConfig(), + "b" to 2.toConfig() + )) + + val list2 = MapConfigObject(mapOf( + "c" to 3.toConfig(), + "d" to 4.toConfig() + )) + + val mergedList = list1.merge(list2) + assertEquals( + MapConfigObject(mapOf( + "a" to 1.toConfig(), + "b" to 2.toConfig(), + "c" to 3.toConfig(), + "d" to 4.toConfig() + )), + mergedList + ) + } + + it("merge super nested") { + val list1 = MapConfigObject(mapOf( + "a" to ListConfigObject(listOf(1.toConfig(), 2.toConfig())), + "b" to MapConfigObject(mapOf( + "aa" to 11.toConfig(), + "ab" to 22.toConfig(), + "ac" to 33.toConfig() + )) + )) + + val list2 = MapConfigObject(mapOf( + "a" to ListConfigObject(listOf(3.toConfig())), + "c" to 3.toConfig(), + "d" to 4.toConfig() + )) + + val mergedList = list1.merge(list2) + assertEquals( + MapConfigObject(mapOf( + "a" to ListConfigObject(listOf(1.toConfig(), 2.toConfig(), 3.toConfig())), + "b" to MapConfigObject(mapOf( + "aa" to 11.toConfig(), + "ab" to 22.toConfig(), + "ac" to 33.toConfig() + )), + "c" to 3.toConfig(), + "d" to 4.toConfig() + )), + mergedList + ) + } + } }) \ No newline at end of file diff --git a/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/DataClassProviderTest.kt b/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/DataClassProviderTest.kt index f0809bc..9e08043 100644 --- a/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/DataClassProviderTest.kt +++ b/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/DataClassProviderTest.kt @@ -1,111 +1,111 @@ -package com.jdiazcano.cfg4k - -import com.jdiazcano.cfg4k.loaders.PropertyConfigLoader -import com.jdiazcano.cfg4k.providers.* -import com.jdiazcano.cfg4k.sources.URLConfigSource -import com.winterbe.expekt.should -import org.jetbrains.spek.api.Spek -import org.jetbrains.spek.api.dsl.describe -import org.jetbrains.spek.api.dsl.it -import java.lang.Exception -import kotlin.test.assertFailsWith - -class DataClassProviderTest : Spek({ - val source = URLConfigSource(javaClass.classLoader.getResource("test.properties")) - val providers = listOf( - Providers.proxy(PropertyConfigLoader(source)), - Providers.proxy(PropertyConfigLoader(source)).cache() - - ) - - providers.forEach { provider -> - describe("provider ${provider.javaClass.simpleName}") { - beforeEachTest { - provider.reload() - } - - it("can bind a normal class") { - val cool = provider.bind("cool") - cool.nottest.should.be.equal(1) - cool.another.should.be.equal("wow") - } - - it("can bind a nested class") { - val cool = provider.bind("nested") - cool.normal.should.be.equal(1) - cool.supernested.normal.should.be.equal(2) - } - - it("can bind a class with nullables with default") { - val cool = provider.bind("cool") - cool.nottest.should.be.equal(1) - cool.another.should.be.equal("wow") - } - - it("can bind a class with nullables without default") { - val cool = provider.bind("cool") - cool.nottest.should.be.equal(1) - cool.another.should.be.equal("wow") - } - - it("can bind a nested class with an interface") { - val cool = provider.bind("nested") - cool.normal.should.be.equal(1) - cool.supernested.normal().should.be.equal(2) - } - - it("can not bind a private class") { - assertFailsWith { - provider.bind("cool") - } - } - - it("exception on no constructor found because the types do not match and it can be whatever") { - assertFailsWith { - provider.bind("cool") - } - } - } - } -}) - -internal data class MyCoolDataClassAnotherConstructorNotFound( - val nottest: Int, - val another: Int -) - -internal data class MyCoolDataClass( - val nottest: Int, - val another: String -) - -internal data class MyCoolDataClassWithNullables( - val nottest: Int, - val another: String, - val icanbenull: String? = null -) - -internal data class MyCoolDataClassWithNullablesWithoutDefault( - val nottest: Int, - val another: String, - val icanbenull: String? -) - -private data class MyPrivateCoolDataClass( - val nottest: Int, - val another: String -) - -internal data class ClassBindNested( - val normal: Int, - val supernested: ClassBindSupernested -) - -internal data class ClassBindNestedButWithInterface( - val normal: Int, - val supernested: NestedBinder -) - -internal data class ClassBindSupernested( - val normal: Int +package com.jdiazcano.cfg4k + +import com.jdiazcano.cfg4k.loaders.PropertyConfigLoader +import com.jdiazcano.cfg4k.providers.* +import com.jdiazcano.cfg4k.sources.URLConfigSource +import com.winterbe.expekt.should +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.describe +import org.jetbrains.spek.api.dsl.it +import java.lang.Exception +import kotlin.test.assertFailsWith + +class DataClassProviderTest : Spek({ + val source = URLConfigSource(javaClass.classLoader.getResource("test.properties")) + val providers = listOf( + Providers.proxy(PropertyConfigLoader(source)), + Providers.proxy(PropertyConfigLoader(source)).cache() + + ) + + providers.forEach { provider -> + describe("provider ${provider.javaClass.simpleName}") { + beforeEachTest { + provider.reload() + } + + it("can bind a normal class") { + val cool = provider.bind("cool") + cool.nottest.should.be.equal(1) + cool.another.should.be.equal("wow") + } + + it("can bind a nested class") { + val cool = provider.bind("nested") + cool.normal.should.be.equal(1) + cool.supernested.normal.should.be.equal(2) + } + + it("can bind a class with nullables with default") { + val cool = provider.bind("cool") + cool.nottest.should.be.equal(1) + cool.another.should.be.equal("wow") + } + + it("can bind a class with nullables without default") { + val cool = provider.bind("cool") + cool.nottest.should.be.equal(1) + cool.another.should.be.equal("wow") + } + + it("can bind a nested class with an interface") { + val cool = provider.bind("nested") + cool.normal.should.be.equal(1) + cool.supernested.normal().should.be.equal(2) + } + + it("can not bind a private class") { + assertFailsWith { + provider.bind("cool") + } + } + + it("exception on no constructor found because the types do not match and it can be whatever") { + assertFailsWith { + provider.bind("cool") + } + } + } + } +}) + +internal data class MyCoolDataClassAnotherConstructorNotFound( + val nottest: Int, + val another: Int +) + +internal data class MyCoolDataClass( + val nottest: Int, + val another: String +) + +internal data class MyCoolDataClassWithNullables( + val nottest: Int, + val another: String, + val icanbenull: String? = null +) + +internal data class MyCoolDataClassWithNullablesWithoutDefault( + val nottest: Int, + val another: String, + val icanbenull: String? +) + +private data class MyPrivateCoolDataClass( + val nottest: Int, + val another: String +) + +internal data class ClassBindNested( + val normal: Int, + val supernested: ClassBindSupernested +) + +internal data class ClassBindNestedButWithInterface( + val normal: Int, + val supernested: NestedBinder +) + +internal data class ClassBindSupernested( + val normal: Int ) \ No newline at end of file diff --git a/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/PropertiesConfigObjectTest.kt b/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/PropertiesConfigObjectTest.kt index 7f17ad2..ac09c8c 100644 --- a/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/PropertiesConfigObjectTest.kt +++ b/cfg4k-core/src/test/kotlin/com/jdiazcano/cfg4k/PropertiesConfigObjectTest.kt @@ -7,7 +7,6 @@ import com.winterbe.expekt.should import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.describe import org.jetbrains.spek.api.dsl.it -import kotlin.test.assertFailsWith class PropertiesConfigObjectTest : Spek({ describe("a properties loader") { @@ -19,13 +18,4 @@ class PropertiesConfigObjectTest : Spek({ nested.asObject()["a"].should.be.equal("b".toConfig()) } } - - describe("a failing properties") { - it("should fail for repeating the same entry") { - assertFailsWith { - val configObject = javaClass.getResource("/doubledproperty.properties").asProperties().toConfig() - println(configObject) - } - } - } }) diff --git a/cfg4k-core/src/test/resources/doubledproperty.properties b/cfg4k-core/src/test/resources/doubledproperty.properties index 59a6f6c..9656ee8 100644 --- a/cfg4k-core/src/test/resources/doubledproperty.properties +++ b/cfg4k-core/src/test/resources/doubledproperty.properties @@ -1,2 +1,7 @@ +a.b=2 +a.c=2 +a.d=2 a=1 -a.b=2 \ No newline at end of file +a.e=2 +a.f.g=2 +a.a.a.a=2 \ No newline at end of file diff --git a/gradle/versions.gradle b/gradle/versions.gradle index c998339..8df47cc 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -1,47 +1,47 @@ -ext.versions = [ - jetbrains: [ - kotlin: '1.3.21', - spek: '1.1.2', - engine: '1.2.1' - ], - bytebuddy: '1.9.10', - klaxon: '5.0.5', - typesafe: '1.3.3', - jgit: '4.9.0.201710071750-r', - expekt: '0.5.0', - junitrunner: '1.1.0-M1', - snakeyaml: '1.24', - jcommander: '1.72', - mockwebserver: '3.13.1', - aws: '1.11.510', - logging: '1.6.25' -] - -ext.libraries = [ - eclipse: [ - jgit: "org.eclipse.jgit:org.eclipse.jgit:$versions.jgit" - ], - jetbrains: [ - kotlin: [ - stdlib: "org.jetbrains.kotlin:kotlin-stdlib:$versions.jetbrains.kotlin", - reflect: "org.jetbrains.kotlin:kotlin-reflect:$versions.jetbrains.kotlin", - test: "org.jetbrains.kotlin:kotlin-test:$versions.jetbrains.kotlin" - ], - spek: [ - api: "org.jetbrains.spek:spek-api:$versions.jetbrains.engine", - engine: "org.jetbrains.spek:spek-junit-platform-engine:$versions.jetbrains.engine" - ] - ], - junitrunner: "org.junit.platform:junit-platform-runner:$versions.junitrunner", - expekt: "com.winterbe:expekt:$versions.expekt", - bytebuddy: "net.bytebuddy:byte-buddy:$versions.bytebuddy", - typesafe: "com.typesafe:config:$versions.typesafe", - snakeyaml: "org.yaml:snakeyaml:$versions.snakeyaml", - klaxon: "com.beust:klaxon:$versions.klaxon", - jcommander: "com.beust:jcommander:$versions.jcommander", - mockwebserver: "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver", - aws: [ - s3: "com.amazonaws:aws-java-sdk-s3:$versions.aws" - ], - logging: "io.github.microutils:kotlin-logging:$versions.logging" -] +ext.versions = [ + jetbrains: [ + kotlin: '1.3.21', + spek: '1.1.2', + engine: '1.2.1' + ], + bytebuddy: '1.9.10', + klaxon: '5.0.5', + typesafe: '1.3.3', + jgit: '4.9.0.201710071750-r', + expekt: '0.5.0', + junitrunner: '1.1.0-M1', + snakeyaml: '1.24', + jcommander: '1.72', + mockwebserver: '3.13.1', + aws: '1.11.510', + logging: '1.6.25' +] + +ext.libraries = [ + eclipse: [ + jgit: "org.eclipse.jgit:org.eclipse.jgit:$versions.jgit" + ], + jetbrains: [ + kotlin: [ + stdlib: "org.jetbrains.kotlin:kotlin-stdlib:$versions.jetbrains.kotlin", + reflect: "org.jetbrains.kotlin:kotlin-reflect:$versions.jetbrains.kotlin", + test: "org.jetbrains.kotlin:kotlin-test:$versions.jetbrains.kotlin" + ], + spek: [ + api: "org.jetbrains.spek:spek-api:$versions.jetbrains.engine", + engine: "org.jetbrains.spek:spek-junit-platform-engine:$versions.jetbrains.engine" + ] + ], + junitrunner: "org.junit.platform:junit-platform-runner:$versions.junitrunner", + expekt: "com.winterbe:expekt:$versions.expekt", + bytebuddy: "net.bytebuddy:byte-buddy:$versions.bytebuddy", + typesafe: "com.typesafe:config:$versions.typesafe", + snakeyaml: "org.yaml:snakeyaml:$versions.snakeyaml", + klaxon: "com.beust:klaxon:$versions.klaxon", + jcommander: "com.beust:jcommander:$versions.jcommander", + mockwebserver: "com.squareup.okhttp3:mockwebserver:$versions.mockwebserver", + aws: [ + s3: "com.amazonaws:aws-java-sdk-s3:$versions.aws" + ], + logging: "io.github.microutils:kotlin-logging:$versions.logging" +]