Kotlin DSL (Domain-Specific Language)
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:
| 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
// 1. Lambda with Receiver
fun html(init: HTML.() -> Unit): HTML
// 2. Extension Functions
fun String.bold() = "<b>$this</b>"
// 3. Infix Functions
infix fun Int.shouldEqual(expected: Int): Unit
// 4. Operator Overloading
operator fun RouteBuilder.div(path: String): Route
// 5. @DslMarker annotation
@DslMarker
annotation class HtmlDsl3. 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 receiver
fun 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
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class 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>"
}
}
@HtmlDsl
class Head {
var title: String = ""
override fun toString() = "<head><title>$title</title></head>"
}
@HtmlDsl
class 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>"
}
}
@HtmlDsl
class 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 Element
class H1(val text: String) : Element {
override fun toString() = "<h1>$text</h1>"
}
class P(val text: String) : Element {
override fun toString() = "<p>$text</p>"
}
// Builder function
fun html(init: HTML.() -> Unit): HTML {
return HTML().apply(init)
}
// Sử dụng DSL
fun 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
@DslMarker ngăn việc truy cập implicit receivers lồng nhau, giúp DSL an toàn hơn:
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Table {
fun tr(init: Tr.() -> Unit) { /* ... */ }
}
@HtmlDsl
class 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)
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
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)
[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 Catalog
dependencies {
implementation(libs.kotlinx.coroutines)
implementation(libs.bundles.ktor.client)
}Custom Tasks
tasks.register("generateDocs") {
group = "documentation"
description = "Generate project documentation"
doLast {
println("Generating documentation...")
// Logic here
}
}
// Task với dependencies
tasks.register<Copy>("copyResources") {
from("src/main/resources")
into("$buildDir/resources")
dependsOn("processResources")
}
// Typed task configuration
tasks.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
Composables cơ bản
@Composable
fun 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
@Composable
fun 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
@Composable
fun 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
@Composable
fun 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
@Composable
fun 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)
@Composable
fun 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
@Composable
fun 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
@Composable
fun FadeSlideContent(visible: Boolean) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(
animationSpec = tween(500)
) + slideInVertically(
initialOffsetY = { -it },
animationSpec = spring()
),
exit = fadeOut() + slideOutVertically()
) {
// Content
}
}8. Ktor Routing DSL
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
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ụng
suspend 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
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
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
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
// Table definitions
object 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 Pattern
class 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 DSL
fun 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 queries
fun 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/Update
fun 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 operations
fun 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
Ví dụ: Form Validation DSL
@DslMarker
annotation class ValidationDsl
// Validation Result
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(val errors: List<String>) : ValidationResult()
}
// Field validator
@ValidationDsl
class 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
@ValidationDsl
class 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 point
fun validate(block: FormValidator.() -> Unit): ValidationResult {
return FormValidator().apply(block).validate()
}
// Sử dụng DSL
data 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"
)
}
}
}
// Test
fun 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
@DslMarker
annotation class ConfigDsl
// Server configuration
@ConfigDsl
class 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)
}
}
@ConfigDsl
class SslConfig {
var keyStore: String = ""
var keyStorePassword: String = ""
var keyAlias: String = ""
}
@ConfigDsl
class 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
}
}
@ConfigDsl
class 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)
}
}
@ConfigDsl
class MigrationConfig {
var enabled: Boolean = true
var locations: List<String> = listOf("db/migration")
}
// Entry point
fun server(block: ServerConfig.() -> Unit): ServerConfig {
return ServerConfig().apply(block)
}
// Sử dụng
fun 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
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:
| 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:
- 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
Last updated on