Nota: Esse conteúdo faz parte de um grupo de estudos sobre Kotlin, descrito aqui!
Diferente das linguagens de programação que se propõem à resolverem conjuntos genéricos de problemas, a DSL Domain Specific Language
foi criada para resolver um problema específico ou um conjunto de problemas que fazem parte de um mesmo domínio.
Um exemplo de DSL
, é a linguagem utilizada nos scripts de Gradle, que serve para configurar o build e dependências de um projeto JVM. Que é feito em Groovy ou Kotlin, percebemos que por mais que tenhamos uma linguagem de programação para fazer isso, a sintaxe se mostra bem diferente e orientada à resolver um problema específico. Isso acontece porque Gradle possui uma DSL
para escrever seus scripts, denominada Gradle Build Language. Outro exemplo bastante utilizado de DSL
é o Graphql.
Um dos autores mais famosos sobre o assunto, Martin Fowler, considera CSS e SQL como DSL
. A partir disto, pode-se dizer que as DSL's
podem ou não serem consideradas linguagens de programação.
Podemos classificá-las de duas formas:
- Internas: São aquelas que utilizam uma linguagem já existente como base, podendo alterar a sintaxe para resolver um problema de domínio específico, como por exemplo a
DSL
emKotlin
(que será apresentada posteriormente); - Externas: São aquelas que utilizam sintaxes próprias e que, geralmente, necessitam da criação de um parser para o processamento das mesmas (exemplo:
GraphQL
,CSS
,SQL
, dentre outras).
Sabendo disso, veremos como podemos criar DSL
internas para resolver problemas específicos em Kotlin
.
Alguns exemplos das funcionalidades do Kotlin
que auxiliam na criação de DSLs
:
- Higher Order Functions: São funções que suportam uma função como argumento ou retornam uma função.
// Higher Order Function
fun logIfTrue(condition: Boolean, value: () -> String) {
if (condition) {
print(value())
}
}
fun main() {
val age = 30 // input
// Chamada
logIfTrue(age >= 18) { "A pessoa é maior de idade, e possui $age anos" }
}
- Extension Functions: É a possibilidade de adicionar funções à classes já existente.
// Extension Function
fun LocalDate.printPtBr() {
println(this.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")))
}
fun main() {
// Chamada
LocalDate.now().printPtBr() // print: 17/06/2020
LocalDate.of(1988, 11, 3).printPtBr() // print: 03/11/1988
}
- Infix Functions: O modificador infix na assinatura de uma função permite que a mesma seja invocada em um objeto sem a necessidade de parênteses ou da inserção de um ponto antes do seu nome.
// Infix Function
infix fun Boolean.logIfTrue(message: String) {
if (this) {
println(message)
}
}
fun main() {
val age = 30 // input
// Chamada
(age >= 18) logIfTrue "A pessoa é maior de idade, e possui $age anos"
}
O que é um lambda com um receptor (receiver)?
Um lambda com um receptor permite chamar métodos de um objeto no corpo de uma lambda sem nenhum qualificador.
Motivação
Além do açúcar sintático e da concisão, as lambdas com receptores permitem o uso de APIs
expressivas adequadas para DSLs
internas. Eles são bons para esse propósito, porque as DSLs
são linguagens estruturadas e o lambda com receptores fornece facilmente essa capacidade de estruturar APIs
e a capacidade de representar estruturas aninhadas.
fun result() =
html {
head {
title {"XML encoding with Kotlin"}
}
body {
h1 {"XML encoding with Kotlin"}
p {"this format can be used as an alternative markup to XML"}
// an element with attributes and text content
a(href = "http://kotlinlang.org") {"Kotlin"}
// mixed content
p {
"This is some"
b {"mixed"}
"text. For more see the"
a(href = "http://kotlinlang.org") {"Kotlin"}
"project"
}
p {"some text"}
// content generated by
p {
for (arg in args)
arg
}
}
}
Nota: Esse exemplo está disponível na documentação oficial do Kotlin.
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*
* For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#apply).
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
// Impl...
}
Nota: Código da função apply
disponível em todos objetos.
import org.junit.Assert
import org.junit.Test
import java.time.LocalDate
// Classe com o conteúdo dos erros de validação.
data class ValidationErrorItem(
var field: String? = null,
var message: String? = null
)
// Exception que é gerada ao executar as validações.
data class ValidationException(val items: Collection<ValidationErrorItem>) : RuntimeException("Erro de validação")
// Classe responsável por gerenciar o tratamento de erros e a criação da DSL feita para isso.
class ValidateHandler {
companion object {
fun context(context: ValidateHandler.() -> Unit) = ValidateContext(ValidateHandler().apply(context))
}
private val items: ArrayList<ValidationErrorItem> = arrayListOf()
infix fun add(item: ValidationErrorItem) = items.add(item)
infix fun Boolean?.isTrue(item: ValidationErrorItem.() -> Unit) = addIfTrue((this != null && !this), item)
infix fun LocalDate?.isInPast(item: ValidationErrorItem.() -> Unit) = this?.run {
val dateCompare = LocalDate.now()
val isInPass = this == dateCompare || this.isAfter(dateCompare)
addIfTrue(isInPass, item)
}
infix fun Any?.required(item: ValidationErrorItem.() -> Unit) {
val isEmpty = when (this) {
null -> true
is String -> this.isBlank()
is Iterable<*> -> this.toList().isEmpty()
is Map<*, *> -> this.isEmpty()
else -> false
}
addIfTrue(isEmpty, item)
}
private fun addIfTrue(condition: Boolean?, item: ValidationErrorItem.() -> Unit) = condition
?.takeIf { condition }
?.apply { add(ValidationErrorItem().apply(item)) }
class ValidateContext(private val handler: ValidateHandler) {
fun hasValidationError() = items().isNotEmpty()
fun items() = handler.items
fun validate() {
if (hasValidationError()) {
throw ValidationException(items())
}
}
}
}
// Classe Model/Entidade com os dados de negócio e as suas regras, no caso aqui a utilização das DLS criada para validação.
data class Company(
val name: String? = null,
val sector: String? = null,
val document: String? = null,
val foundation: LocalDate? = null,
val hasGain: Boolean = false,
val collaborators: List<String>? = null
) {
// Função com as regras de validação utilizando a DLS criada.
fun validate() = ValidateHandler.context {
name required {
field = "name"
message = "O nome é obrigatório"
}
sector required {
field = "sector"
message = "O setor é obrigatório"
}
document required {
field = "document"
message = "O documento é obrigatório"
}
foundation isInPast {
field = "foundation"
message = "A fundação deve estar no passado"
}
hasGain isTrue {
field = "gain"
message = "Deve possuir ganhos"
}
collaborators required {
field = "collaborators"
message = "É necessário ter ao menos um colaborador"
}
}.validate()
}
// Classe com os testes unitários que garantem o funcionamento da DLS.
class Test {
@Test
fun `with errors`() {
val company = Company(
name = " ",
sector = "",
document = null,
foundation = LocalDate.now().plusDays(1),
hasGain = false,
collaborators = listOf()
)
try {
company.validate()
Assert.fail("Deveria gerar erro de validação")
} catch (validationException: ValidationException) {
val expectErrors = arrayOf(
ValidationErrorItem("name", "O nome é obrigatório"),
ValidationErrorItem("sector", "O setor é obrigatório"),
ValidationErrorItem("document", "O documento é obrigatório"),
ValidationErrorItem("gain", "Deve possuir ganhos"),
ValidationErrorItem("foundation", "A fundação deve estar no passado"),
ValidationErrorItem("collaborators", "É necessário ter ao menos um colaborador")
)
assertValidationException(expectErrors, validationException)
}
}
@Test
fun `without errors`() {
val company = Company(
name = "Mercado Livre",
sector = "TI/E-Commerce",
document = "123456789",
foundation = LocalDate.of(1999, 8, 2),
hasGain = true,
collaborators = listOf("André Justi", "Wellington Gustavo")
)
try {
company.validate()
} catch (validate: ValidationException) {
Assert.fail("Não deveria gerar erro de validação")
}
}
fun assertValidationException(expectErrors: Array<ValidationErrorItem>, validationException: ValidationException) {
val errors = validationException.items.toTypedArray()
val validationErrorItemSort: (ValidationErrorItem) -> String? = ValidationErrorItem::message
errors.sortBy(validationErrorItemSort)
expectErrors.sortBy(validationErrorItemSort)
Assert.assertArrayEquals("Erros não esperados", expectErrors, errors)
}
}