From 07d19e269baaf29ef29510d3f8d85eef1b5955a0 Mon Sep 17 00:00:00 2001 From: David Denton Date: Wed, 3 Jan 2024 22:42:51 +0000 Subject: [PATCH] added optional support --- .../dev/forkhandles/data/DataContainer.kt | 16 ++-- .../dev/forkhandles/data/DataProperty.kt | 4 +- .../forkhandles/lens/DataContainerContract.kt | 94 +++++++++++++------ .../lens/JacksonDataContainerTest.kt | 6 +- .../forkhandles/lens/MapDataContainerTest.kt | 14 ++- 5 files changed, 91 insertions(+), 43 deletions(-) diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt index ade4302..5e32cee 100644 --- a/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt @@ -18,23 +18,21 @@ abstract class DataContainer( fun field() = DataProperty, OUT>(exists, get) - fun field(mapFn: (OUT) -> NEXT) = DataProperty, NEXT>(exists) { - (get(it) as OUT).let(mapFn) + fun field(mapFn: (OUT) -> NEXT) = DataProperty, NEXT>(exists) { + (get(it) as OUT)?.let(mapFn) } - fun > field(factory: ValueFactory) = - DataProperty, OUT>(exists) { (get(it) as IN).let(factory::of) } + fun > field(factory: ValueFactory) = field(factory::of) fun list(mapFn: (IN) -> OUT) = - DataProperty, List>(exists) { (get(it) as List).map(mapFn) } + DataProperty, List>(exists) { get(it)?.let { (it as List).map(mapFn) } } - fun > list(factory: ValueFactory) = - DataProperty, List>(exists) { (get(it) as List).map(factory::of) } + fun > list(factory: ValueFactory) = list(factory::of) fun list() = list { it } fun > obj(mapFn: (CONTENT) -> OUT) = - DataProperty, OUT>(exists) { mapFn(get(it) as CONTENT) } + DataProperty, OUT>(exists) { (get(it) as CONTENT)?.let(mapFn) } - fun obj() = DataProperty, CONTENT>(exists) { get(it) as CONTENT } + fun obj() = DataProperty, CONTENT>(exists) { get(it) as CONTENT? } } diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt index f1f13b6..d1775a7 100644 --- a/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt @@ -4,15 +4,17 @@ import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty import kotlin.reflect.jvm.jvmErasure -open class DataProperty( +open class DataProperty( private val existsFn: IN.(String) -> Boolean, private val getFn: IN.(String) -> Any? ) : ReadOnlyProperty { @Suppress("UNCHECKED_CAST") override fun getValue(thisRef: IN, property: KProperty<*>): OUT { val result = thisRef.getFn(property.name) + return when { result == null -> when { + property.returnType.isMarkedNullable -> null as OUT thisRef.existsFn(property.name) -> throw NoSuchElementException("Value for field <${property.name}> is null") else -> throw NoSuchElementException("Field <${property.name}> is missing") } diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt index 66763be..ad6959f 100644 --- a/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.api.expectThrows import strikt.assertions.isEqualTo +import strikt.assertions.isNull import strikt.assertions.message interface MainClassFields { @@ -15,16 +16,22 @@ interface MainClassFields { val longField: Long val decimalField: Double val notAStringField: String - val optionalField: String? - val noSuchField: String + + val mappedField: Int + val listField: List val listSubClassField: List val listIntsField: List val listValueField: List - val listStringsField: List + val objectField: SubClassFields + val valueField: MyType - val mappedField: Int + + val optionalField: String? + val optionalValueField: MyType? + val optionalObjectField: SubClassFields? + val optionalListField: List? } interface SubClassFields { @@ -41,11 +48,10 @@ abstract class DataContainerContract { abstract fun container(input: Map): MainClassFields @Test - fun `can get values from properties`() { + fun `can get primitive values from properties`() { val input = container( mapOf( "stringField" to "string", - "optionalField" to "optional", "booleanField" to true, "intField" to 123, "longField" to Long.MAX_VALUE, @@ -53,37 +59,71 @@ abstract class DataContainerContract { "notAStringField" to 123, "valueField" to 123, "mappedField" to "123", - "listField" to listOf("hello"), - "listField" to listOf("hello"), - "listValueField" to listOf(1, 2, 3), - "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" - ) + + "optionalValueField" to 123, + "optionalField" to "optional", ) ) expectThat(input.stringField).isEqualTo("string") - expectThat(input.optionalField).isEqualTo("optional") + expectThrows { container(mapOf()).stringField }.message.isEqualTo("Field is missing") + expectThrows { input.notAStringField }.message.isEqualTo("Value for field is not a class kotlin.String but class kotlin.Int") + 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.valueField).isEqualTo(MyType.of(123)) + expectThat(input.mappedField).isEqualTo(123) - expectThat(input.listField).isEqualTo(listOf("hello")) - expectThat(input.listIntsField).isEqualTo(listOf(1, 2, 3)) - expectThat(input.listValueField).isEqualTo(listOf(1, 2, 3).map(MyType::of)) - expectThat(input.listSubClassField.map { it.stringField }).isEqualTo(listOf("string1", "string2")) - expectThat(input.listStringsField).isEqualTo(listOf("string1", "string2")) + expectThat(input.valueField).isEqualTo(MyType.of(123)) + + expectThat(input.optionalField).isEqualTo("optional") + expectThat(container(mapOf()).optionalField).isNull() + + expectThat(input.optionalValueField).isEqualTo(MyType.of(123)) + expectThat(container(mapOf()).optionalValueField).isNull() + } + + @Test + fun `object inputs`() { + val input = container( + mapOf( + "objectField" to mapOf( + "stringField" to "string" + ), + "optionalObjectField" to mapOf( + "stringField" to "string" + ) + ) + ) + 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") + + expectThat(input.optionalObjectField?.stringField).isEqualTo("string") + expectThat(container(mapOf()).optionalObjectField).isNull() + } + + @Test + fun `list inputs`() { + val listInput = container( + mapOf( + "listField" to listOf("string1", "string2"), + "listIntsField" to listOf(1, 2, 3), + "listValueField" to listOf(1, 2, 3), + "listSubClassField" to listOf( + mapOf("stringField" to "string1"), + mapOf("stringField" to "string2"), + ), + "optionalListField" to listOf("hello") + ) + ) + expectThat(listInput.listField).isEqualTo(listOf("string1", "string2")) + expectThat(listInput.listIntsField).isEqualTo(listOf(1, 2, 3)) + expectThat(listInput.listValueField).isEqualTo(listOf(1, 2, 3).map(MyType::of)) + expectThat(listInput.listSubClassField.map { it.stringField }).isEqualTo(listOf("string1", "string2")) + + expectThat(listInput.optionalListField).isEqualTo(listOf("hello")) + expectThat(container(mapOf()).optionalListField).isNull() } } diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt index 1aad5ac..9ab5b06 100644 --- a/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt @@ -19,15 +19,17 @@ class JacksonDataContainerTest : DataContainerContract() { 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 listField by list() override val listIntsField by list() override val listValueField by list(MyType) override val objectField by obj(::SubNodeBacked) override val valueField by field(MyType) override val mappedField by field(String::toInt) + + override val optionalListField: List? by list() + override val optionalObjectField: SubClassFields? by obj(::SubNodeBacked) + override val optionalValueField: MyType? by field(MyType) } 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 index 38d6372..fee2421 100644 --- a/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt @@ -11,21 +11,27 @@ class MapDataContainerTest : DataContainerContract() { 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 mappedField by field(String::toInt) + override val listField by list() override val listValueField by list(MyType) override val listSubClassField by list(::SubMap) - override val listStringsField by list(Any::toString) override val listIntsField by list() + override val objectField by obj(::SubMap) + override val valueField by field(MyType) - override val mappedField by field(String::toInt) + + override val optionalField by field() + override val optionalListField: List? by list() + override val optionalObjectField: SubClassFields? by obj(::SubMap) + override val optionalValueField: MyType? by field(MyType) } override fun container(input: Map): MainClassFields = MapBacked(input)