Skip to content

Instantly share code, notes, and snippets.

@sababado
Created May 11, 2022 17:37
Show Gist options
  • Save sababado/38d8cc19f9ac2b58b585290193ee726f to your computer and use it in GitHub Desktop.
Save sababado/38d8cc19f9ac2b58b585290193ee726f to your computer and use it in GitHub Desktop.
Practice at creating a DSL in Kotlin.
import java.lang.IllegalStateException
// Tutorial and inspiration from https://www.baeldung.com/kotlin/dsl
@DslMarker
annotation class SqlDslMarker
@SqlDslMarker
abstract class Condition {
abstract fun addCondition(condition: Condition)
fun and(initializer: Condition.() -> Unit) {
addCondition(And().apply(initializer))
}
fun or(initializer: Condition.() -> Unit) {
addCondition(Or().apply(initializer))
}
infix fun String.eq(value: Any?) {
addCondition(Eq(this, value))
}
infix fun String.like(value: String) {
addCondition(Like(this, value))
}
}
open class CompositeCondition(private val sqlOperator: String) : Condition() {
private val conditions = mutableListOf<Condition>()
override fun addCondition(condition: Condition) {
conditions += condition
}
override fun toString(): String {
return if (conditions.size == 1) {
conditions.first().toString()
} else {
conditions.joinToString(prefix = "( ", postfix = " )", separator = "\n\t$sqlOperator ")
}
}
}
class And : CompositeCondition("AND")
class Or : CompositeCondition("OR")
class Like(private val column: String, private val value: String) : Condition() {
override fun addCondition(condition: Condition) {
throw IllegalStateException("Can't add a nested condition to the sql 'like'")
}
override fun toString(): String {
return "$column LIKE '$value'"
}
}
class Eq(private val column: String, private val value: Any?) : Condition() {
init {
if (value != null && value !is Number && value !is String) {
throw IllegalArgumentException(
"Only <null>, numbers and strings values can be used in the 'where' clause"
)
}
}
override fun addCondition(condition: Condition) {
throw IllegalStateException("Can't add a nested condition to the sql 'eq'")
}
override fun toString(): String {
return when (value) {
null -> "$column IS NULL"
is String -> "$column = '$value'"
else -> "$column = $value"
}
}
}
class SqlSelectBuilder {
private val columns = mutableListOf<String>()
private lateinit var table: String
private var condition: Condition? = null
fun select(vararg columns: String) {
if (columns.isEmpty()) {
throw IllegalArgumentException("At least one column should be defined")
}
if (this.columns.isNotEmpty()) {
throw IllegalStateException(
"Detected an attempt to re-define columns to fetch. "
+ "Current columns list: "
+ "${this.columns}, new columns list: $columns"
)
}
this.columns.addAll(columns)
}
fun where(initializer: Condition.() -> Unit) {
condition = And().apply(initializer)
}
fun from(table: String) {
this.table = table
}
fun build(): String {
if (!::table.isInitialized) {
throw IllegalStateException("Failed to build an sql select - target table is undefined")
}
return toString()
}
override fun toString(): String {
val columnsToFetch =
if (columns.isEmpty()) {
"*"
} else {
columns.joinToString(", ")
}
val conditionString =
if (condition == null) {
""
} else {
"\nWHERE $condition"
}
return "SELECT $columnsToFetch\nFROM $table$conditionString"
}
}
fun query(initializer: SqlSelectBuilder.() -> Unit): SqlSelectBuilder {
return SqlSelectBuilder().apply(initializer)
}
fun print(sql: SqlSelectBuilder.() -> Unit) {
println(query(sql).build())
}
val sql =
query {
select("column1", "column2")
from("myTable")
}.build()
print {
from("table1")
}
print {
select("column1", "column2")
from("table1")
}
print {
from("table1")
where {
"column3" eq 4
"column4" eq null
}
}
print {
from("myTable")
where {
or {
"column2" eq null
and {
"column9" eq 66655
"column10" like "%234%"
}
}
"column1" eq 42
"column5" eq 99
"column6" like "%a"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment