From c6be55e1784e94dca89cefdc862ba92ddb588eb6 Mon Sep 17 00:00:00 2001 From: David Denton Date: Wed, 3 Jan 2024 23:09:28 +0000 Subject: [PATCH] adding write support --- .../dev/forkhandles/data/DataContainer.kt | 80 ++++++++-- .../dev/forkhandles/data/DataProperty.kt | 10 +- .../forkhandles/data/JacksonDataContainer.kt | 13 +- .../dev/forkhandles/data/MapDataContainer.kt | 9 +- .../forkhandles/lens/DataContainerContract.kt | 145 ++++++++++++++---- .../lens/JacksonDataContainerTest.kt | 40 ++--- .../forkhandles/lens/MapDataContainerTest.kt | 42 ++--- 7 files changed, 244 insertions(+), 95 deletions(-) diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt index 8ec17fe6..aea126d6 100644 --- a/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt @@ -9,30 +9,76 @@ import dev.forkhandles.values.ValueFactory */ @Suppress("UNCHECKED_CAST") abstract class DataContainer( - protected val data: CONTENT, - existsFn: (CONTENT, String) -> Boolean, - getFn: (CONTENT, String) -> Any? + val data: CONTENT, + private val existsFn: (CONTENT, String) -> Boolean, + private val getFn: (CONTENT, String) -> Any?, + private val setFn: (CONTENT, String, Any?) -> Unit ) { - private val exists: DataContainer.(String) -> Boolean = { existsFn(data, it) } - private val get: DataContainer.(String) -> Any? = { getFn(data, it) } - fun field() = DataProperty, OUT>(exists, get) + // + // CORE FUNCTIONS - these are the only ones that should call the defined mappings + // - fun field(mapFn: (OUT) -> NEXT) = DataProperty, NEXT>(exists) { - (get(it) as OUT)?.let(mapFn) - } + fun field(mapInFn: (OUT) -> NEXT, mapOutFn: (NEXT) -> OUT?) = + property(mapInFn, mapOutFn) + + fun ?> obj(mapInFn: (CONTENT) -> OUT, mapOutFn: (OUT) -> CONTENT?) = + property(mapInFn, mapOutFn) + + fun list(mapInFn: (IN) -> OUT, mapOutFn: (OUT) -> IN?) = + property, List, List>({ it.map(mapInFn) }, { it.mapNotNull(mapOutFn) }) + + // + // "PRE MAPPED" FUNCTIONS + // + + fun field() = field({ it }, { it }) + + fun list() = list({ it }, { it }) + + fun field(mapInFn: (OUT) -> NEXT) = field(mapInFn) { error("no outbound mapping defined") } + +// fun list(mapInFn: (IN) -> OUT) = +// list(mapInFn) { error("no outbound mapping defined") } +// - fun , OUT2 : OUT?> field(factory: ValueFactory) = field(factory::of) + fun ?> list(mapInFn: (CONTENT) -> OUT) = + list(mapInFn) { it?.data } - fun list(mapFn: (IN) -> OUT) = - DataProperty, List>(exists) { get(it)?.let { (it as List).map(mapFn) } } + fun ?> obj(mapInFn: (CONTENT) -> OUT) = + obj(mapInFn) { it?.data } - fun > list(factory: ValueFactory) = list(factory::of) + // + // VALUES4K functions + // + + fun > field(factory: ValueFactory) = + field(factory::of) { it.value } + + fun > list(factory: ValueFactory) = + list(factory::of) { it.value } + + // + // UTILITY FUNCTIONS - for comparisons etc + // + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DataContainer<*> + + return data == other.data + } - fun list() = list { it } + override fun hashCode() = data?.hashCode() ?: 0 - fun ?> obj(mapFn: (CONTENT) -> OUT) = - DataProperty, OUT>(exists) { (get(it) as CONTENT)?.let(mapFn) } + override fun toString() = data.toString() - fun obj() = DataProperty, CONTENT>(exists) { get(it) as CONTENT? } + private fun property(mapInFn: (OUT) -> IN, mapOutFn: (IN) -> OUT2?) = + DataProperty, IN>( + { existsFn(data, it) }, + { getFn(data, it)?.let { it as OUT }?.let(mapInFn) }, + { name, value -> setFn(data, name, (value as IN?)?.let(mapOutFn)) } + ) } diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt index d1775a7f..959e9e16 100644 --- a/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt @@ -1,13 +1,14 @@ package dev.forkhandles.data -import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import kotlin.reflect.jvm.jvmErasure open class DataProperty( private val existsFn: IN.(String) -> Boolean, - private val getFn: IN.(String) -> Any? -) : ReadOnlyProperty { + private val getFn: IN.(String) -> Any?, + private val setFn: IN.(String, Any?) -> Unit +) : ReadWriteProperty { @Suppress("UNCHECKED_CAST") override fun getValue(thisRef: IN, property: KProperty<*>): OUT { val result = thisRef.getFn(property.name) @@ -18,10 +19,13 @@ open class DataProperty( thisRef.existsFn(property.name) -> throw NoSuchElementException("Value for field <${property.name}> is null") else -> throw NoSuchElementException("Field <${property.name}> is missing") } + property.returnType.jvmErasure.isInstance(result) -> result as OUT else -> throw NoSuchElementException("Value for field <${property.name}> is not a ${property.returnType.jvmErasure} but ${result.javaClass.kotlin}") } } + + override fun setValue(thisRef: IN, property: KProperty<*>, value: OUT) = thisRef.setFn(property.name, value) } diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt index 8de3e22d..c202ddf9 100644 --- a/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/JacksonDataContainer.kt @@ -1,7 +1,15 @@ package dev.forkhandles.data import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.node.* +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.BooleanNode +import com.fasterxml.jackson.databind.node.DecimalNode +import com.fasterxml.jackson.databind.node.DoubleNode +import com.fasterxml.jackson.databind.node.IntNode +import com.fasterxml.jackson.databind.node.LongNode +import com.fasterxml.jackson.databind.node.NullNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode /** * Jackson JsonNode-based implementation of the DataContainer @@ -10,7 +18,8 @@ abstract class JacksonDataContainer(input: JsonNode) : DataContainer( input, { content, it -> content.has(it) }, - { content, it -> content[it]?.let(Companion::nodeToValue) } + { content, it -> content[it]?.let(Companion::nodeToValue) }, + { _, _, _ -> TODO() } ) { companion object { diff --git a/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt b/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt index 6b1759f2..cc10e152 100644 --- a/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt +++ b/data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt @@ -1,11 +1,10 @@ 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] }) - - + DataContainer>(input.toMutableMap(), { content, it -> content.containsKey(it) }, + { content, it -> content[it] }, + { map, name, value -> map[name] = value } + ) diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt index c62fa528..b65bbff0 100644 --- a/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt @@ -8,47 +8,49 @@ import strikt.api.expectThrows import strikt.assertions.isEqualTo import strikt.assertions.isNull import strikt.assertions.message +import kotlin.reflect.KMutableProperty0 -interface MainClassFields { - val stringField: String - val booleanField: Boolean - val intField: Int - val longField: Long - val decimalField: Double - val notAStringField: String +interface MainClassFields { + var stringField: String + var booleanField: Boolean + var intField: Int + var longField: Long + var decimalField: Double + var notAStringField: String - val mappedField: Int + var mappedField: Int - val listField: List - val listSubClassField: List - val listIntsField: List - val listValueField: List + var listField: List + var listSubClassField: List + var listIntsField: List + var listValueField: List - val objectField: SubClassFields + var objectField: T - val valueField: MyType + var valueField: MyType - val optionalField: String? + var optionalField: String? val optionalValueField: MyType? - val optionalObjectField: SubClassFields? + var optionalObjectField: T? val optionalListField: List? } interface SubClassFields { - val stringField: String - val noSuchField: String + var stringField: String + var noSuchField: String } class MyType private constructor(value: Int) : IntValue(value) { companion object : IntValueFactory(::MyType) } -abstract class DataContainerContract { +abstract class DataContainerContract { - abstract fun container(input: Map): MainClassFields + abstract fun container(input: Map): MainClassFields + abstract fun subContainer(input: Map): T @Test - fun `can get primitive values from properties`() { + fun `can read primitives values`() { val input = container( mapOf( "stringField" to "string", @@ -86,7 +88,35 @@ abstract class DataContainerContract { } @Test - fun `object inputs`() { + fun `can write primitives values`() { + val input = container( + mapOf( + "stringField" to "string", + "booleanField" to true, + "intField" to 123, + "longField" to Long.MAX_VALUE, + "decimalField" to 1.1234, + "valueField" to 123, + "mappedField" to "123", + + "optionalValueField" to 123, + "optionalField" to "optional", + ) + ) + + expectSetWorks(input::stringField, "123") + expectSetWorks(input::booleanField, false) + expectSetWorks(input::intField, 999) + expectSetWorks(input::longField, 0) + expectSetWorks(input::decimalField, 5.4536) + + expectSetWorks(input::optionalField, "123123") + expectSetWorks(input::optionalField, null) +// expectThat(input::mappedField, 123) + } + + @Test + fun `read object values`() { val input = container( mapOf( "objectField" to mapOf( @@ -103,11 +133,36 @@ abstract class DataContainerContract { expectThat(input.optionalObjectField?.stringField).isEqualTo("string") expectThat(container(mapOf()).optionalObjectField).isNull() + expectThat(input.optionalObjectField?.stringField).isEqualTo("string") + } + + @Test + fun `write object values`() { + val objFieldNext = mapOf( + "stringField" to "string2" + ) + val input = container( + mapOf( + "objectField" to objFieldNext, + "optionalObjectField" to mapOf( + "stringField" to "string" + ) + ) + ) + + val nextObj = subContainer(objFieldNext) + expectSetWorks(input::objectField, nextObj) + expectThat(input.objectField).isEqualTo(subContainer(objFieldNext)) + + expectSetWorks(input::optionalObjectField, nextObj) + expectThat(input.optionalObjectField).isEqualTo(nextObj) + expectSetWorks(input::optionalObjectField, null) + expectThat(input.optionalObjectField).isEqualTo(null) } @Test - fun `list inputs`() { - val listInput = container( + fun `read list values`() { + val input = container( mapOf( "listField" to listOf("string1", "string2"), "listIntsField" to listOf(1, 2, 3), @@ -119,12 +174,44 @@ abstract class DataContainerContract { "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(input.listField).isEqualTo(listOf("string1", "string2")) + 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(listInput.optionalListField).isEqualTo(listOf("hello")) + expectThat(input.optionalListField).isEqualTo(listOf("hello")) expectThat(container(mapOf()).optionalListField).isNull() } + + @Test + fun `write list values`() { + val input = container( + mapOf( + "listField" to listOf("string1", "string2"), + "listSubClassField" to listOf( + mapOf("stringField" to "string1"), + mapOf("stringField" to "string2"), + ), + "listValueField" to listOf(1, 2, 3), + "optionalListField" to listOf("hello") + ) + ) + + expectSetWorks(input::listField, listOf("123")) + expectSetWorks(input::listSubClassField, listOf(subContainer(mapOf("123" to "123")))) +// +// expectThat(input.listField).isEqualTo(listOf("string1", "string2")) +// 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.optionalListField).isEqualTo(listOf("hello")) +// expectThat(container(mapOf()).optionalListField).isNull() + } + + private fun expectSetWorks(prop: KMutableProperty0, value: T) { + prop.set(value) + expectThat(prop.get()).isEqualTo(value) + } + } diff --git a/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt b/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt index 9ab5b06d..9685386b 100644 --- a/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/JacksonDataContainerTest.kt @@ -4,33 +4,35 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import dev.forkhandles.data.JacksonDataContainer -class JacksonDataContainerTest : DataContainerContract() { +class JacksonDataContainerTest : DataContainerContract() { class SubNodeBacked(node: JsonNode) : JacksonDataContainer(node), SubClassFields { - override val stringField by field() - override val noSuchField by field() + override var stringField by field() + override var 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 listSubClassField by list(::SubNodeBacked) - 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) + class NodeBacked(node: JsonNode) : JacksonDataContainer(node), MainClassFields { + override var stringField by field() + override var optionalField by field() + override var booleanField by field() + override var intField by field() + override var longField by field() + override var decimalField by field() + override var notAStringField by field() + override var listSubClassField by list(::SubNodeBacked) + override var listField by list() + override var listIntsField by list() + override var listValueField by list(MyType) + override var objectField by obj(::SubNodeBacked) + override var valueField by field(MyType) + override var mappedField by field(String::toInt) override val optionalListField: List? by list() - override val optionalObjectField: SubClassFields? by obj(::SubNodeBacked) + override var optionalObjectField by obj(::SubNodeBacked) override val optionalValueField: MyType? by field(MyType) } override fun container(input: Map) = NodeBacked(ObjectMapper().valueToTree(input)) + override fun subContainer(input: Map) = + SubNodeBacked(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 6548404b..a4be3a67 100644 --- a/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt +++ b/data4k/src/test/kotlin/dev/forkhandles/lens/MapDataContainerTest.kt @@ -2,37 +2,39 @@ package dev.forkhandles.lens import dev.forkhandles.data.MapDataContainer -class MapDataContainerTest : DataContainerContract() { +class MapDataContainerTest : DataContainerContract() { class SubMap(propertySet: Map) : MapDataContainer(propertySet), SubClassFields { - override val stringField by field() - override val noSuchField by field() + override var stringField by field() + override var noSuchField by field() } - class MapBacked(propertySet: Map) : MapDataContainer(propertySet), MainClassFields { - override val stringField 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() + class MapBacked(map: Map) : MapDataContainer(map), MainClassFields { + override var stringField by field() + override var booleanField by field() + override var intField by field() + override var longField by field() + override var decimalField by field() + override var notAStringField by field() - override val mappedField by field(String::toInt) + override var mappedField by field(String::toInt) - override val listField by list() - override val listValueField by list(MyType) - override val listSubClassField by list(::SubMap) - override val listIntsField by list() + override var listField by list() + override var listValueField by list(MyType) + override var listSubClassField by list(::SubMap) + override var listIntsField by list() - override val objectField by obj(::SubMap) + override var objectField by obj(::SubMap) - override val valueField by field(MyType) + override var valueField by field(MyType) - override val optionalField by field() + override var optionalField by field() override val optionalListField: List? by list() - override val optionalObjectField: SubMap? by obj(::SubMap) + override var optionalObjectField by obj(::SubMap) override val optionalValueField: MyType? by field(MyType) } - override fun container(input: Map): MainClassFields = MapBacked(input) + override fun container(input: Map) = MapBacked(input) + + override fun subContainer(input: Map) = SubMap(input) }