Skip to content

Instantly share code, notes, and snippets.

@justiandre
Last active June 18, 2020 20:02
Show Gist options
  • Save justiandre/7327cfc37252c5e23b11f982849886ee to your computer and use it in GitHub Desktop.
Save justiandre/7327cfc37252c5e23b11f982849886ee to your computer and use it in GitHub Desktop.

DSLs personalizadas

Nota: Esse conteúdo faz parte de um grupo de estudos sobre Kotlin, descrito aqui!

Conceito

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 em Kotlin (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"
}

Receptores de funções

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.

Exemplos

Exemplo de uso

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.

Exemplo de criação

/**
 * 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.

Exemplo prático

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)
   }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment