Files
amethyst/.claude/skills/kotlin-expert/references/dsl-builder-examples.md
T
2025-12-30 15:18:48 +02:00

14 KiB

DSL Builder Examples

Type-safe fluent APIs and DSL patterns from the codebase.

Table of Contents


TagArrayBuilder Pattern

Core Implementation

File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt:23-91

class TagArrayBuilder<T : IEvent> {
    private val tagList = mutableMapOf<String, MutableList<Tag>>()

    fun remove(tagName: String): TagArrayBuilder<T> {
        tagList.remove(tagName)
        return this  // Method chaining
    }

    fun remove(tagName: String, tagValue: String): TagArrayBuilder<T> {
        tagList[tagName]?.removeAll { it.valueOrNull() == tagValue }
        if (tagList[tagName]?.isEmpty() == true) {
            tagList.remove(tagName)
        }
        return this
    }

    fun removeIf(
        predicate: (Tag, Tag) -> Boolean,
        toCompare: Tag
    ): TagArrayBuilder<T> {
        val tagName = toCompare.nameOrNull() ?: return this
        tagList[tagName]?.removeAll { predicate(it, toCompare) }
        if (tagList[tagName]?.isEmpty() == true) {
            tagList.remove(tagName)
        }
        return this
    }

    fun add(tag: Array<String>): TagArrayBuilder<T> {
        if (tag.isEmpty() || tag[0].isEmpty()) return this
        tagList.getOrPut(tag[0], ::mutableListOf).add(tag)
        return this
    }

    fun addFirst(tag: Array<String>): TagArrayBuilder<T> {
        if (tag.isEmpty() || tag[0].isEmpty()) return this
        tagList.getOrPut(tag[0], ::mutableListOf).add(0, tag)
        return this
    }

    fun addUnique(tag: Array<String>): TagArrayBuilder<T> {
        if (tag.isEmpty() || tag[0].isEmpty()) return this
        tagList[tag[0]] = mutableListOf(tag)  // Replace existing
        return this
    }

    fun addAll(tag: List<Array<String>>): TagArrayBuilder<T> {
        tag.forEach(::add)
        return this
    }

    fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray()

    fun build() = toTypedArray()
}

// Inline DSL function with lambda receiver
inline fun <T : Event> tagArray(
    initializer: TagArrayBuilder<T>.() -> Unit = {}
): TagArray = TagArrayBuilder<T>().apply(initializer).build()

Usage Examples

Basic usage:

val tags = tagArray<TextNoteEvent> {
    add(arrayOf("e", eventId, relay, "reply"))
    add(arrayOf("p", pubkey))
    add(arrayOf("t", "bitcoin"))
}

Advanced patterns:

// Remove and add
val tags = tagArray<TextNoteEvent> {
    addAll(existingTags)
    remove("a")  // Remove all address tags
    addUnique(arrayOf("client", "Amethyst"))  // Replace client tag
}

// Conditional building
val tags = tagArray<TextNoteEvent> {
    add(arrayOf("e", rootId, "", "root"))

    if (replyToId != null) {
        add(arrayOf("e", replyToId, "", "reply"))
    }

    mentionedPubkeys.forEach { pubkey ->
        add(arrayOf("p", pubkey))
    }

    hashtags.forEach { tag ->
        add(arrayOf("t", tag.lowercase()))
    }
}

// Custom predicate removal
val tags = tagArray<TextNoteEvent> {
    addAll(originalTags)
    removeIf(
        predicate = { tag, compare -> tag[1] == compare[1] },
        toCompare = arrayOf("e", eventIdToRemove)
    )
}

Builder Variations

PrivateTagArrayBuilder

File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt

class PrivateTagArrayBuilder {
    private val builder = TagArrayBuilder<Event>()

    fun add(tag: PrivateTag): PrivateTagArrayBuilder {
        builder.add(tag.toArray())
        return this
    }

    fun addAll(tags: List<PrivateTag>): PrivateTagArrayBuilder {
        tags.forEach { add(it) }
        return this
    }

    fun build(): Array<Array<String>> = builder.build()
}

// DSL function
inline fun privateTagArray(
    initializer: PrivateTagArrayBuilder.() -> Unit
): Array<Array<String>> = PrivateTagArrayBuilder().apply(initializer).build()

Usage:

val privateTags = privateTagArray {
    add(PrivateTag.Event(eventId, marker = "bookmark"))
    add(PrivateTag.Profile(pubkey))
    addAll(existingPrivateTags)
}

TlvBuilder

File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt

class TlvBuilder {
    private val entries = mutableListOf<TlvEntry>()

