Skip to main content

🎨 Creating DSL

πŸ“– What is DSL?​

DSL (Domain-Specific Language) is a language specialized for a specific domain. You can create readable and intuitive APIs with Kotlin!

πŸ’‘ Basic Concepts​

Lambda with Receiver​

// Regular lambda
fun buildString1(action: () -> String): String {
return action()
}

// Lambda with receiver
fun buildString2(action: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.action()
return builder.toString()
}

fun main() {
val result = buildString2 {
append("Hello ") // this.append("Hello ")
append("World!")
}
println(result) // Hello World!
}

apply and with​

class Person {
var name: String = ""
var age: Int = 0
}

fun main() {
// apply - returns object
val person1 = Person().apply {
name = "홍길동"
age = 25
}

// with - returns result
val greeting = with(person1) {
"μ•ˆλ…•ν•˜μ„Έμš”, ${name}λ‹˜! (${age}μ„Έ)"
}

println(greeting)
}

🎯 HTML DSL​

Simple HTML Builder​

class HtmlBuilder {
private val content = StringBuilder()

fun h1(text: String) {
content.append("<h1>$text</h1>\n")
}

fun p(text: String) {
content.append("<p>$text</p>\n")
}

fun div(block: HtmlBuilder.() -> Unit) {
content.append("<div>\n")
block()
content.append("</div>\n")
}

override fun toString() = content.toString()
}

fun html(block: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.block()
return builder.toString()
}

fun main() {
val page = html {
h1("Welcome!")
p("This is a paragraph.")
div {
h1("Section")
p("Content here.")
}
}

println(page)
}

Adding Attributes​

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

fun attribute(key: String, value: String) {
attributes[key] = value
}

operator fun String.unaryPlus() {
text = this
}

override fun toString(): String {
val attrs = attributes.entries.joinToString(" ") { "${it.key}=\"${it.value}\"" }
val attrsString = if (attrs.isNotEmpty()) " $attrs" else ""

return if (children.isEmpty() && text.isEmpty()) {
"<$name$attrsString />"
} else {
val childrenString = children.joinToString("\n") { it.toString() }
val content = if (text.isNotEmpty()) text else childrenString
"<$name$attrsString>$content</$name>"
}
}
}

fun tag(name: String, block: Tag.() -> Unit): Tag {
val tag = Tag(name)
tag.block()
return tag
}

fun main() {
val div = tag("div") {
attribute("class", "container")
+"Hello World"
}

println(div)
// <div class="container">Hello World</div>
}

πŸ”§ SQL DSL​

Query Builder​

class Query {
private var table: String = ""
private val columns = mutableListOf<String>()
private var whereClause: String = ""

fun from(table: String) {
this.table = table
}

fun select(vararg columns: String) {
this.columns.addAll(columns)
}

fun where(condition: String) {
whereClause = condition
}

fun build(): String {
val cols = if (columns.isEmpty()) "*" else columns.joinToString(", ")
var sql = "SELECT $cols FROM $table"
if (whereClause.isNotEmpty()) {
sql += " WHERE $whereClause"
}
return sql
}
}

fun query(block: Query.() -> Unit): String {
val query = Query()
query.block()
return query.build()
}

fun main() {
val sql = query {
select("id", "name", "email")
from("users")
where("age > 20")
}

println(sql)
// SELECT id, name, email FROM users WHERE age > 20
}

🎨 Configuration DSL​

App Configuration​

class AppConfig {
var host: String = "localhost"
var port: Int = 8080
var debug: Boolean = false

private val routes = mutableListOf<Route>()

fun route(path: String, block: Route.() -> Unit) {
val route = Route(path)
route.block()
routes.add(route)
}

fun showConfig() {
println("=== Config ===")
println("Host: $host")
println("Port: $port")
println("Debug: $debug")
println("\n=== Routes ===")
routes.forEach { println(it) }
}
}

