diff --git a/data4k/README.md b/data4k/README.md new file mode 100644 index 0000000..619b666 --- /dev/null +++ b/data4k/README.md @@ -0,0 +1,56 @@ +# Data4k + +Download +[![.github/workflows/build.yaml](https://github.com/fork-handles/forkhandles/actions/workflows/build.yaml/badge.svg)](https://github.com/fork-handles/forkhandles/actions/workflows/build.yaml) + +GitHub license +codebeat badge + +Library to make working with Data-Oriented programming in Kotlin easier, to extract values from dynamic data structures such as Maps. + +## Installation + +In Gradle, install the ForkHandles BOM and then this module in the dependency block: + +```kotlin +implementation(platform("dev.forkhandles:forkhandles-bom:X.Y.Z")) +implementation("dev.forkhandles:data4k") +``` + +## Usage + +The library defines a DataContainer class and implementations for: + +- Map +- Jackson JSON Node + +To extract data from the underlying data, define wrappers which provides access via delegated-properties: + +```kotlin +class MapBacked(propertySet: Map) : MapDataContainer(propertySet) { + val stringField by field() + val listSubClassField by list(::SubMap) + val objectField by obj(::SubMap) +} + +class SubMap(propertySet: Map) : MapDataContainer(propertySet) { + val stringField by field() +} + +val input = MapBacked( + mapOf( + "stringField" to "string", + "listSubClassField" to listOf( + mapOf("stringField" to "string1"), + mapOf("stringField" to "string2"), + ), + "listStringsField" to listOf("string1", "string2"), + "objectField" to mapOf( + "stringField" to "string" + ) + ) +) + +// then just get the values from the underlying data using the type system. Errors will be thrown for missing/invalid properties +val data: String = input.objectField.stringField +``` diff --git a/data4k/build.gradle b/data4k/build.gradle new file mode 100644 index 0000000..6655030 --- /dev/null +++ b/data4k/build.gradle @@ -0,0 +1,8 @@ +description = 'ForkHandles data-oriented programming library' + +dependencies { + api("org.jetbrains.kotlin:kotlin-reflect:_") + compileOnly("com.fasterxml.jackson.core:jackson-databind:_") + testImplementation("com.fasterxml.jackson.core:jackson-databind:_") + testImplementation("io.strikt:strikt-jvm:_") +} diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt new file mode 100644 index 0000000..b8dbe6a --- /dev/null +++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt @@ -0,0 +1,23 @@ +package dev.forkhandles.data + +/** + * Superclass for all container implementations. Defines the delegate property classes to extract data from the + * underlying data structure. + */ +@Suppress("UNCHECKED_CAST", "ClassName") +abstract class DataContainer( + protected val data: CONTENT, + existsFn: (CONTENT, String) -> Boolean, + getFn: (CONTENT, String) -> Any? +) { + private val exists: DataContainer.(String) -> Boolean = { existsFn(data, it) } + private val get: DataContainer.(String) -> Any? = { getFn(data, it) } + + inner class field : DataProperty, OUT>(exists, get) + + inner class list(mapFn: (IN) -> OUT) : + DataProperty, List>(exists, { (get(it) as List).map(mapFn) }) + + inner class obj>(mapFn: (CONTENT) -> OUT) : + DataProperty, OUT>(exists, { mapFn(get(it) as CONTENT) }) +} diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt similarity index 93% rename from lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt rename to data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt index 403b3b1..7a98ba0 100644 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt @@ -1,10 +1,10 @@ -package dev.forkhandles.lens +package dev.forkhandles.data import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty import kotlin.reflect.jvm.jvmErasure -abstract class LensProp( +abstract class DataProperty( private val existsFn: IN.(String) -> Boolean, private val getFn: IN.(String) -> Any? ) : ReadOnlyProperty { diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt new file mode 100644 index 0000000..8de3e22 --- /dev/null +++ b/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt @@ -0,0 +1,30 @@ +package dev.forkhandles.data + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.* + +/** + * Jackson JsonNode-based implementation of the DataContainer + */ +abstract class JacksonDataContainer(input: JsonNode) : + DataContainer( + input, + { content, it -> content.has(it) }, + { content, it -> content[it]?.let(Companion::nodeToValue) } + ) { + + companion object { + private fun nodeToValue(input: JsonNode): Any? = when (input) { + is BooleanNode -> input.booleanValue() + is IntNode -> input.intValue() + is LongNode -> input.longValue() + is DecimalNode -> input.decimalValue() + is DoubleNode -> input.doubleValue() + is TextNode -> input.textValue() + is ArrayNode -> input.map(::nodeToValue) + is ObjectNode -> input + is NullNode -> null + else -> error("Invalid node type $input") + } + } +} diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt new file mode 100644 index 0000000..6b1759f --- /dev/null +++ b/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt @@ -0,0 +1,11 @@ +package dev.forkhandles.data + +import kotlin.collections.Map + +/** + * Map-based implementation of the DataContainer + */ +abstract class MapDataContainer(input: Map) : + DataContainer>(input, { content, it -> content.containsKey(it) }, { content, it -> content[it] }) + + diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt new file mode 100644 index 0000000..b2d503e --- /dev/null +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt @@ -0,0 +1,70 @@ +package dev.forkhandles.lens + +import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.api.expectThrows +import strikt.assertions.isEqualTo +import strikt.assertions.message + +interface MainClassFields { + val stringField: String + val booleanField: Boolean + val intField: Int + val longField: Long + val decimalField: Double + val notAStringField: String + val optionalField: String? + val noSuchField: String + val listSubClassField: List + val listIntsField: List + val listStringsField: List + val objectField: SubClassFields +} + +interface SubClassFields { + val stringField: String + val noSuchField: String +} + +abstract class DataContainerContract { + + abstract fun container(input: Map): MainClassFields + + @Test + fun `can get values from properties`() { + val input = container( + mapOf( + "stringField" to "string", + "optionalField" to "optional", + "booleanField" to true, + "intField" to 123, + "longField" to Long.MAX_VALUE, + "decimalField" to 1.1234, + "notAStringField" to 123, + "listIntsField" to listOf(1, 2, 3), + "listSubClassField" to listOf( + mapOf("stringField" to "string1"), + mapOf("stringField" to "string2"), + ), + "listStringsField" to listOf("string1", "string2"), + "objectField" to mapOf( + "stringField" to "string" + ) + ) + ) + + expectThat(input.stringField).isEqualTo("string") + expectThat(input.optionalField).isEqualTo("optional") + expectThat(input.booleanField).isEqualTo(true) + expectThat(input.intField).isEqualTo(123) + expectThat(input.longField).isEqualTo(Long.MAX_VALUE) + expectThat(input.decimalField).isEqualTo(1.1234) + expectThat(input.listIntsField).isEqualTo(listOf(1, 2, 3)) + expectThat(input.listSubClassField.map { it.stringField }).isEqualTo(listOf("string1", "string2")) + expectThat(input.listStringsField).isEqualTo(listOf("string1", "string2")) + expectThat(input.objectField.stringField).isEqualTo("string") + expectThrows { input.notAStringField }.message.isEqualTo("Value for field is not a class kotlin.String but class kotlin.Int") + expectThrows { input.noSuchField }.message.isEqualTo("Field is missing") + expectThrows { input.objectField.noSuchField }.message.isEqualTo("Field is missing") + } +} diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt new file mode 100644 index 0000000..b5cd6dd --- /dev/null +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt @@ -0,0 +1,30 @@ +package dev.forkhandles.lens + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import dev.forkhandles.data.JacksonDataContainer + +class JacksonDataContainerTest : DataContainerContract() { + + class SubNodeBacked(node: JsonNode) : JacksonDataContainer(node), SubClassFields { + override val stringField by field() + override val noSuchField by field() + } + + class NodeBacked(node: JsonNode) : JacksonDataContainer(node), MainClassFields { + override val stringField by field() + override val optionalField by field() + override val booleanField by field() + override val intField by field() + override val longField by field() + override val decimalField by field() + override val notAStringField by field() + override val noSuchField by field() + override val listSubClassField by list(::SubNodeBacked) + override val listStringsField by list(Any::toString) + override val listIntsField by list { it as Int } + override val objectField by obj(::SubNodeBacked) + } + + override fun container(input: Map) = NodeBacked(ObjectMapper().valueToTree(input)) +} diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt new file mode 100644 index 0000000..11f14c9 --- /dev/null +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt @@ -0,0 +1,28 @@ +package dev.forkhandles.lens + +import dev.forkhandles.data.MapDataContainer + +class MapDataContainerTest : DataContainerContract() { + + class SubMap(propertySet: Map) : MapDataContainer(propertySet), SubClassFields { + override val stringField by field() + override val noSuchField by field() + } + + class MapBacked(propertySet: Map) : MapDataContainer(propertySet), MainClassFields { + override val stringField by field() + override val optionalField by field() + override val booleanField by field() + override val intField by field() + override val longField by field() + override val decimalField by field() + override val notAStringField by field() + override val noSuchField by field() + override val listSubClassField by list(::SubMap) + override val listStringsField by list(Any::toString) + override val listIntsField by list { it as Int } + override val objectField by obj(::SubMap) + } + + override fun container(input: Map): MainClassFields = MapBacked(input) +} diff --git a/lens4k/build.gradle b/lens4k/build.gradle deleted file mode 100644 index 00691af..0000000 --- a/lens4k/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -description = 'ForkHandles simple lens library' - -dependencies { - api("org.jetbrains.kotlin:kotlin-reflect:_") - api("com.fasterxml.jackson.core:jackson-databind:2.16.1") - testImplementation("io.strikt:strikt-jvm:0.34.1") -} diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/AbstractWrapper.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/AbstractWrapper.kt deleted file mode 100644 index 64ce529..0000000 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/AbstractWrapper.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.forkhandles.lens - -@Suppress("UNCHECKED_CAST") -abstract class AbstractWrapper( - private val contents: CONTENT, - existsFn: (CONTENT, String) -> Boolean, - getFn: (CONTENT, String) -> Any? -) { - private val exists: AbstractWrapper.(String) -> Boolean = { existsFn(contents, it) } - private val get: AbstractWrapper.(String) -> Any? = { getFn(contents, it) } - - inner class Field : LensProp, OUT>(exists, get) - - inner class ListField(mapFn: (IN) -> OUT) : - LensProp, List>(exists, { (get(it) as List).map(mapFn) }) - - inner class ObjectField>(mapFn: (CONTENT) -> OUT) : - LensProp, OUT>(exists, { mapFn(get(it) as CONTENT) }) -} diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/JacksonWrapper.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/JacksonWrapper.kt deleted file mode 100644 index 7941020..0000000 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/JacksonWrapper.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.forkhandles.lens - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.* - -abstract class JacksonWrapper(node: JsonNode) : - AbstractWrapper( - node, - { content, it -> content.has(it) }, - { content, it -> content[it]?.let(::asValue) } - ) { - - companion object { - private fun asValue(jsonNode: JsonNode): Any? = when (jsonNode) { - is BooleanNode -> jsonNode.booleanValue() - is IntNode -> jsonNode.intValue() - is LongNode -> jsonNode.longValue() - is DecimalNode -> jsonNode.decimalValue() - is DoubleNode -> jsonNode.doubleValue() - is TextNode -> jsonNode.textValue() - is ArrayNode -> jsonNode.map { asValue(it) } - is ObjectNode -> jsonNode - is NullNode -> null - else -> error("Invalid node type $jsonNode") - } - - } -} diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/Lens.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/Lens.kt deleted file mode 100644 index 44bf122..0000000 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/Lens.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.forkhandles.lens - -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.instanceParameter -import kotlin.reflect.full.memberFunctions - -interface Lens : (T) -> R { - fun get(subject: T): R - fun inject(subject: T, value: R): T - fun update(subject: T, f: (R) -> R): T = inject(subject, f(get(subject))) - override fun invoke(subject: T): R = get(subject) - operator fun invoke(subject: T, value: R): T = inject(subject, value) -} - -infix fun LensObject.andThen(second: LensObject) = LensObject( - { second.get(get(it)) }, - { subject, value -> - inject( - subject, - second.inject(get(subject), value) - ) - } -) - -operator fun IN.get(extractor: (IN) -> OUT) = extractor(this) -fun IN.with(lens: Lens, of: OUT) = lens.inject(this, of) -fun IN.updatedWith(lens: Lens, f: (OUT) -> OUT) = lens.update(this, f) - -data class LensObject( - val getter: (IN) -> OUT, - val injector: (IN, OUT) -> IN -) : Lens { - override fun get(subject: IN) = getter(subject) - override fun inject(subject: IN, value: OUT) = injector(subject, value) -} - -inline fun KProperty1.asLens(): LensObject = LensObject( - ::get, - reflectiveCopy(name) -) - -inline fun reflectiveCopy(propertyName: String): (T, R) -> T { - val copyFunction = T::class.memberFunctions.firstOrNull { it.name == "copy" } - ?: error("No copy method found") - val instanceParam = copyFunction.instanceParameter - ?: error("No copy method instance parameter found") - val nameParam = copyFunction.parameters.find { it.name == propertyName } - ?: error("No copy method parameter named $propertyName found") - return { subject, value -> - copyFunction.callBy( - mapOf( - instanceParam to subject, - nameParam to value - ) - ) as T - } -} diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt deleted file mode 100644 index d200f01..0000000 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.forkhandles.lens - -abstract class MapWrapper(map: Map) : - AbstractWrapper>(map, { content, it -> content.containsKey(it) }, { content, it -> content[it] }) - - diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/PropertySet.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/PropertySet.kt deleted file mode 100644 index 30cf1ab..0000000 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/PropertySet.kt +++ /dev/null @@ -1,32 +0,0 @@ -package dev.forkhandles.lens - -typealias PropertySet = Map - -inline fun PropertySet.valueOf(key: String): T = - when (val result: Any? = get(key)) { - is T -> result - null -> { - when { - containsKey(key) -> throw NoSuchElementException("Value for key <$key> is null") - else -> throw NoSuchElementException("Key <$key> is missing in the map") - } - } - - else -> throw NoSuchElementException("Value for key <$key> is not a ${T::class}") - } - -object PropertySets { - @JvmName("lensPropertySet") - fun lens(propertyName: String) = lens(propertyName) - - @JvmName("asLensPropertySet") - fun String.asLens() = lens(this) - - inline fun String.asLens() = lens(this) - - inline fun lens(propertyName: String) = - LensObject( - getter = { it.valueOf(propertyName) }, - injector = { subject, value -> subject.toMutableMap().apply { this[propertyName] = value } } - ) -} diff --git a/lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt b/lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt deleted file mode 100644 index 466d681..0000000 --- a/lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package dev.forkhandles.lens - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import org.junit.jupiter.api.Test -import strikt.api.expectThat -import strikt.api.expectThrows -import strikt.assertions.isEqualTo -import strikt.assertions.message - -class JacksonWrapperTest { - - class SubNodeBacked(node: JsonNode) : JacksonWrapper(node) { - val stringField by Field() - val noSuchField by Field() - } - - class NodeBacked(node: JsonNode) : JacksonWrapper(node) { - val stringField by Field() - val booleanField by Field() - val intField by Field() - val longField by Field() - val decimalField by Field() - val notAStringField by Field() - val noSuchField by Field() - val listField by ListField(::SubNodeBacked) - val listField2 by ListField(Any::toString) - val objectField by ObjectField(::SubNodeBacked) - } - - @Test - fun `can get values from properties`() { - val map = mapOf( - "stringField" to "string", - "booleanField" to true, - "intField" to 123, - "longField" to Long.MAX_VALUE, - "decimalField" to 1.1234, - "notAStringField" to 123, - "listField" to listOf( - mapOf("stringField" to "string1"), - mapOf("stringField" to "string2"), - ), - "listField2" to listOf("string1", "string2"), - "objectField" to mapOf( - "stringField" to "string" - ) - ) - - val mapBacked = NodeBacked( - ObjectMapper().valueToTree( - map - ) - ) - - expectThat(mapBacked.stringField).isEqualTo("string") - expectThat(mapBacked.booleanField).isEqualTo(true) - expectThat(mapBacked.intField).isEqualTo(123) - expectThat(mapBacked.longField).isEqualTo(Long.MAX_VALUE) - expectThat(mapBacked.decimalField).isEqualTo(1.1234) - expectThat(mapBacked.listField.map { it.stringField }).isEqualTo(listOf("string1", "string2")) - expectThat(mapBacked.listField2).isEqualTo(listOf("string1", "string2")) - expectThat(mapBacked.objectField.stringField).isEqualTo("string") - expectThrows { mapBacked.notAStringField }.message.isEqualTo("Value for field is not a class kotlin.String but class kotlin.Int") - expectThrows { mapBacked.noSuchField }.message.isEqualTo("Field is missing") - expectThrows { mapBacked.objectField.noSuchField }.message.isEqualTo("Field is missing") - } -} diff --git a/lens4k/src/test/kotlin/dev/forkhandles/lens/MapWrapperTest.kt b/lens4k/src/test/kotlin/dev/forkhandles/lens/MapWrapperTest.kt deleted file mode 100644 index e8114d7..0000000 --- a/lens4k/src/test/kotlin/dev/forkhandles/lens/MapWrapperTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package dev.forkhandles.lens - -import org.junit.jupiter.api.Test -import strikt.api.expectThat -import strikt.api.expectThrows -import strikt.assertions.isEqualTo -import strikt.assertions.message - -class MapWrapperTest { - - class SubMap(propertySet: Map) : MapWrapper(propertySet) { - val stringField by Field() - val noSuchField by Field() - } - - class MapBacked(propertySet: Map) : MapWrapper(propertySet) { - val stringField by Field() - val booleanField by Field() - val intField by Field() - val longField by Field() - val decimalField by Field() - val notAStringField by Field() - val noSuchField by Field() - val listField by ListField(::SubMap) - val listField2 by ListField(Any::toString) - val objectField by ObjectField(::SubMap) - } - - @Test - fun `can get values from properties`() { - val mapBacked = MapBacked( - mapOf( - "stringField" to "string", - "booleanField" to true, - "intField" to 123, - "longField" to Long.MAX_VALUE, - "decimalField" to 1.1234, - "notAStringField" to 123, - "listField" to listOf( - mapOf("stringField" to "string1"), - mapOf("stringField" to "string2"), - ), - "listField2" to listOf("string1", "string2"), - "objectField" to mapOf( - "stringField" to "string" - ) - ) - ) - - expectThat(mapBacked.stringField).isEqualTo("string") - expectThat(mapBacked.booleanField).isEqualTo(true) - expectThat(mapBacked.intField).isEqualTo(123) - expectThat(mapBacked.longField).isEqualTo(Long.MAX_VALUE) - expectThat(mapBacked.decimalField).isEqualTo(1.1234) - expectThat(mapBacked.listField.map { it.stringField }).isEqualTo(listOf("string1", "string2")) - expectThat(mapBacked.listField2).isEqualTo(listOf("string1", "string2")) - expectThat(mapBacked.objectField.stringField).isEqualTo("string") - expectThrows { mapBacked.notAStringField }.message.isEqualTo("Value for field is not a class kotlin.String but class kotlin.Int") - expectThrows { mapBacked.noSuchField }.message.isEqualTo("Field is missing") - expectThrows { mapBacked.objectField.noSuchField }.message.isEqualTo("Field is missing") - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8a324e0..e60b3b3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,8 +18,8 @@ fun String.includeModule(dir: String) { include("forkhandles-bom") include("bunting4k") +include("data4k") include("fabrikate4k") -include("lens4k") include("mock4k") include("parser4k") include("partial4k") diff --git a/versions.properties b/versions.properties index 7df9a8d..6a07db5 100644 --- a/versions.properties +++ b/versions.properties @@ -13,6 +13,8 @@ plugin.io.codearte.nexus-staging=0.30.0 plugin.me.champeau.jmh=0.7.2 +version.com.fasterxml.jackson.core..jackson-databind=2.16.1 + version.com.natpryce..hamkrest=1.8.0.1 version.junit.jupiter=5.10.1 @@ -25,4 +27,12 @@ version.kotlinx.coroutines=1.7.3 version.kotlinx.serialization=1.6.2 +version.strikt=0.34.1 + +## unused +version.org.openjdk.jmh..jmh-generator-bytecode=1.36 + +## unused +version.org.openjdk.jmh..jmh-core=1.36 + version.org.kt3k.gradle.plugin..coveralls-gradle-plugin=2.8.3