Skip to content

Commit

Permalink
added optional support
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton committed Jan 4, 2024
1 parent ef2e3f3 commit 07d19e2
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 43 deletions.
16 changes: 7 additions & 9 deletions data4k/src/main/kotlin/dev/forkhandles/data/DataContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,21 @@ abstract class DataContainer<CONTENT>(

fun <OUT> field() = DataProperty<DataContainer<CONTENT>, OUT>(exists, get)

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

fun <IN : Any, OUT : Value<IN>> field(factory: ValueFactory<OUT, IN>) =
DataProperty<DataContainer<CONTENT>, OUT>(exists) { (get(it) as IN).let(factory::of) }
fun <IN : Any, OUT : Value<IN>> field(factory: ValueFactory<OUT, IN>) = field(factory::of)

fun <IN, OUT> list(mapFn: (IN) -> OUT) =
DataProperty<DataContainer<CONTENT>, List<OUT>>(exists) { (get(it) as List<IN>).map(mapFn) }
DataProperty<DataContainer<CONTENT>, List<OUT>>(exists) { get(it)?.let { (it as List<IN>).map(mapFn) } }

fun <IN : Any, OUT : Value<IN>> list(factory: ValueFactory<OUT, IN>) =
DataProperty<DataContainer<CONTENT>, List<OUT>>(exists) { (get(it) as List<IN>).map(factory::of) }
fun <IN : Any, OUT : Value<IN>> list(factory: ValueFactory<OUT, IN>) = list(factory::of)

fun <OUT> list() = list<OUT, OUT> { it }

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

fun obj() = DataProperty<DataContainer<CONTENT>, CONTENT>(exists) { get(it) as CONTENT }
fun obj() = DataProperty<DataContainer<CONTENT>, CONTENT>(exists) { get(it) as CONTENT? }
}
4 changes: 3 additions & 1 deletion data4k/src/main/kotlin/dev/forkhandles/data/DataProperty.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.jvmErasure

open class DataProperty<IN, OUT>(
open class DataProperty<IN, OUT : Any?>(
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 {
property.returnType.isMarkedNullable -> null as OUT
thisRef.existsFn(property.name) -> throw NoSuchElementException("Value for field <${property.name}> is null")
else -> throw NoSuchElementException("Field <${property.name}> is missing")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import strikt.assertions.isNull
import strikt.assertions.message

interface MainClassFields {
Expand All @@ -15,16 +16,22 @@ interface MainClassFields {
val longField: Long
val decimalField: Double
val notAStringField: String
val optionalField: String?
val noSuchField: String

val mappedField: Int

val listField: List<String>
val listSubClassField: List<SubClassFields>
val listIntsField: List<Int>
val listValueField: List<MyType>
val listStringsField: List<String>

val objectField: SubClassFields

val valueField: MyType
val mappedField: Int

val optionalField: String?
val optionalValueField: MyType?
val optionalObjectField: SubClassFields?
val optionalListField: List<String>?
}

interface SubClassFields {
Expand All @@ -41,49 +48,82 @@ abstract class DataContainerContract {
abstract fun container(input: Map<String, Any?>): MainClassFields

@Test
fun `can get values from properties`() {
fun `can get primitive 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,
"valueField" to 123,
"mappedField" to "123",
"listField" to listOf("hello"),
"listField" to listOf("hello"),
"listValueField" to listOf(1, 2, 3),
"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"
)

"optionalValueField" to 123,
"optionalField" to "optional",
)
)

expectThat(input.stringField).isEqualTo("string")
expectThat(input.optionalField).isEqualTo("optional")
expectThrows<NoSuchElementException> { container(mapOf()).stringField }.message.isEqualTo("Field <stringField> is missing")
expectThrows<NoSuchElementException> { input.notAStringField }.message.isEqualTo("Value for field <notAStringField> is not a class kotlin.String but class kotlin.Int")

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.valueField).isEqualTo(MyType.of(123))

expectThat(input.mappedField).isEqualTo(123)
expectThat(input.listField).isEqualTo(listOf("hello"))
expectThat(input.listIntsField).isEqualTo(listOf(1, 2, 3))
expectThat(input.listValueField).isEqualTo(listOf(1, 2, 3).map(MyType::of))
expectThat(input.listSubClassField.map { it.stringField }).isEqualTo(listOf("string1", "string2"))
expectThat(input.listStringsField).isEqualTo(listOf("string1", "string2"))
expectThat(input.valueField).isEqualTo(MyType.of(123))

expectThat(input.optionalField).isEqualTo("optional")
expectThat(container(mapOf()).optionalField).isNull()

expectThat(input.optionalValueField).isEqualTo(MyType.of(123))
expectThat(container(mapOf()).optionalValueField).isNull()
}

@Test
fun `object inputs`() {
val input = container(
mapOf(
"objectField" to mapOf(
"stringField" to "string"
),
"optionalObjectField" to mapOf(
"stringField" to "string"
)
)
)

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")

expectThat(input.optionalObjectField?.stringField).isEqualTo("string")
expectThat(container(mapOf()).optionalObjectField).isNull()
}

@Test
fun `list inputs`() {
val listInput = container(
mapOf(
"listField" to listOf("string1", "string2"),
"listIntsField" to listOf(1, 2, 3),
"listValueField" to listOf(1, 2, 3),
"listSubClassField" to listOf(
mapOf("stringField" to "string1"),
mapOf("stringField" to "string2"),
),
"optionalListField" to listOf("hello")
)
)
expectThat(listInput.listField).isEqualTo(listOf("string1", "string2"))
expectThat(listInput.listIntsField).isEqualTo(listOf(1, 2, 3))
expectThat(listInput.listValueField).isEqualTo(listOf(1, 2, 3).map(MyType::of))
expectThat(listInput.listSubClassField.map { it.stringField }).isEqualTo(listOf("string1", "string2"))

expectThat(listInput.optionalListField).isEqualTo(listOf("hello"))
expectThat(container(mapOf()).optionalListField).isNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ class JacksonDataContainerTest : DataContainerContract() {
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 listField by list<String>()
override val listIntsField by list<Int>()
override val listValueField by list(MyType)
override val objectField by obj(::SubNodeBacked)
override val valueField by field(MyType)
override val mappedField by field(String::toInt)

override val optionalListField: List<String>? by list()
override val optionalObjectField: SubClassFields? by obj(::SubNodeBacked)
override val optionalValueField: MyType? by field(MyType)
}

override fun container(input: Map<String, Any?>) = NodeBacked(ObjectMapper().valueToTree(input))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,27 @@ class MapDataContainerTest : DataContainerContract() {

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 mappedField by field(String::toInt)

override val listField by list<String>()
override val listValueField by list(MyType)
override val listSubClassField by list(::SubMap)
override val listStringsField by list(Any::toString)
override val listIntsField by list<Int>()

override val objectField by obj(::SubMap)

override val valueField by field(MyType)
override val mappedField by field(String::toInt)

override val optionalField by field<String?>()
override val optionalListField: List<String>? by list()
override val optionalObjectField: SubClassFields? by obj(::SubMap)
override val optionalValueField: MyType? by field(MyType)
}

override fun container(input: Map<String, Any?>): MainClassFields = MapBacked(input)
Expand Down

0 comments on commit 07d19e2

Please sign in to comment.