Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add write implementation and rename methods to cope with optional/required #55

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
This list is not intended to be all-encompassing - it will document major and breaking API changes with their rationale
when appropriate:

### v2.12.0.0
- **data4k** : [Breaking] Add support for writing to data structure, and rename methods to determine between required and optional fields

### v2.11.1.0
- **data4k** : Add support for Value classes and removed inner class structures to tidy up.

Expand Down
11 changes: 8 additions & 3 deletions data4k/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,23 @@ The library defines a `DataContainer` class and implementations for:

Support for extracting:
- primitive values
- subobjects
- sub-objects
- lists
- values4k value types

To extract data from the underlying data, define wrappers which provides access via delegated-properties:

```kotlin
class MapBacked(propertySet: Map<String, Any?>) : MapDataContainer(propertySet) {
val stringField by field<String>()
val stringField by required<String>()
val optionalStringField by optional<String>()
val listSubClassField by list(::SubMap)
val objectField by obj(::SubMap)
}

class SubMap(propertySet: Map<String, Any?>) : MapDataContainer(propertySet) {
val stringField by field<String>()
val stringField by required<String>()
var optionalStringField by optional<String>()
}

val input = MapBacked(
Expand All @@ -58,4 +60,7 @@ val input = MapBacked(

// 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

// to write into the structure, just assign to a var
input.objectField.optionalStringField = "hello"
```
96 changes: 79 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,92 @@ 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) }
/** Required **/

fun <OUT> field() = DataProperty<DataContainer<CONTENT>, OUT>(exists, get)
fun <OUT : Any?, NEXT> required(mapInFn: (OUT) -> NEXT, mapOutFn: (NEXT) -> OUT?) =
property<NEXT, OUT, OUT>(mapInFn, mapOutFn)

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

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

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

/** Optional **/

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

fun <OUT, NEXT : Any> optional(mapInFn: (OUT) -> NEXT, mapOutFn: (NEXT) -> OUT?) =
required<OUT, NEXT?>(mapInFn) { it?.let(mapOutFn) }

fun <OUT> optional() = required<OUT, OUT?>({ it }, { it })

fun <IN : Any, OUT : Value<IN>> optional(factory: ValueFactory<OUT, IN>): DataProperty<DataContainer<CONTENT>, OUT?> =
required(factory::of) { it?.value }

/** Object **/

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

fun <OUT : DataContainer<CONTENT>> obj(mapInFn: (CONTENT) -> OUT) =
obj(mapInFn) { it.data }

fun <OUT : DataContainer<CONTENT>> optionalObj(mapInFn: (CONTENT) -> OUT): DataProperty<DataContainer<CONTENT>, OUT?> =
property<OUT?, CONTENT, CONTENT>(mapInFn) { it?.data }

/** List **/

fun <IN : Any, OUT : Value<IN>, OUT2 : OUT?> field(factory: ValueFactory<OUT, IN>) = field(factory::of)
fun <OUT, IN> list(mapInFn: (IN) -> OUT, mapOutFn: (OUT) -> IN?) =
property<List<OUT>, List<IN>, List<IN>>({ it.map(mapInFn) }, { it.mapNotNull(mapOutFn) })

fun <IN, OUT> list(mapFn: (IN) -> OUT) =
DataProperty<DataContainer<CONTENT>, List<OUT>>(exists) { get(it)?.let { (it as List<IN>).map(mapFn) } }
fun <IN, OUT> list(mapInFn: (IN) -> OUT) = list(mapInFn) { error("no outbound mapping defined") }

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

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

@JvmName("listDataContainer")
fun <OUT : DataContainer<CONTENT>?> list(mapInFn: (CONTENT) -> OUT) = list(mapInFn) { it?.data }

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

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

fun <OUT> optionalList() = optionalList<OUT, OUT & Any>({ it }, { it })

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

@JvmName("optionalListDataContainer")
fun <OUT : DataContainer<CONTENT>?> optionalList(mapInFn: (CONTENT) -> OUT) = optionalList(mapInFn) { it?.data }

/** Utility **/

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 { value -> value 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,20 @@
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.BooleanNode.FALSE
import com.fasterxml.jackson.databind.node.BooleanNode.TRUE
import com.fasterxml.jackson.databind.node.DecimalNode
import com.fasterxml.jackson.databind.node.DoubleNode
import com.fasterxml.jackson.databind.node.FloatNode
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory.instance
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
import java.math.BigDecimal

/**
* Jackson JsonNode-based implementation of the DataContainer
Expand All @@ -10,21 +23,45 @@ 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) },
{ node: JsonNode, name, value ->
(node as? ObjectNode)?.also {
node.set<JsonNode>(name, value.toNode())
} ?: error("Invalid node type ${input::class.java}")
}
) {

companion object {
private fun nodeToValue(input: JsonNode): Any? = when (input) {
is BooleanNode -> input.booleanValue()
is IntNode -> input.intValue()
is LongNode -> input.longValue()
is FloatNode -> input.floatValue()
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")
else -> error("Invalid node type ${input::class.java}")
}

private fun Any?.toNode(): JsonNode? =
when (this) {
null -> NullNode.instance
is JsonNode -> this
is DataContainer<*> -> data.toNode()
is Boolean -> if (this) TRUE else FALSE
is Int -> IntNode(this)
is Long -> LongNode(this)
is Float -> FloatNode(this)
is BigDecimal -> DecimalNode(this)
is Double -> DoubleNode(this)
is String -> TextNode(this)
is Iterable<*> -> ArrayNode(instance).also {
map { if (it is JsonNode) it else it.toNode() }.forEach(it::add)
}
else -> error("Cannot set value of type ${this::class.java}")
}
}
}
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 }
)
Loading