I'm trying to create a DSL for typesafe JavaScript...
# getting-started
b
I'm trying to create a DSL for typesafe JavaScript Record manipulation for a specific API, e.g.
Copy code
buildRecord {
    name = "test"
    address {
        street = "s"
    }
}
should result in
Copy code
{
  "name": "test",
  "address.street": "s"
}
looking into delegated properties, it looks like delegates can't implement any other operator funs except for setValue/getValue? e.g. this does not seem to work when address is a delegated property
Copy code
buildRecord {
    name = "test"
    address["street"] ="s"
}
is that correct?
✅ 1
m
Copy code
import kotlin.reflect.*

fun main() {
    val address by Address()
	kotlin.test.assertEquals(mapOf(), address)

    address["street"] = "foo"
    kotlin.test.assertEquals(
        mapOf<String, Any>(Pair("street", "foo")),
        address,
    )
}

class Address(val map: MutableMap<String, Any> = mutableMapOf()) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap<String, Any> {
        return this.map
    }
}
operator fun {g, s}et
should be implemented by what the delegate
getValue
method returns. Also I’m sure your question only contains a minimal relevant example and leaves out a lot of context, but unless
buildRecord
does some meaningful heavy lifting, I’d just use object literals.
Copy code
object {
    val name = "test"
    val address = object {
        val street = "s"
    }
}
b
yeah, there's some other stuff that I need like deleting properties requires assigning them to a global object
e.g. {"prop.to.delete": _del}
so I was hoping I could use unary minus for that but I don't think that's supported either
e.g.
Copy code
buildRecord {
    property = "test"
    -property
}
should result in {"property": _del}
for previous attempts I've resorted to classes that require
Copy code
buildRecord {
    property.set("test")
    property.delete()
}
the generated object is a custom patching syntax to allow concurrent writes to different attributes btw
m
{"property": _del}
I don’t create objects like this in JS, so I’m afraid I don’t understand your end goal and can’t help you much. Why don’t you just create a type that wraps
Map<JsKey, JsValue>
and define methods on it?
b
that's what I did previously
I'm basically passing a map around which records the current path and added accessors onto those to do the manipulation
I was just hoping for a way to replace .set() with = to make it more ergonomic
m
Replacing
.set()
with
=
saves 4 keystrokes. I’d think twice about whether saving 4 keystrokes is worth adding a whole lot of complexity to
buildRecord
b
you are correct, but if I having to break APIs later on because there's a workaround isn't great, so I was asking in here in advance
but your answer at least helped me to understand why it does not work
each time I'm accessing the property, it calls getValue() which means that any method defined on the delegate property can't work
e
another option for building DSLs: if the library looked like
Copy code
package com.example

internal annotation class ValueContainer

@ValueContainer
class Key(public val id: String) {
    override fun toString(): String = id
}

class RecordBuilder<V> private constructor(
    private val map: MutableMap<String, V>,
    private val path: List<Key>,
) {
    constructor() : this(mutableMapOf(), emptyList())

    fun Key.assign(value: V) {
        map[path.plus(this@assign).joinToString(".")] = value
    }

    operator fun Key.invoke(block: RecordBuilder<V>.() -> Unit) {
        RecordBuilder(map, path.plus(this@invoke)).apply(block)
    }

    fun Key.delete() {
        map.remove(path.plus(this@delete).joinToString("."))
    }

    fun toMap(): Map<String, V> = map.toMap()
}

fun <V> buildRecord(block: RecordBuilder<V>.() -> Unit): Map<String, V> = RecordBuilder<V>().apply(block).toMap()
then a consumer could use
Copy code
plugins {
    id("org.jetbrains.kotlin.plugin.assignment") version "2.4.20"
}

assignment {
    annotation("com.example.ValueContainer")
}
in its
build.gradle.kts
and then write code like
Copy code
val name = Key("name")
val address = Key("address")
val street = Key("street")

buildRecord {
    name = "test"
    address.delete()
    address {
        street = "s"
    }
}
➕ 1
b
cool never heard of this plugin; is this what gets used to let you assign abstract class vals in build.gradle.kts files?
e
yes
not just abstract class vals, but any
val
with types annotated by
@org.gradle.api.SupportsKotlinAssignmentOverloading
in
*.gradle.kts
, as
kotlin-dsl
uses the plugin and configures it for its annotation
b
thank you, gonna take alook