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

Data4k - new module #54

Merged
merged 9 commits into from
Jan 3, 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
56 changes: 56 additions & 0 deletions data4k/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Data4k

<a href="https://mvnrepository.com/artifact/dev.forkhandles"><img alt="Download" src="https://img.shields.io/maven-central/v/dev.forkhandles/forkhandles-bom"></a>
[![.github/workflows/build.yaml](https://github.com/fork-handles/forkhandles/actions/workflows/build.yaml/badge.svg)](https://github.com/fork-handles/forkhandles/actions/workflows/build.yaml)
<a href="https://codecov.io/gh/fork-handles/forkhandles"><img src="https://codecov.io/gh/fork-handles/forkhandles/branch/trunk/graph/badge.svg"/></a>
<a href="http//www.apache.org/licenses/LICENSE-2.0"><img alt="GitHub license" src="https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat"></a>
<a href="https://codebeat.co/projects/github-com-fork-handles-forkhandles-trunk"><img alt="codebeat badge" src="https://codebeat.co/badges/5b369ed4-af27-46f4-ad9c-a307d900617e"></a>

Library to make working with Data-Oriented programming in Kotlin easier, to extract values from dynamic data structures such as Maps.

## Installation

In Gradle, install the ForkHandles BOM and then this module in the dependency block:

```kotlin
implementation(platform("dev.forkhandles:forkhandles-bom:X.Y.Z"))
implementation("dev.forkhandles:data4k")
```

## Usage

The library defines a DataContainer class and implementations for:

- Map<String, Any?>
- Jackson JSON Node

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 listSubClassField by list(::SubMap)
val objectField by obj(::SubMap)
}

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

val input = MapBacked(
mapOf(
"stringField" to "string",
"listSubClassField" to listOf(
mapOf("stringField" to "string1"),
mapOf("stringField" to "string2"),
),
"listStringsField" to listOf("string1", "string2"),
"objectField" to mapOf(
"stringField" to "string"
)
)
)

// 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
```
8 changes: 8 additions & 0 deletions data4k/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description = 'ForkHandles data-oriented programming library'

dependencies {
api("org.jetbrains.kotlin:kotlin-reflect:_")
compileOnly("com.fasterxml.jackson.core:jackson-databind:_")
testImplementation("com.fasterxml.jackson.core:jackson-databind:_")
testImplementation("io.strikt:strikt-jvm:_")
}
23 changes: 23 additions & 0 deletions data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.forkhandles.data

/**
* Superclass for all container implementations. Defines the delegate property classes to extract data from the
* underlying data structure.
*/
@Suppress("UNCHECKED_CAST", "ClassName")
abstract class DataContainer<CONTENT>(
protected val data: CONTENT,
existsFn: (CONTENT, String) -> Boolean,
getFn: (CONTENT, String) -> Any?
) {
private val exists: DataContainer<CONTENT>.(String) -> Boolean = { existsFn(data, it) }
private val get: DataContainer<CONTENT>.(String) -> Any? = { getFn(data, it) }

inner class field<OUT> : DataProperty<DataContainer<CONTENT>, OUT>(exists, get)

inner class list<IN : Any, OUT>(mapFn: (IN) -> OUT) :
DataProperty<DataContainer<CONTENT>, List<OUT>>(exists, { (get(it) as List<IN>).map(mapFn) })

inner class obj<OUT : DataContainer<CONTENT>>(mapFn: (CONTENT) -> OUT) :
DataProperty<DataContainer<CONTENT>, OUT>(exists, { mapFn(get(it) as CONTENT) })
}
25 changes: 25 additions & 0 deletions data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.forkhandles.data

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.jvmErasure

abstract class DataProperty<IN, OUT>(
private val existsFn: IN.(String) -> Boolean,
private val getFn: IN.(String) -> Any?
) : ReadOnlyProperty<IN, OUT> {
@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: IN, property: KProperty<*>): OUT {
val result = thisRef.getFn(property.name)
return when {
result == null -> when {
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}")
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.forkhandles.data

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.*

/**
* Jackson JsonNode-based implementation of the DataContainer
*/
abstract class JacksonDataContainer(input: JsonNode) :
DataContainer<JsonNode>(
input,
{ content, it -> content.has(it) },
{ content, it -> content[it]?.let(Companion::nodeToValue) }
) {

companion object {
private fun nodeToValue(input: JsonNode): Any? = when (input) {
is BooleanNode -> input.booleanValue()
is IntNode -> input.intValue()
is LongNode -> input.longValue()
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")
}
}
}
11 changes: 11 additions & 0 deletions data4k/src/main/kotlin/dev/forkhandles/data/MapDataContainer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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] })


Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.forkhandles.lens

import org.junit.jupiter.api.Test
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import strikt.assertions.message

interface MainClassFields {
val stringField: String
val booleanField: Boolean
val intField: Int
val longField: Long
val decimalField: Double
val notAStringField: String
val optionalField: String?
val noSuchField: String
val listSubClassField: List<SubClassFields>
val listIntsField: List<Int>
val listStringsField: List<String>
val objectField: SubClassFields
}

interface SubClassFields {
val stringField: String
val noSuchField: String
}

abstract class DataContainerContract {

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

@Test
fun `can get values from properties`() {
val input = container(
mapOf(
"stringField" to "string",
"optionalField" to "optional",
"booleanField" to true,
"intField" to 123,
"longField" to Long.MAX_VALUE,
"decimalField" to 1.1234,
"notAStringField" to 123,
"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"
)
)
)

expectThat(input.stringField).isEqualTo("string")
expectThat(input.optionalField).isEqualTo("optional")
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.listIntsField).isEqualTo(listOf(1, 2, 3))
expectThat(input.listSubClassField.map { it.stringField }).isEqualTo(listOf("string1", "string2"))
expectThat(input.listStringsField).isEqualTo(listOf("string1", "string2"))
expectThat(input.objectField.stringField).isEqualTo("string")
expectThrows<NoSuchElementException> { input.notAStringField }.message.isEqualTo("Value for field <notAStringField> is not a class kotlin.String but class kotlin.Int")
expectThrows<NoSuchElementException> { input.noSuchField }.message.isEqualTo("Field <noSuchField> is missing")
expectThrows<NoSuchElementException> { input.objectField.noSuchField }.message.isEqualTo("Field <noSuchField> is missing")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.forkhandles.lens

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import dev.forkhandles.data.JacksonDataContainer

class JacksonDataContainerTest : DataContainerContract() {

class SubNodeBacked(node: JsonNode) : JacksonDataContainer(node), SubClassFields {
override val stringField by field<String>()
override val noSuchField by field<String>()
}

class NodeBacked(node: JsonNode) : JacksonDataContainer(node), MainClassFields {
override val stringField by field<String>()
override val optionalField by field<String?>()
override val booleanField by field<Boolean>()
override val intField by field<Int>()
override val longField by field<Long>()
override val decimalField by field<Double>()
override val notAStringField by field<String>()
override val noSuchField by field<String>()
override val listSubClassField by list(::SubNodeBacked)
override val listStringsField by list(Any::toString)
override val listIntsField by list<Any, Int> { it as Int }
override val objectField by obj(::SubNodeBacked)
}

override fun container(input: Map<String, Any?>) = NodeBacked(ObjectMapper().valueToTree(input))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.forkhandles.lens

import dev.forkhandles.data.MapDataContainer

class MapDataContainerTest : DataContainerContract() {

class SubMap(propertySet: Map<String, Any?>) : MapDataContainer(propertySet), SubClassFields {
override val stringField by field<String>()
override val noSuchField by field<String>()
}

class MapBacked(propertySet: Map<String, Any?>) : MapDataContainer(propertySet), MainClassFields {
override val stringField by field<String>()
override val optionalField by field<String?>()
override val booleanField by field<Boolean>()
override val intField by field<Int>()
override val longField by field<Long>()
override val decimalField by field<Double>()
override val notAStringField by field<String>()
override val noSuchField by field<String>()
override val listSubClassField by list(::SubMap)
override val listStringsField by list(Any::toString)
override val listIntsField by list<Any, Int> { it as Int }
override val objectField by obj(::SubMap)
}

override fun container(input: Map<String, Any?>): MainClassFields = MapBacked(input)
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ fun String.includeModule(dir: String) {
include("forkhandles-bom")

include("bunting4k")
include("data4k")
include("fabrikate4k")
include("lens4k")
include("mock4k")
include("parser4k")
include("partial4k")
Expand Down
10 changes: 10 additions & 0 deletions versions.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ plugin.io.codearte.nexus-staging=0.30.0

plugin.me.champeau.jmh=0.7.2

version.com.fasterxml.jackson.core..jackson-databind=2.16.1

version.com.natpryce..hamkrest=1.8.0.1

version.junit.jupiter=5.10.1
Expand All @@ -25,4 +27,12 @@ version.kotlinx.coroutines=1.7.3

version.kotlinx.serialization=1.6.2

version.strikt=0.34.1

## unused
version.org.openjdk.jmh..jmh-generator-bytecode=1.36

## unused
version.org.openjdk.jmh..jmh-core=1.36

version.org.kt3k.gradle.plugin..coveralls-gradle-plugin=2.8.3