🧩 Kotlin DSL
1. Giới thiệu
Phần tiêu đề “1. Giới thiệu”DSL (Domain-Specific Language) là ngôn ngữ được thiết kế cho một lĩnh vực cụ thể. Kotlin cung cấp các tính năng mạnh mẽ để tạo DSL với cú pháp tự nhiên, dễ đọc.
DSL phổ biến trong Kotlin ecosystem:
Phần tiêu đề “DSL phổ biến trong Kotlin ecosystem:”| DSL | Mục đích |
|---|---|
| Gradle Kotlin DSL (.kts) | Build configuration |
| Jetpack Compose | UI declarative |
| Ktor Routing | Server routing |
| kotlinx.html | HTML generation |
| Exposed | Database queries |
| Kotest | Testing specifications |
2. Các tính năng Kotlin hỗ trợ DSL
Phần tiêu đề “2. Các tính năng Kotlin hỗ trợ DSL”// 1. Lambda with Receiverfun html(init: HTML.() -> Unit): HTML
// 2. Extension Functionsfun String.bold() = "<b>$this</b>"
// 3. Infix Functionsinfix fun Int.shouldEqual(expected: Int): Unit
// 4. Operator Overloadingoperator fun RouteBuilder.div(path: String): Route
// 5. @DslMarker annotation@DslMarkerannotation class HtmlDsl3. Lambda with Receiver - Nền tảng của DSL
Phần tiêu đề “3. Lambda with Receiver - Nền tảng của DSL”// Function type with receiver// Type: A.() -> R// Có thể gọi methods của A (this) trong lambda
class StringBuilder { private val content = StringBuilder()
fun append(str: String) { content.append(str) }
override fun toString() = content.toString()}
// Lambda with receiverfun buildString(action: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.action() // Gọi action với sb là receiver return sb.toString()}
fun main() { val result = buildString { append("Hello, ") // this.append() append("World!") } println(result) // Hello, World!}4. HTML DSL - Ví dụ cơ bản
Phần tiêu đề “4. HTML DSL - Ví dụ cơ bản”@DslMarkerannotation class HtmlDsl
@HtmlDslclass HTML { private val children = mutableListOf<Element>()
fun head(init: Head.() -> Unit) { children.add(Head().apply(init)) }
fun body(init: Body.() -> Unit) { children.add(Body().apply(init)) }
override fun toString(): String { return "<html>${children.joinToString("")}</html>" }}
@HtmlDslclass Head { var title: String = ""
override fun toString() = "<head><title>$title</title></head>"}
@HtmlDslclass Body { private val children = mutableListOf<Element>()
fun h1(text: String) { children.add(H1(text)) }
fun p(text: String) { children.add(P(text)) }
fun div(init: Div.() -> Unit) { children.add(Div().apply(init)) }
override fun toString(): String { return "<body>${children.joinToString("")}</body>" }}
@HtmlDslclass Div { private val children = mutableListOf<Element>() var className: String? = null
fun p(text: String) { children.add(P(text)) }
override fun toString(): String { val classAttr = className?.let { " class=\"$it\"" } ?: "" return "<div$classAttr>${children.joinToString("")}</div>" }}
interface Elementclass H1(val text: String) : Element { override fun toString() = "<h1>$text</h1>"}class P(val text: String) : Element { override fun toString() = "<p>$text</p>"}
// Builder functionfun html(init: HTML.() -> Unit): HTML { return HTML().apply(init)}
// Sử dụng DSLfun main() { val page = html { head { title = "My Page" } body { h1("Welcome") p("This is a paragraph.") div { className = "container" p("Inside div") } } } println(page)}Output:
<html><head><title>My Page</title></head><body><h1>Welcome</h1><p>This is a paragraph.</p><div class="container"><p>Inside div</p></div></body></html>5. @DslMarker - Kiểm soát scope
Phần tiêu đề “5. @DslMarker - Kiểm soát scope”@DslMarker ngăn việc truy cập implicit receivers lồng nhau, giúp DSL an toàn hơn:
@DslMarkerannotation class HtmlDsl
@HtmlDslclass Table { fun tr(init: Tr.() -> Unit) { /* ... */ }}
@HtmlDslclass Tr { fun td(text: String) { /* ... */ }}
fun main() { val table = table { tr { td("Cell 1") // Không thể gọi tr() ở đây vì @DslMarker // tr { } // ❌ Compile error } }
// Nếu thực sự cần, dùng labeled this val table2 = table { tr { td("Cell 1") this@table.tr { // ✅ Explicit reference td("Cell 2") } } }}6. Gradle Kotlin DSL (.kts)
Phần tiêu đề “6. Gradle Kotlin DSL (.kts)”build.gradle.kts cơ bản
Phần tiêu đề “build.gradle.kts cơ bản”plugins { kotlin("jvm") version "1.9.22" kotlin("plugin.serialization") version "1.9.22" application}
group = "com.example"version = "1.0.0"
repositories { mavenCentral() google()}
dependencies { // Implementation dependencies implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
// Test dependencies testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
// Platform/BOM implementation(platform("io.ktor:ktor-bom:2.3.7")) implementation("io.ktor:ktor-client-core") implementation("io.ktor:ktor-client-cio")}
application { mainClass.set("com.example.MainKt")}
tasks.test { useJUnitPlatform()}
kotlin { jvmToolchain(17)}settings.gradle.kts
Phần tiêu đề “settings.gradle.kts”pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() }}
rootProject.name = "my-project"
include(":app")include(":core")include(":feature:home")include(":feature:profile")Version Catalogs (libs.versions.toml)
Phần tiêu đề “Version Catalogs (libs.versions.toml)”[versions]kotlin = "1.9.22"ktor = "2.3.7"coroutines = "1.7.3"
[libraries]kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
[bundles]ktor-client = ["ktor-client-core", "ktor-client-cio"]
[plugins]kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }// build.gradle.kts với Version Catalogdependencies { implementation(libs.kotlinx.coroutines) implementation(libs.bundles.ktor.client)}Custom Tasks
Phần tiêu đề “Custom Tasks”tasks.register("generateDocs") { group = "documentation" description = "Generate project documentation"
doLast { println("Generating documentation...") // Logic here }}
// Task với dependenciestasks.register<Copy>("copyResources") { from("src/main/resources") into("$buildDir/resources")
dependsOn("processResources")}
// Typed task configurationtasks.named<Jar>("jar") { archiveFileName.set("${project.name}-${project.version}.jar") manifest { attributes( "Main-Class" to "com.example.MainKt", "Implementation-Version" to project.version ) }}7. Jetpack Compose UI DSL
Phần tiêu đề “7. Jetpack Compose UI DSL”Composables cơ bản
Phần tiêu đề “Composables cơ bản”@Composablefun Greeting(name: String, modifier: Modifier = Modifier) { Column( modifier = modifier .fillMaxWidth() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary )
Spacer(modifier = Modifier.height(8.dp))
Button( onClick = { /* Handle click */ }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ) ) { Icon( imageVector = Icons.Default.Favorite, contentDescription = null ) Spacer(modifier = Modifier.width(4.dp)) Text("Like") } }}Modifier Chain DSL
Phần tiêu đề “Modifier Chain DSL”@Composablefun StyledCard() { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp) .shadow( elevation = 8.dp, shape = RoundedCornerShape(12.dp) ) .clip(RoundedCornerShape(12.dp)) .clickable { /* onClick */ } .background( brush = Brush.horizontalGradient( colors = listOf(Color.Blue, Color.Cyan) ) ), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { // Card content }}Custom Composable với DSL Pattern
Phần tiêu đề “Custom Composable với DSL Pattern”@Composablefun FormField( label: String, value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, errorMessage: String? = null) { Column(modifier = modifier) { OutlinedTextField( value = value, onValueChange = onValueChange, label = { Text(label) }, leadingIcon = leadingIcon, trailingIcon = trailingIcon, isError = isError, modifier = Modifier.fillMaxWidth() )
if (isError && errorMessage != null) { Text( text = errorMessage, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp, top = 4.dp) ) } }}
// Sử dụng@Composablefun LoginForm() { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) { FormField( label = "Email", value = email, onValueChange = { email = it }, leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }, isError = !email.contains("@"), errorMessage = "Invalid email format" )
Spacer(modifier = Modifier.height(16.dp))
FormField( label = "Password", value = password, onValueChange = { password = it }, leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) } ) }}LazyColumn DSL
Phần tiêu đề “LazyColumn DSL”@Composablefun UserList(users: List<User>) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { // Header item { Text( text = "Users (${users.size})", style = MaterialTheme.typography.titleLarge ) }
// Items items( items = users, key = { it.id } ) { user -> UserCard(user = user) }
// Grouped items val grouped = users.groupBy { it.department } grouped.forEach { (department, deptUsers) -> stickyHeader { Text( text = department, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) .padding(8.dp) ) }
items(deptUsers) { user -> UserCard(user = user) } }
// Footer item { Text( text = "End of list", modifier = Modifier.padding(16.dp) ) } }}Navigation DSL (Compose Navigation)
Phần tiêu đề “Navigation DSL (Compose Navigation)”@Composablefun AppNavigation() { val navController = rememberNavController()
NavHost( navController = navController, startDestination = "home" ) { composable("home") { HomeScreen( onNavigateToDetail = { id -> navController.navigate("detail/$id") } ) }
composable( route = "detail/{itemId}", arguments = listOf( navArgument("itemId") { type = NavType.StringType } ) ) { backStackEntry -> val itemId = backStackEntry.arguments?.getString("itemId") DetailScreen(itemId = itemId) }
// Nested navigation navigation( startDestination = "settings/main", route = "settings" ) { composable("settings/main") { SettingsScreen() } composable("settings/profile") { ProfileSettingsScreen() } }
// Bottom sheet dialog("dialog") { AlertDialogContent() } }}Animation DSL
Phần tiêu đề “Animation DSL”@Composablefun AnimatedContent() { var expanded by remember { mutableStateOf(false) }
// Animate multiple values val transition = updateTransition( targetState = expanded, label = "card_transition" )
val cardHeight by transition.animateDp( label = "height", transitionSpec = { spring(dampingRatio = 0.8f, stiffness = 300f) } ) { isExpanded -> if (isExpanded) 200.dp else 100.dp }
val cardColor by transition.animateColor( label = "color" ) { isExpanded -> if (isExpanded) Color.Blue else Color.Gray }
val rotation by transition.animateFloat( label = "rotation" ) { isExpanded -> if (isExpanded) 180f else 0f }
Card( modifier = Modifier .fillMaxWidth() .height(cardHeight) .clickable { expanded = !expanded }, colors = CardDefaults.cardColors(containerColor = cardColor) ) { Icon( imageVector = Icons.Default.ExpandMore, contentDescription = null, modifier = Modifier.rotate(rotation) ) }}
// AnimatedVisibility DSL@Composablefun FadeSlideContent(visible: Boolean) { AnimatedVisibility( visible = visible, enter = fadeIn( animationSpec = tween(500) ) + slideInVertically( initialOffsetY = { -it }, animationSpec = spring() ), exit = fadeOut() + slideOutVertically() ) { // Content }}8. Ktor Routing DSL
Phần tiêu đề “8. Ktor Routing DSL”Server Routing
Phần tiêu đề “Server Routing”fun Application.configureRouting() { routing { // Simple route get("/") { call.respondText("Hello, World!") }
// Route với path parameter get("/users/{id}") { val id = call.parameters["id"] call.respond(getUserById(id)) }
// POST với request body post("/users") { val user = call.receive<CreateUserRequest>() val createdUser = createUser(user) call.respond(HttpStatusCode.Created, createdUser) }
// Route grouping route("/api/v1") { // Authentication routes route("/auth") { post("/login") { /* ... */ } post("/register") { /* ... */ } post("/refresh") { /* ... */ } }
// Protected routes authenticate("jwt") { route("/users") { get { /* List users */ } get("/{id}") { /* Get user */ } put("/{id}") { /* Update user */ } delete("/{id}") { /* Delete user */ } }
route("/posts") { get { /* List posts */ } post { /* Create post */ } } } }
// Static files static("/static") { resources("static") defaultResource("index.html") }
// WebSocket webSocket("/ws") { for (frame in incoming) { when (frame) { is Frame.Text -> { val text = frame.readText() send("Echo: $text") } else -> {} } } } }}Ktor Client DSL
Phần tiêu đề “Ktor Client DSL”val client = HttpClient(CIO) { install(ContentNegotiation) { json(Json { prettyPrint = true ignoreUnknownKeys = true }) }
install(Logging) { level = LogLevel.INFO logger = Logger.DEFAULT }
install(HttpTimeout) { requestTimeoutMillis = 30_000 connectTimeoutMillis = 10_000 }
defaultRequest { url("https://api.example.com") header("Accept", "application/json") }}
// Sử dụngsuspend fun fetchUsers(): List<User> { return client.get("/users") { parameter("limit", 10) parameter("offset", 0) header("Authorization", "Bearer $token") }.body()}
suspend fun createUser(request: CreateUserRequest): User { return client.post("/users") { contentType(ContentType.Application.Json) setBody(request) }.body()}9. Testing DSL
Phần tiêu đề “9. Testing DSL”Kotest Specification DSL
Phần tiêu đề “Kotest Specification DSL”class UserServiceSpec : FunSpec({
val userRepository = mockk<UserRepository>() val userService = UserService(userRepository)
beforeTest { clearMocks(userRepository) }
context("createUser") { test("should create user with valid data") { // Given val request = CreateUserRequest("john@example.com", "John") val expectedUser = User(id = "1", email = "john@example.com", name = "John")
coEvery { userRepository.save(any()) } returns expectedUser
// When val result = userService.createUser(request)
// Then result shouldBe expectedUser coVerify(exactly = 1) { userRepository.save(any()) } }
test("should throw exception for invalid email") { val request = CreateUserRequest("invalid-email", "John")
shouldThrow<ValidationException> { userService.createUser(request) } } }
context("getUser") { test("should return user when exists") { val userId = "123" val expectedUser = User(id = userId, email = "test@example.com", name = "Test")
coEvery { userRepository.findById(userId) } returns expectedUser
val result = userService.getUser(userId)
result.shouldNotBeNull() result.id shouldBe userId }
test("should return null when user not found") { coEvery { userRepository.findById(any()) } returns null
val result = userService.getUser("non-existent")
result.shouldBeNull() } }})BehaviorSpec DSL
Phần tiêu đề “BehaviorSpec DSL”class OrderServiceSpec : BehaviorSpec({
Given("a shopping cart with items") { val cart = ShoppingCart().apply { addItem(Product("iPhone", 999.99), quantity = 1) addItem(Product("Case", 29.99), quantity = 2) }
When("checkout is initiated") { val order = OrderService().checkout(cart)
Then("order should be created") { order.shouldNotBeNull() order.status shouldBe OrderStatus.PENDING }
Then("order total should be correct") { order.total shouldBe 1059.97 }
Then("order should have correct items") { order.items shouldHaveSize 2 } }
When("checkout with empty cart") { val emptyCart = ShoppingCart()
Then("should throw exception") { shouldThrow<EmptyCartException> { OrderService().checkout(emptyCart) } } } }})Matchers DSL
Phần tiêu đề “Matchers DSL”class MatchersExample : StringSpec({
"collection matchers" { val list = listOf(1, 2, 3, 4, 5)
list shouldContain 3 list shouldContainAll listOf(1, 2, 3) list shouldContainExactly listOf(1, 2, 3, 4, 5) list shouldHaveSize 5 list.shouldNotBeEmpty() list shouldStartWith 1 list shouldEndWith 5 }
"string matchers" { val str = "Hello, Kotlin!"
str shouldContain "Kotlin" str shouldStartWith "Hello" str shouldEndWith "!" str shouldMatch Regex("Hello.*") str.shouldNotBeBlank() str.length shouldBeGreaterThan 5 }
"comparison matchers" { val number = 42
number shouldBe 42 number shouldBeGreaterThan 40 number shouldBeLessThanOrEqual 50 number shouldBeInRange 40..50 }
"custom matchers" { data class User(val name: String, val age: Int)
fun beAdult() = object : Matcher<User> { override fun test(value: User) = MatcherResult( passed = value.age >= 18, failureMessageFn = { "${value.name} should be adult but was ${value.age}" }, negatedFailureMessageFn = { "${value.name} should not be adult" } ) }
val user = User("John", 25) user should beAdult() }})10. Exposed Database DSL
Phần tiêu đề “10. Exposed Database DSL”// Table definitionsobject Users : Table("users") { val id = integer("id").autoIncrement() val email = varchar("email", 255).uniqueIndex() val name = varchar("name", 100) val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)}
object Posts : Table("posts") { val id = integer("id").autoIncrement() val title = varchar("title", 200) val content = text("content") val authorId = integer("author_id").references(Users.id) val publishedAt = datetime("published_at").nullable()
override val primaryKey = PrimaryKey(id)}
// DAO Patternclass UserDAO(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<UserDAO>(Users)
var email by Users.email var name by Users.name var createdAt by Users.createdAt
val posts by PostDAO referrersOn Posts.authorId}
// Queries với DSLfun getAllUsers(): List<User> = transaction { Users.selectAll() .map { row -> User( id = row[Users.id], email = row[Users.email], name = row[Users.name] ) }}
fun getUserWithPosts(userId: Int): UserWithPosts? = transaction { (Users innerJoin Posts) .select { Users.id eq userId } .map { row -> UserWithPosts( user = User( id = row[Users.id], email = row[Users.email], name = row[Users.name] ), post = Post( id = row[Posts.id], title = row[Posts.title], content = row[Posts.content] ) ) } .firstOrNull()}
// Complex queriesfun searchPosts( keyword: String? = null, authorId: Int? = null, publishedOnly: Boolean = true): List<Post> = transaction { Posts.select { val conditions = mutableListOf<Op<Boolean>>()
if (keyword != null) { conditions += (Posts.title like "%$keyword%") or (Posts.content like "%$keyword%") }
if (authorId != null) { conditions += Posts.authorId eq authorId }
if (publishedOnly) { conditions += Posts.publishedAt.isNotNull() }
conditions.reduce { acc, op -> acc and op } } .orderBy(Posts.publishedAt, SortOrder.DESC) .map { it.toPost() }}
// Insert/Updatefun createUser(email: String, name: String): Int = transaction { Users.insert { it[Users.email] = email it[Users.name] = name } get Users.id}
fun updateUser(userId: Int, name: String): Int = transaction { Users.update({ Users.id eq userId }) { it[Users.name] = name }}
// Batch operationsfun createUsers(users: List<CreateUserRequest>): List<Int> = transaction { Users.batchInsert(users) { user -> this[Users.email] = user.email this[Users.name] = user.name }.map { it[Users.id] }}11. Tự tạo DSL
Phần tiêu đề “11. Tự tạo DSL”Ví dụ: Form Validation DSL
Phần tiêu đề “Ví dụ: Form Validation DSL”@DslMarkerannotation class ValidationDsl
// Validation Resultsealed class ValidationResult { object Valid : ValidationResult() data class Invalid(val errors: List<String>) : ValidationResult()}
// Field validator@ValidationDslclass FieldValidator<T>(private val fieldName: String, private val value: T) { private val errors = mutableListOf<String>()
fun notNull(message: String = "$fieldName cannot be null") { if (value == null) errors += message }
fun notBlank(message: String = "$fieldName cannot be blank") { if (value is String && value.isBlank()) errors += message }
fun minLength(min: Int, message: String = "$fieldName must be at least $min characters") { if (value is String && value.length < min) errors += message }
fun maxLength(max: Int, message: String = "$fieldName cannot exceed $max characters") { if (value is String && value.length > max) errors += message }
fun matches(regex: Regex, message: String = "$fieldName has invalid format") { if (value is String && !value.matches(regex)) errors += message }
fun range(min: Number, max: Number, message: String = "$fieldName must be between $min and $max") { if (value is Number) { val num = value.toDouble() if (num < min.toDouble() || num > max.toDouble()) errors += message } }
fun custom(predicate: (T) -> Boolean, message: String) { if (!predicate(value)) errors += message }
fun getErrors() = errors.toList()}
// Form validator builder@ValidationDslclass FormValidator { private val allErrors = mutableListOf<String>()
fun <T> field(name: String, value: T, block: FieldValidator<T>.() -> Unit) { val validator = FieldValidator(name, value) validator.block() allErrors += validator.getErrors() }
fun validate(): ValidationResult { return if (allErrors.isEmpty()) { ValidationResult.Valid } else { ValidationResult.Invalid(allErrors) } }}
// DSL entry pointfun validate(block: FormValidator.() -> Unit): ValidationResult { return FormValidator().apply(block).validate()}
// Sử dụng DSLdata class RegistrationForm( val email: String, val password: String, val age: Int, val username: String)
fun validateRegistration(form: RegistrationForm): ValidationResult { return validate { field("email", form.email) { notBlank() matches( regex = Regex("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}\$"), message = "Invalid email format" ) }
field("password", form.password) { notBlank() minLength(8, "Password must be at least 8 characters") custom( predicate = { it.any { c -> c.isDigit() } }, message = "Password must contain at least one digit" ) custom( predicate = { it.any { c -> c.isUpperCase() } }, message = "Password must contain at least one uppercase letter" ) }
field("age", form.age) { range(18, 120, "You must be at least 18 years old") }
field("username", form.username) { notBlank() minLength(3) maxLength(20) matches( regex = Regex("^[a-zA-Z0-9_]+$"), message = "Username can only contain letters, numbers, and underscores" ) } }}
// Testfun main() { val form = RegistrationForm( email = "invalid-email", password = "weak", age = 15, username = "jo" )
when (val result = validateRegistration(form)) { is ValidationResult.Valid -> println("Form is valid!") is ValidationResult.Invalid -> { println("Validation errors:") result.errors.forEach { println(" - $it") } } }}Output:
Validation errors: - Invalid email format - Password must be at least 8 characters - Password must contain at least one digit - Password must contain at least one uppercase letter - You must be at least 18 years old - username must be at least 3 characters12. Configuration DSL Pattern
Phần tiêu đề “12. Configuration DSL Pattern”@DslMarkerannotation class ConfigDsl
// Server configuration@ConfigDslclass ServerConfig { var host: String = "localhost" var port: Int = 8080
private var _ssl: SslConfig? = null val ssl: SslConfig? get() = _ssl
private var _cors: CorsConfig? = null val cors: CorsConfig? get() = _cors
private var _database: DatabaseConfig? = null val database: DatabaseConfig? get() = _database
fun ssl(block: SslConfig.() -> Unit) { _ssl = SslConfig().apply(block) }
fun cors(block: CorsConfig.() -> Unit) { _cors = CorsConfig().apply(block) }
fun database(block: DatabaseConfig.() -> Unit) { _database = DatabaseConfig().apply(block) }}
@ConfigDslclass SslConfig { var keyStore: String = "" var keyStorePassword: String = "" var keyAlias: String = ""}
@ConfigDslclass CorsConfig { private val _allowedOrigins = mutableListOf<String>() val allowedOrigins: List<String> get() = _allowedOrigins
private val _allowedMethods = mutableListOf<String>() val allowedMethods: List<String> get() = _allowedMethods
var allowCredentials: Boolean = false
fun allowOrigin(vararg origins: String) { _allowedOrigins += origins }
fun allowMethod(vararg methods: String) { _allowedMethods += methods }}
@ConfigDslclass DatabaseConfig { var url: String = "" var username: String = "" var password: String = "" var poolSize: Int = 10
private var _migrations: MigrationConfig? = null val migrations: MigrationConfig? get() = _migrations
fun migrations(block: MigrationConfig.() -> Unit) { _migrations = MigrationConfig().apply(block) }}
@ConfigDslclass MigrationConfig { var enabled: Boolean = true var locations: List<String> = listOf("db/migration")}
// Entry pointfun server(block: ServerConfig.() -> Unit): ServerConfig { return ServerConfig().apply(block)}
// Sử dụngfun main() { val config = server { host = "0.0.0.0" port = 443
ssl { keyStore = "/path/to/keystore.jks" keyStorePassword = "secret" keyAlias = "server" }
cors { allowOrigin("https://example.com", "https://app.example.com") allowMethod("GET", "POST", "PUT", "DELETE") allowCredentials = true }
database { url = "jdbc:postgresql://localhost:5432/mydb" username = "admin" password = "password" poolSize = 20
migrations { enabled = true locations = listOf("db/migration", "db/seeds") } } }
println("Server: ${config.host}:${config.port}") println("SSL enabled: ${config.ssl != null}") println("CORS origins: ${config.cors?.allowedOrigins}") println("Database: ${config.database?.url}")}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”Các tính năng Kotlin hỗ trợ DSL:
Phần tiêu đề “Các tính năng Kotlin hỗ trợ DSL:”- Lambda with Receiver:
Type.() -> R- truy cậpthistrong lambda - Extension Functions: mở rộng API hiện có
- Infix Functions: cú pháp tự nhiên
a to b - Operator Overloading:
operator fun plus() - @DslMarker: kiểm soát scope, ngăn lồng nhau vô tình
DSL phổ biến:
Phần tiêu đề “DSL phổ biến:”| DSL | Đặc điểm |
|---|---|
| Gradle .kts | Build config, dependency management |
| Compose | Declarative UI, Modifier chains |
| Ktor | Server routing, client config |
| Kotest | BDD-style testing |
| Exposed | Type-safe SQL queries |
Best Practices:
Phần tiêu đề “Best Practices:”- Sử dụng
@DslMarkerđể tránh scope confusion - Giữ DSL đơn giản và có mục đích rõ ràng
- Cung cấp builder functions làm entry points
- Document cách sử dụng với ví dụ
- Test DSL kỹ lưỡng với nhiều use cases