Skip to content

Commit

Permalink
adding Jackson implementation and write (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton authored Jan 5, 2024
1 parent c6affb0 commit 4b8dd13
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 157 deletions.
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

0 comments on commit 4b8dd13

Please sign in to comment.