class Route(val path: String) {
var method: String = "GET"
var handler: String = ""

override fun toString() = "$method $path -> $handler"
}

fun configure(block: AppConfig.() -> Unit): AppConfig {
val config = AppConfig()
config.block()
return config
}

fun main() {
val config = configure {
host = "example.com"
port = 3000
debug = true

route("/api/users") {
method = "GET"
handler = "listUsers"
}

route("/api/users") {
method = "POST"
handler = "createUser"
}
}

config.showConfig()
}

🎯 Test DSL​

Simple Test Framework​

class TestSuite(val name: String) {
private val tests = mutableListOf<Test>()

fun test(name: String, block: TestContext.() -> Unit) {
tests.add(Test(name, block))
}

fun run() {
println("=== $name ===\n")
var passed = 0
var failed = 0

for (test in tests) {
val context = TestContext()
try {
test.block(context)
println("βœ“ ${test.name}")
passed++
} catch (e: AssertionError) {
println("βœ— ${test.name}: ${e.message}")
failed++
}
}

println("\nResults: $passed passed, $failed failed")
}
}

class Test(val name: String, val block: TestContext.() -> Unit)

class TestContext {
infix fun <T> T.shouldBe(expected: T) {
if (this != expected) {
throw AssertionError("Expected $expected but was $this")
}
}
}

fun describe(name: String, block: TestSuite.() -> Unit) {
val suite = TestSuite(name)
suite.block()
suite.run()
}

fun main() {
describe("Calculator") {
test("addition") {
val result = 2 + 3
result shouldBe 5
}

test("subtraction") {
val result = 5 - 2
result shouldBe 3
}

test("multiplication") {
val result = 3 * 4
result shouldBe 12
}
}
}

πŸ—οΈ Builder Pattern​

User Builder​

class User private constructor(
val name: String,
val email: String?,
val age: Int?,
val address: String?
) {
class Builder {
private var name: String = ""
private var email: String? = null
private var age: Int? = null
private var address: String? = null

fun name(name: String) = apply { this.name = name }
fun email(email: String) = apply { this.email = email }
fun age(age: Int) = apply { this.age = age }
fun address(address: String) = apply { this.address = address }

fun build(): User {
require(name.isNotBlank()) { "Name is required" }
return User(name, email, age, address)
}
}

override fun toString() = "User(name=$name, email=$email, age=$age, address=$address)"
}

fun user(block: User.Builder.() -> Unit): User {
return User.Builder().apply(block).build()
}

fun main() {
val user = user {
name("홍길동")
email("hong@example.com")
age(25)
address("μ„œμšΈ")
}

println(user)
}

πŸ€” Frequently Asked Questions​

Q1. When should I use DSL?​

A: For clean, repetitive tasks!

// ❌ Verbose code
val config = Config()
config.setHost("localhost")
config.setPort(8080)
config.addRoute("/api", "GET", "handler")

// βœ… DSL
val config = configure {
host = "localhost"
port = 8080
route("/api") {
method = "GET"
handler = "handler"
}
}

Q2. What is @DslMarker?​

A: Restricts nested DSL!

@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HtmlTag {
fun div(block: HtmlTag.() -> Unit) { }
}

fun html(block: HtmlTag.() -> Unit) {
val tag = HtmlTag()
tag.block()
}

Q3. What about performance?​

A: Almost the same as regular code!

// Generates nearly identical bytecode after compilation
val result1 = buildString {
append("Hello")
}

val builder = StringBuilder()
builder.append("Hello")
val result2 = builder.toString()

🎬 Conclusion​

Create beautiful APIs with DSL!

Key Takeaways:
βœ… Utilize lambda with receiver
βœ… Builder pattern with apply and with
βœ… Intuitive syntax
βœ… Maintain type safety
βœ… Domain-specific language

Congratulations! You've completed the Practical series! πŸŽ‰

Next Step: Learn asynchronous programming in Coroutines!