Skip to content

Commit

Permalink
added len4k
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton committed Jan 3, 2024
1 parent 7158fcc commit 0e75eec
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 13 deletions.
3 changes: 2 additions & 1 deletion lens4k/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.jvmErasure

abstract class AbstractLensProp<IN, OUT>(
abstract class LensProp<IN, OUT>(
private val existsFn: IN.(String) -> Boolean,
private val getFn: IN.(String) -> Any?
) : ReadOnlyProperty<IN, OUT> {
Expand Down
32 changes: 21 additions & 11 deletions lens4k/src/main/kotlin/dev/forkhandles/lens/MapWrapper.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package dev.forkhandles.lens

import com.fasterxml.jackson.databind.JsonNode

@Suppress("UNCHECKED_CAST")
abstract class MapWrapper(private val map: Map<String, Any?>) {
class Field<OUT> : AbstractLensProp<MapWrapper, OUT>({ map.containsKey(it) }, { map[it] })
class ListField<IN : Any, OUT>(mapFn: (IN) -> OUT) : AbstractLensProp<MapWrapper, List<OUT>>(
{ map.containsKey(it) },
{ (map[it] as List<IN>).map(mapFn) }
)
abstract class AbstractWrapper<CONTENT>(
private val contents: CONTENT,
existsFn: CONTENT.(String) -> Boolean,
getFn: CONTENT.(String) -> Any?
) {
private val exists: AbstractWrapper<CONTENT>.(String) -> Boolean = { contents.existsFn(it) }
private val get: AbstractWrapper<CONTENT>.(String) -> Any? = { contents.getFn(it) }

inner class Field<OUT> : LensProp<AbstractWrapper<CONTENT>, OUT>(exists, get)

class ObjectField<OUT : MapWrapper>(wrapper: (Map<String, Any?>) -> OUT) :
AbstractLensProp<MapWrapper, OUT>(
{ map.containsKey(it) },
{ wrapper(map[it] as Map<String, Any?>) }
)
inner class ListField<IN : Any, OUT>(mapFn: (IN) -> OUT) :
LensProp<AbstractWrapper<CONTENT>, List<OUT>>(exists, { (get(it) as List<IN>).map(mapFn) })

inner class ObjectField<OUT : AbstractWrapper<CONTENT>>(mapFn: (CONTENT) -> OUT) :
LensProp<AbstractWrapper<CONTENT>, OUT>(exists, { mapFn(get(it) as CONTENT) })
}

abstract class MapWrapper(map: Map<String, Any?>) :
AbstractWrapper<Map<String, Any?>>(map, { map.containsKey(it) }, { map[it] })

abstract class JacksonWrapper(node: JsonNode) :
AbstractWrapper<JsonNode>(node, { node.has(it) }, { node[it] })
64 changes: 64 additions & 0 deletions lens4k/src/test/kotlin/dev/forkhandles/lens/JacksonWrapperTest.kt
Original file line number Diff line number Diff line change
@@ -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<String>()
}

class NodeBacked(node: JsonNode) : JacksonWrapper(node) {
val stringField by Field<String>()
val booleanField by Field<Boolean>()
val intField by Field<Int>()
val longField by Field<Long>()
val decimalField by Field<Double>()
val notAStringField by Field<String>()
val noSuchField by Field<String>()
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<NoSuchElementException> { mapBacked.notAStringField }.message.isEqualTo("Value for field <notAStringField> is not a class kotlin.String but class kotlin.Int")
expectThrows<NoSuchElementException> { mapBacked.noSuchField }.message.isEqualTo("Field <noSuchField> is missing")
}
}

0 comments on commit 0e75eec

Please sign in to comment.