Kotlin DSL for collections


#1

Suppose I have Person and House classes :

data class Person(val name: String, val age: Int)
data class House(val address: String, val persons: List<Person>?)

I want to build a DSL for it . This is what I’ve done :

  data class Person(val name: String, val age: Int)

  class Persons : ArrayList<Person>() {
    fun person(p : Person) {
      add(p)
    }
  }

  data class House(val address: String, val persons: List<Person>?) {
    class Builder {
      var address = ""
      private val persons = mutableListOf<Person>()

      fun persons(block : Persons.() -> Unit) {
        persons.addAll(Persons().apply(block))
      }

      fun addList(vararg list : Person) {
        persons.addAll(list)
      }

      fun build() = House(address , persons.takeIf { it.isNotEmpty() })
    }
  }

  private fun house(block: House.Builder.() -> Unit) = House.Builder().apply(block).build()

  @Test
  fun testPersons() {
    val house = house {
      address = "Broadway 1"
      persons {
        person(Person("John" , 21))
        person(Person("Amy" , 20))
      }
    }
    println(house)
  }

  @Test
  fun testList() {
    val house = house {
      address = "Broadway 1"
      addList(Person("John" , 21) , Person("Amy" , 20))
    }
    println(house)
  }

The two tests both outputs

House(address=Broadway 1, persons=[Person(name=John, age=21), Person(name=Amy, age=20)])
House(address=Broadway 1, persons=[Person(name=John, age=21), Person(name=Amy, age=20)])

But these are not what I want .
The problem is in House.Builder’s persons() and addList() function.
They cannot achieve what I really want to do :

@Test  // this is what I really want
fun test() {
  val house = house {
    address = "Broadway 1"
    persons {
      Person("John" , 21)
      Person("Amy" , 20)
    }
  }
  println(house)  // House(address=Broadway 1, persons=null)
}

How to achieve such DSL ?

Thanks.


#2

Give your Persons class a function named Person that has the right signature you want it to have. Let it do what you want.

class Persons : ArrayList<Person>() {
    fun Person(name: String, age: Int) {
        add(Person(name, age))
    }
}

#3

The class :

class Persons : ArrayList<Person>() {
    fun Person(name: String, age: Int) {
        add(Person(name, age))
    }
}

add(Person(name, age)) is in fact calling function itself (returning Unit) , not creating a Person object.

And , a method name with first letter capitalized is not normal.

And if fun Person is renamed to fun addPerson , it still cannot do this :

  @Test
  fun test() {
    val p3 = Person("Bill", 24)

    val house = house {
      address = "Broadway 1"
      persons {
        addPerson("John" , 21)
        addPerson("Amy" , 20)
        p3 // p3 not added
      }
    }
    println(house)
  }

Maybe this is impossible in kotlin…


#4

My mistake. Within the Person function, the Person constructor needs to be called with fully qualified package name.

That’s how you specified it. You could just as well name it person() instead of Person() When it comes to DSLs, everything is allowed :wink:

Yes, you cannot make it behave like that.

What you can do without adjustments is:

val p3 = Person("Bill", 24)
val house = house {
    address = "Broadway 1"
    persons {
        this += Person("John" , 21)
        this += Person("Amy" , 20)
        this += p3
    }
}

With slight adjustment to the API, the following is possible also:

val p3 = Person("Bill", 24)
val house = house {
    address = "Broadway 1"
    persons {
        +Person("John" , 21)
        +Person("Amy" , 20)
        +p3
    }
}

class Persons : ArrayList<Person>() {
    operator fun unaryPlus(person: Person) {
        add(person)
    }
}

And this is the best you can get in Kotlin.