Skip to content

Commit

Permalink
adding write support
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton committed Jan 4, 2024
1 parent c6affb0 commit c6be55e
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 95 deletions.
80 changes: 63 additions & 17 deletions data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,76 @@ import dev.forkhandles.values.ValueFactory
*/
@Suppress("UNCHECKED_CAST")
abstract class DataContainer<CONTENT>(
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<CONTENT>.(String) -> Boolean = { existsFn(data, it) }
private val get: DataContainer<CONTENT>.(String) -> Any? = { getFn(data, it) }

fun <OUT> field() = DataProperty<DataContainer<CONTENT>, OUT>(exists, get)
//
// CORE FUNCTIONS - these are the only ones that should call the defined mappings
//

fun <OUT : Any?, NEXT> field(mapFn: (OUT) -> NEXT) = DataProperty<DataContainer<CONTENT>, NEXT>(exists) {
(get(it) as OUT)?.let(mapFn)
}
fun <OUT : Any?, NEXT> field(mapInFn: (OUT) -> NEXT, mapOutFn: (NEXT) -> OUT?) =
property<NEXT, OUT, OUT>(mapInFn, mapOutFn)

fun <OUT : DataContainer<CONTENT>?> obj(mapInFn: (CONTENT) -> OUT, mapOutFn: (OUT) -> CONTENT?) =
property<OUT, CONTENT, CONTENT>(mapInFn, mapOutFn)

fun <OUT, IN> list(mapInFn: (IN) -> OUT, mapOutFn: (OUT) -> IN?) =
property<List<OUT>, List<IN>, List<IN>>({ it.map(mapInFn) }, { it.mapNotNull(mapOutFn) })

//
// "PRE MAPPED" FUNCTIONS
//

fun <OUT> field() = field<OUT, OUT>({ it }, { it })

fun <OUT> list() = list<OUT, OUT>({ it }, { it })

fun <OUT, NEXT> field(mapInFn: (OUT) -> NEXT) = field(mapInFn) { error("no outbound mapping defined") }

// fun <IN, OUT> list(mapInFn: (IN) -> OUT) =
// list(mapInFn) { error("no outbound mapping defined") }
//

fun <IN : Any, OUT : Value<IN>, OUT2 : OUT?> field(factory: ValueFactory<OUT, IN>) = field(factory::of)
fun <OUT : DataContainer<CONTENT>?> list(mapInFn: (CONTENT) -> OUT) =
list(mapInFn) { it?.data }

fun <IN, OUT> list(mapFn: (IN) -> OUT) =
DataProperty<DataContainer<CONTENT>, List<OUT>>(exists) { get(it)?.let { (it as List<IN>).map(mapFn) } }
fun <OUT : DataContainer<CONTENT>?> obj(mapInFn: (CONTENT) -> OUT) =
obj(mapInFn) { it?.data }

fun <IN : Any, OUT : Value<IN>> list(factory: ValueFactory<OUT, IN>) = list(factory::of)
//
// VALUES4K functions
//

fun <IN : Any, OUT : Value<IN>> field(factory: ValueFactory<OUT, IN>) =
field(factory::of) { it.value }

fun <IN : Any, OUT : Value<IN>> list(factory: ValueFactory<OUT, IN>) =
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 <OUT> list() = list<OUT, OUT> { it }
override fun hashCode() = data?.hashCode() ?: 0

fun <OUT : DataContainer<CONTENT>?> obj(mapFn: (CONTENT) -> OUT) =
DataProperty<DataContainer<CONTENT>, OUT>(exists) { (get(it) as CONTENT)?.let(mapFn) }
override fun toString() = data.toString()

fun obj() = DataProperty<DataContainer<CONTENT>, CONTENT>(exists) { get(it) as CONTENT? }
private fun <IN, OUT : Any?, OUT2> property(mapInFn: (OUT) -> IN, mapOutFn: (IN) -> OUT2?) =
DataProperty<DataContainer<CONTENT>, IN>(
{ existsFn(data, it) },
{ getFn(data, it)?.let { it as OUT }?.let(mapInFn) },
{ name, value -> setFn(data, name, (value as IN?)?.let(mapOutFn)) }
)
}
10 changes: 7 additions & 3 deletions data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt
Original file line number Diff line number Diff line change
@@ -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<IN, OUT : Any?>(
private val existsFn: IN.(String) -> Boolean,
private val getFn: IN.(String) -> Any?
) : ReadOnlyProperty<IN, OUT> {
private val getFn: IN.(String) -> Any?,
private val setFn: IN.(String, Any?) -> Unit
) : ReadWriteProperty<IN, OUT> {
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: IN, property: KProperty<*>): OUT {
val result = thisRef.getFn(property.name)
Expand All @@ -18,10 +19,13 @@ open class DataProperty<IN, OUT : Any?>(
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)
}

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,7 +18,8 @@ abstract class JacksonDataContainer(input: JsonNode) :
DataContainer<JsonNode>(
input,
{ content, it -> content.has(it) },
{ content, it -> content[it]?.let(Companion::nodeToValue) }
{ content, it -> content[it]?.let(Companion::nodeToValue) },
{ _, _, _ -> TODO() }
) {

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package dev.forkhandles.data

import kotlin.collections.Map

/**
* Map-based implementation of the DataContainer
*/
abstract class MapDataContainer(input: Map<String, Any?>) :
DataContainer<Map<String, Any?>>(input, { content, it -> content.containsKey(it) }, { content, it -> content[it] })


DataContainer<MutableMap<String, Any?>>(input.toMutableMap(), { content, it -> content.containsKey(it) },
{ content, it -> content[it] },
{ map, name, value -> map[name] = value }
)
145 changes: 116 additions & 29 deletions data4k/src/test/kotlin/dev/forkhandles/lens/DataContainerContract.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T : SubClassFields> {
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<String>
val listSubClassField: List<SubClassFields>
val listIntsField: List<Int>
val listValueField: List<MyType>
var listField: List<String>
var listSubClassField: List<T>
var listIntsField: List<Int>
var listValueField: List<MyType>

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<String>?
}

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>(::MyType)
}

abstract class DataContainerContract {
abstract class DataContainerContract<T : SubClassFields> {

abstract fun container(input: Map<String, Any?>): MainClassFields
abstract fun container(input: Map<String, Any?>): MainClassFields<T>
abstract fun subContainer(input: Map<String, Any?>): T

@Test
fun `can get primitive values from properties`() {
fun `can read primitives values`() {
val input = container(
mapOf(
"stringField" to "string",
Expand Down Expand Up @@ -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(
Expand All @@ -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),
Expand All @@ -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 <T> expectSetWorks(prop: KMutableProperty0<T>, value: T) {
prop.set(value)
expectThat(prop.get()).isEqualTo(value)
}

}
Loading

0 comments on commit c6be55e

Please sign in to comment.