Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin delegated property by map throwing NoSuchElementException if not present in map

Tags:

kotlin

I have a kotlin object defined as such:

data class UserUpdateRequest(val map: Map<String, Any?>) {
    @get:Email
    val email: String? by map
    val firstName: String? by map
    val lastName: String? by map
}

So that works just fine, so the problem I'm having is that the properties are nullable, and when I access one, say by doing instance.email it throws a NoSuchElementException if said property is not set in the map.

Instead, it'd be more convenient if it returned null, since it's optional/nullable. Is there any way to achieve this without writing my own delegate?

like image 475
arg20 Avatar asked Apr 22 '18 19:04

arg20


2 Answers

You can basically use the .withDefault { ... } extension that wraps a Map for delegation, so that it executes the lambda to calculate a value on absent key:

data class UserUpdateRequest(val map: Map<String, Any?>) {
    private val defaultMap = map.withDefault { null }

    @get:Email
    val email: String? by defaultMap
    val firstName: String? by defaultMap
    val lastName: String? by defaultMap
}

Note that simple defaultMap.get(key) and defaultMap[key] queries are not handled by this wrapper, it only affects the defaulMap.getValue(key) calls (which also happen to be used by the delegation implementation).

like image 156
hotkey Avatar answered Oct 21 '22 04:10

hotkey


Here is a delegate implementation, if you want to have more control over what the delegate does and understand how it works.

First of a demonstration how it could be used in your scenario:

data class UserUpdateRequest(val map: Map<String, Any?>) {

        var email: String? by valueFromMap(map = map)
        var firstName: String? by valueFromMap(map = map)
        var lastName: String? by valueFromMap(map = map)
        var age: Int? by valueFromMap(map = map)
    }

    val myInitialMap = mutableMapOf<String,Any?>()
    myInitialMap["email"] = "[email protected]"
    myInitialMap["age"] = 34

    val request = UserUpdateRequest(myInitialMap)
    request.lastName = "Genericname"

    println("email: ${request.email}, firstNme: ${request.firstName}, lastName: ${request.lastName}, age: ${request.age}")

    request.lastName = null

    println("lastName after setting to null: ${request.lastName}")

This will print:
email: [email protected], firstName: null, lastName: Genericname, age: 34
lastName after setting to null: null

So the variables are set to the initialMap's values if they exist and the value's type matches the variable type. Else the variable is set to null.

The Delegate Implementation

fun <R>valueFromMap(key: (KProperty<*>) -> String = KProperty<*>::name,
                map: Map<String, Any?>): ReadWriteProperty<Any, R?>{

    val myMap = map.toMutableMap()

    return object : ReadWriteProperty<Any, R?> {

    override fun getValue(thisRef: Any, property: KProperty<*>): R? {
        return (myMap.getOrDefault(key(property), null) as? R)?: return null
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: R?) {
        if(value == null){
            myMap.remove(key(property))
        } else {
            myMap[key(property)] = value
        }
    }
}

}

So valueFromMap(map = map) is a function that returns a ReadWriteProperty for a val/var(KProperty) that uses the KPproperty's name as key to look for a value in the map, passed as argument.
So when we call:

request.lastName = "Genericname"

In the above example, actually we call .

initialMap["lastName"] = "Genericname"

If the variable name and the corresponding key differ you could even pass your key as argument.
With this approach you're able to handle the value access on your own in the getValue and setValue functions.

Hope this helps anybody.

like image 2
Erik Schmidt Avatar answered Oct 21 '22 04:10

Erik Schmidt