    fun add(type: TlvType, value: ByteArray): TlvBuilder {
        entries.add(TlvEntry(type, value))
        return this
    }

    fun addRelay(relay: String): TlvBuilder {
        add(TlvType.Relay, relay.encodeToByteArray())
        return this
    }

    fun addAuthor(pubkey: ByteArray): TlvBuilder {
        add(TlvType.Author, pubkey)
        return this
    }

    fun addKind(kind: Int): TlvBuilder {
        add(TlvType.Kind, kind.toByteArray())
        return this
    }

    fun build(): ByteArray {
        return entries.flatMap { it.encode() }.toByteArray()
    }
}

fun tlv(init: TlvBuilder.() -> Unit): ByteArray =
    TlvBuilder().apply(init).build()

Usage:

val tlvData = tlv {
    addAuthor(pubkeyBytes)
    addRelay("wss://relay.damus.io")
    addKind(1)
}

MapOfSetBuilder

File: quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt

class MapOfSetBuilder<K, V> {
    private val map = mutableMapOf<K, MutableSet<V>>()

    fun add(key: K, value: V): MapOfSetBuilder<K, V> {
        map.getOrPut(key) { mutableSetOf() }.add(value)
        return this
    }

    fun addAll(key: K, values: Collection<V>): MapOfSetBuilder<K, V> {
        map.getOrPut(key) { mutableSetOf() }.addAll(values)
        return this
    }

    fun remove(key: K, value: V): MapOfSetBuilder<K, V> {
        map[key]?.remove(value)
        if (map[key]?.isEmpty() == true) {
            map.remove(key)
        }
        return this
    }

    fun build(): Map<K, Set<V>> = map.mapValues { it.value.toSet() }
}

inline fun <K, V> mapOfSets(
    init: MapOfSetBuilder<K, V>.() -> Unit
): Map<K, Set<V>> = MapOfSetBuilder<K, V>().apply(init).build()

Usage:

val relayMap = mapOfSets<String, EventId> {
    add("wss://relay1.com", eventId1)
    add("wss://relay1.com", eventId2)
    add("wss://relay2.com", eventId3)
}
// Result: {"wss://relay1.com": [eventId1, eventId2], "wss://relay2.com": [eventId3]}

DSL Principles

1. Lambda with Receiver

Mental model: Lambda receiver makes this refer to builder instance inside lambda.

// Without receiver
fun buildTags(config: (TagArrayBuilder<Event>) -> Unit) {
    val builder = TagArrayBuilder<Event>()
    config(builder)  // Must pass builder explicitly
    builder.build()
}

buildTags { builder ->
    builder.add(...)  // Verbose
}

// With receiver
inline fun buildTags(config: TagArrayBuilder<Event>.() -> Unit) {
    TagArrayBuilder<Event>().apply(config).build()
}

buildTags {
    add(...)  // Clean - 'this' is builder
}

2. Method Chaining

Pattern: Return this from mutator methods.

class Builder {
    private var value: String = ""

    fun setValue(v: String): Builder {
        value = v
        return this  // Enable chaining
    }

    fun append(s: String): Builder {
        value += s
        return this
    }

    fun build(): String = value
}

// Usage
val result = Builder()
    .setValue("Hello")
    .append(" ")
    .append("World")
    .build()

3. Inline for Performance

Why inline:

  • Eliminates lambda allocation
  • Allows reified type parameters
  • Better for hot paths (frequently called)
// NOT inline - lambda object created each call
fun <T> myDsl(init: Builder<T>.() -> Unit): Result<T> {
    return Builder<T>().apply(init).build()
}

// Inline - lambda code inlined at call site
inline fun <T> myDsl(init: Builder<T>.() -> Unit): Result<T> {
    return Builder<T>().apply(init).build()
}

4. Type Safety

Use generics for compile-time safety:

// Type-safe builder
class EventBuilder<T : Event> {
    fun addTag(tag: Tag<T>): EventBuilder<T> {  // Only accepts tags for this event type
        tags.add(tag)
        return this
    }
}

// Usage
val textNote = EventBuilder<TextNoteEvent>()
    .addTag(TextNoteTag.Subject("Hello"))  // OK
    // .addTag(ChannelTag.Name("test"))     // Compile error!
    .build()

Creating Custom DSLs

Pattern: Simple Builder DSL

class QueryBuilder {
    private val filters = mutableListOf<String>()
    private var limit: Int? = null
    private var offset: Int? = null

    fun filter(field: String, value: String): QueryBuilder {
        filters.add("$field:$value")
        return this
    }

