lapots
lapots

Reputation: 13395

Add new key-value represented by a `Pair` to a `MutableMap`

I currently have this class with dsl like building ability

class GRLMessage {
    var headerMap : MutableMap<String, String> = mutableMapOf()
    lateinit var methodType : GRLMethod
    lateinit var multipartObject : IGRLMultipart

    fun message(closure: GRLMessage.() -> Unit) : GRLMessage {
        closure()
        return this
    }

    fun method(closure: GRLMessage.() -> GRLMethod) : GRLMessage {
        methodType = closure()
        return this
    }

    fun headers(closure: GRLMessage.() -> Unit) : GRLMessage {
        closure()
        return this
    }

    fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
        var pair = closure()
        headerMap.put(pair.first, pair.second)
        return this
    }

    fun multipart(closure: GRLMessage.() -> IGRLMultipart) : GRLMessage {
        multipartObject = closure()
        return this
    }
}

And I test it like this

class GRLMessageTest {

    data class DummyMultipart(val field: String) : IGRLMultipart {
        override fun getContent() {
            this
        }
    }

    @Test fun grlMessageBuilderTest() {
        val grlMessage = GRLMessage().message {
            method { GRLMethod.POST }
            headers {
                header { Pair("contentType", "object") }
                header { Pair("objectType", "DummyMultipart") }
            }
            multipart { DummyMultipart("dummy") }
        }

        val multipart = DummyMultipart("dummy")
        val headers = mapOf(
                Pair("contentType", "object"),
                Pair("objectType", "DummyMultipart")
        )
        val method = GRLMethod.POST

        assertEquals(multipart, grlMessage.multipartObject)
        assertEquals(method, grlMessage.methodType)
        assertEquals(headers, grlMessage.headerMap)
    }
}

But despite providing

header { Pair("contentType", "object") }

I still have to evaluate closure inside header method and directly put key and value into my MutableMap

fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
    var pair = closure()
    headerMap.put(pair.first, pair.second)
    return this
}

Is there a better way adding entries to Map?

Upvotes: 1

Views: 1263

Answers (3)

Jayson Minard
Jayson Minard

Reputation: 85946

Adding an extension function makes your fluent methods more obviously fluent:

fun <T: Any> T.fluently(func: ()->Unit): T {
    return this.apply { func() } 
}

With that your fluent function is always clear about its return:

fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
    return fluently { headerMap += closure() }
}

Which is really the same as:

fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
    return this.apply { headerMap += closure() }
}

But the extension function adds a touch of readability.

Above I use the answer given in by @Ruckus for solving your specific question of adding a Pair to the headerMap. But you have other options that you might want to know about for other use cases of your DSL...

You can use let, apply or with which would allow any type of decomposition of the results of closure() call (maybe it is more complicated than Pair in the future). All of these are basically the same, minus their resulting value:

with(closure()) { headerMap.put(this.first, this.second) }
closure().apply { headerMap.put(this.first, this.second) }
closure().let { headerMap.put(it.first, it.second) }

Using let or apply is nice if you want to handle a case where closure() allows nullable return, in which case you might want to take action only if not null:

closure()?.apply { headerMap.put(this.first, this.second) }
closure()?.let { headerMap.put(it.first, it.second) }

Other notes about your code:

  • use val instead of var unless you have no other choice
  • lateinit (or the similar Delegates.notNull()) seem dangerous to use in an uncontrolled lifecycle where there is no guarantee it will be completed, because the error message will be confusing and happen at some unexpected time in the future. There are likely other ways to solve this with a DSL that chains calls to create more of a multi-step grammar
  • You can shorten code by only having types on one side of the assignment, for example:

    val myMap = mutableMapOf<String, String>()
    

    instead of

    var myMap : MutableMap<String, String> = mutableMapOf()
    

Upvotes: 2

Ruckus T-Boom
Ruckus T-Boom

Reputation: 4786

Does your headerMap need to be a var? If not, you can change it to a val and use headerMap += closure().

Upvotes: 3

lapots
lapots

Reputation: 13395

Well for now as a solution I created extension for MutableMap

fun MutableMap<String, String>.put(pair : Pair<String, String>) {
    this.put(pair.first, pair.second)
}

Which allowed me to write like this

fun header(closure: GRLMessage.() -> Pair<String, String>) : GRLMessage {
    headerMap.put(closure())
    return this
}

Upvotes: 0

Related Questions