From 0e75eec59c47fa3ffcf0397436ca80387a032042 Mon Sep 17 00:00:00 2001 From: David Denton Date: Tue, 2 Jan 2024 23:27:07 +0000 Subject: [PATCH] added len4k --- lens4k/build.gradle | 3 +- .../lens/{AbstractLensProp.kt => LensProp.kt} | 2 +- .../kotlin/dev/forkhandles/lens/MapWrapper.kt | 32 ++++++---- .../forkhandles/lens/JacksonWrapperTest.kt | 64 +++++++++++++++++++ 4 files changed, 88 insertions(+), 13 deletions(-) rename lens4k/src/main/kotlin/dev/forkhandles/lens/{AbstractLensProp.kt => LensProp.kt} (95%) create mode 100644 lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt diff --git a/lens4k/build.gradle b/lens4k/build.gradle index 610f713..00691af 100644 --- a/lens4k/build.gradle +++ b/lens4k/build.gradle @@ -2,5 +2,6 @@ description = 'ForkHandles simple lens library' dependencies { api("org.jetbrains.kotlin:kotlin-reflect:_") - testImplementation("io.strikt:strikt-jvm:0.34.1") + api("com.fasterxml.jackson.core:jackson-databind:2.16.1") + testImplementation("io.strikt:strikt-jvm:0.34.1") } diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/AbstractLensProp.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt similarity index 95% rename from lens4k/src/main/kotlin/dev/forkhandles/lens/AbstractLensProp.kt rename to lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt index c2f1449..403b3b1 100644 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/AbstractLensProp.kt +++ b/lens4k/src/main/kotlin/dev/forkhandles/lens/LensProp.kt @@ -4,7 +4,7 @@ import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty import kotlin.reflect.jvm.jvmErasure -abstract class AbstractLensProp( +abstract class LensProp( private val existsFn: IN.(String) -> Boolean, private val getFn: IN.(String) -> Any? ) : ReadOnlyProperty { diff --git a/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt b/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt index f8b986e..8f16337 100644 --- a/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt +++ b/lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt @@ -1,17 +1,27 @@ package dev.forkhandles.lens +import com.fasterxml.jackson.databind.JsonNode + @Suppress("UNCHECKED_CAST") -abstract class MapWrapper(private val map: Map) { - class Field : AbstractLensProp({ map.containsKey(it) }, { map[it] }) - class ListField(mapFn: (IN) -> OUT) : AbstractLensProp>( - { map.containsKey(it) }, - { (map[it] as List).map(mapFn) } - ) +abstract class AbstractWrapper( + private val contents: CONTENT, + existsFn: CONTENT.(String) -> Boolean, + getFn: CONTENT.(String) -> Any? +) { + private val exists: AbstractWrapper.(String) -> Boolean = { contents.existsFn(it) } + private val get: AbstractWrapper.(String) -> Any? = { contents.getFn(it) } + + inner class Field : LensProp, OUT>(exists, get) - class ObjectField(wrapper: (Map) -> OUT) : - AbstractLensProp( - { map.containsKey(it) }, - { wrapper(map[it] as Map) } - ) + inner class ListField(mapFn: (IN) -> OUT) : + LensProp, List>(exists, { (get(it) as List).map(mapFn) }) + inner class ObjectField>(mapFn: (CONTENT) -> OUT) : + LensProp, OUT>(exists, { mapFn(get(it) as CONTENT) }) } + +abstract class MapWrapper(map: Map) : + AbstractWrapper>(map, { map.containsKey(it) }, { map[it] }) + +abstract class JacksonWrapper(node: JsonNode) : + AbstractWrapper(node, { node.has(it) }, { node[it] }) diff --git a/lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt b/lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt new file mode 100644 index 0000000..0a4b144 --- /dev/null +++ b/lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt @@ -0,0 +1,64 @@ +package dev.forkhandles.lens + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.api.expectThrows +import strikt.assertions.isEqualTo +import strikt.assertions.message + +class JacksonWrapperTest { + + class SubMap(node: JsonNode) : JacksonWrapper(node) { + val stringField by Field() + } + + class NodeBacked(node: JsonNode) : JacksonWrapper(node) { + val stringField by Field() + val booleanField by Field() + val intField by Field() + val longField by Field() + val decimalField by Field() + val notAStringField by Field() + val noSuchField by Field() + val listField by ListField(::SubMap) + val listField2 by ListField(Any::toString) + val objectField by ObjectField(::SubMap) + } + + @Test + fun `can get values from properties`() { + val mapBacked = NodeBacked( + ObjectMapper().valueToTree( + mapOf( + "stringField" to "string", + "booleanField" to true, + "intField" to 123, + "longField" to Long.MAX_VALUE, + "decimalField" to 1.1234, + "notAStringField" to 123, + "listField" to listOf( + mapOf("stringField" to "string1"), + mapOf("stringField" to "string2"), + ), + "listField2" to listOf("string1", "string2"), + "objectField" to mapOf( + "stringField" to "string" + ) + ) + ) + ) + + expectThat(mapBacked.stringField).isEqualTo("string") + expectThat(mapBacked.booleanField).isEqualTo(true) + expectThat(mapBacked.intField).isEqualTo(123) + expectThat(mapBacked.longField).isEqualTo(Long.MAX_VALUE) + expectThat(mapBacked.decimalField).isEqualTo(1.1234) + expectThat(mapBacked.listField.map { it.stringField }).isEqualTo(listOf("string1", "string2")) + expectThat(mapBacked.listField2).isEqualTo(listOf("string1", "string2")) + expectThat(mapBacked.objectField.stringField).isEqualTo("string") + expectThrows { mapBacked.notAStringField }.message.isEqualTo("Value for field is not a class kotlin.String but class kotlin.Int") + expectThrows { mapBacked.noSuchField }.message.isEqualTo("Field is missing") + } +}