    fun limit(n: Int): QueryBuilder {
        limit = n
        return this
    }

    fun offset(n: Int): QueryBuilder {
        offset = n
        return this
    }

    fun build(): String {
        val parts = mutableListOf<String>()
        if (filters.isNotEmpty()) {
            parts.add(filters.joinToString(" AND "))
        }
        if (limit != null) {
            parts.add("LIMIT $limit")
        }
        if (offset != null) {
            parts.add("OFFSET $offset")
        }
        return parts.joinToString(" ")
    }
}

inline fun query(init: QueryBuilder.() -> Unit): String =
    QueryBuilder().apply(init).build()

// Usage
val sql = query {
    filter("status", "active")
    filter("age", ">18")
    limit(10)
    offset(20)
}
// Result: "status:active AND age:>18 LIMIT 10 OFFSET 20"

Pattern: Nested Builders

class FilterBuilder {
    private val conditions = mutableListOf<String>()

    fun equals(field: String, value: String) {
        conditions.add("$field = '$value'")
    }

    fun greaterThan(field: String, value: Int) {
        conditions.add("$field > $value")
    }

    fun build(): String = conditions.joinToString(" AND ")
}

class QueryBuilder {
    private var filterClause: String = ""
    private var selectClause: String = "*"

    fun select(vararg fields: String): QueryBuilder {
        selectClause = fields.joinToString(", ")
        return this
    }

    fun where(init: FilterBuilder.() -> Unit): QueryBuilder {
        filterClause = FilterBuilder().apply(init).build()
        return this
    }

    fun build(): String {
        return "SELECT $selectClause WHERE $filterClause"
    }
}

inline fun query(init: QueryBuilder.() -> Unit): String =
    QueryBuilder().apply(init).build()

// Usage
val sql = query {
    select("id", "name", "age")
    where {
        equals("status", "active")
        greaterThan("age", 18)
    }
}
// Result: "SELECT id, name, age WHERE status = 'active' AND age > 18"

Pattern: Type-Safe HTML DSL

abstract class Tag(val name: String) {
    private val children = mutableListOf<Tag>()
    private val attributes = mutableMapOf<String, String>()

    fun <T : Tag> tag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }

    fun attr(name: String, value: String) {
        attributes[name] = value
    }

    fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name")
        attributes.forEach { (k, v) -> builder.append(" $k=\"$v\"") }
        if (children.isEmpty()) {
            builder.append("/>\n")
        } else {
            builder.append(">\n")
            children.forEach { it.render(builder, "$indent  ") }
            builder.append("$indent</$name>\n")
        }
    }
}

class HTML : Tag("html")
class Head : Tag("head")
class Body : Tag("body")
class Div : Tag("div")
class P : Tag("p")
class A : Tag("a")

fun HTML.head(init: Head.() -> Unit) = tag(Head(), init)
fun HTML.body(init: Body.() -> Unit) = tag(Body(), init)
fun Body.div(init: Div.() -> Unit) = tag(Div(), init)
fun Div.p(init: P.() -> Unit) = tag(P(), init)
fun Div.a(init: A.() -> Unit) = tag(A(), init)

fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)

// Usage
val page = html {
    head {
        // ...
    }
    body {
        div {
            attr("class", "container")
            p {
                attr("id", "intro")
            }
            a {
                attr("href", "https://example.com")
            }
        }
    }
}

Best Practices

DO

  1. Return this for chaining:

    fun add(item: Item): Builder {
        items.add(item)
        return this
    }
    
  2. Use inline for DSL functions:

    inline fun myDsl(init: Builder.() -> Unit) = Builder().apply(init).build()
    
  3. Provide sensible defaults:

    inline fun query(
        init: QueryBuilder.() -> Unit = {}  // Empty lambda as default
    ) = QueryBuilder().apply(init).build()
    
  4. Validate in build():

    fun build(): Result {
        require(fields.isNotEmpty()) { "Must specify at least one field" }
        return Result(fields)
    }
    

DON'T

  1. Forget to return this:

    fun add(item: Item) {  // BAD: Can't chain
        items.add(item)
    }
    
  2. Mutate after build:

    val builder = Builder()
    builder.add("foo")
    val result = builder.build()
    builder.add("bar")  // BAD: Confusing state
    
  3. Expose mutable state:

    class Builder {
        val items = mutableListOf<Item>()  // BAD: Can be mutated externally
    }
    
  4. Make DSL functions non-inline unnecessarily:

    fun myDsl(init: Builder.() -> Unit) = ...  // BAD: Lambda allocation overhead
    

References