Статьи

Манифест приложения Cloud Foundry с использованием Kotlin DSL

Я с удовольствием работал и получал отличную поддержку по созданию DSL на Kotlin Language .

Эта функция теперь используется для создания файлов сборки gradle , для определения маршрутов в Spring Webflux , для создания html-шаблонов с использованием библиотеки kotlinx.html .

Здесь я собираюсь продемонстрировать создание DSL на основе kotlin для представления содержимого манифеста приложения Cloud Foundry .

Пример манифеста выглядит следующим образом, если он представлен в виде файла yaml:

01
02
03
04
05
06
07
08
09
10
11
applications:
 - name: myapp
   memory: 512M
   instances: 1
   path: target/someapp.jar
   routes:
     - somehost.com
     - antother.com/path
   envs:
    ENV_NAME1: VALUE1
    ENV_NAME2: VALUE2

И вот тип DSL, к которому я стремлюсь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
cf {
    name = "myapp"
    memory = 512(M)
    instances = 1
    path = "target/someapp.jar"
    routes {
        +"somehost.com"
        +"another.com/path"
    }
    envs {
        env["ENV_NAME1"] = "VALUE1"
        env["ENV_NAME2"] = "VALUE2"
    }
}

Получение базовой структуры

Позвольте мне начать с более простой структуры, которая выглядит следующим образом:

1
2
3
4
5
cf {
    name = "myapp"
    instances = 1
    path = "target/someapp.jar"
}

и хотим, чтобы этот вид DSL отображался на структуру, которая выглядит следующим образом:

1
2
3
4
5
data class CfManifest(
        var name: String = "",
        var instances: Int? = 0,
        var path: String? = null
)

Это будет переводиться в функцию Kotlin, которая принимает лямбда-выражение :

1
2
3
fun cf(init: CfManifest.() -> Unit) {
 ...
}

Параметр, который выглядит так:

1
() -> Unit

является довольно очевидным, лямбда-выражение, которое не принимает никаких параметров и ничего не возвращает.

Часть, которая заняла какое-то время, чтобы проникнуть в мои мысли, — это модифицированное лямбда-выражение, называемое лямбда-выражением с получателем

1
CfManifest.() -> Unit

Это делает две вещи, как я понял:

1. Он определяет в области обернутой функции функцию расширения для типа получателя — в моем случае
CfManifest класс

2. this в лямбда-выражении теперь относится к функции приемника.

Учитывая это, функция cf переводится в:

1
2
3
4
5
fun cf(init: CfManifest.() -> Unit): CfManifest {
    val manifest = CfManifest()
    manifest.init()
    return manifest
}

который может быть кратко выражен как:

1
fun cf(init: CfManifest.() -> Unit) = CfManifest().apply(init)

так что теперь, когда я звоню:

1
2
3
4
5
cf {
    name = "myapp"
    instances = 1
    path = "target/someapp.jar"
}

Это переводится как:

1
2
3
4
5
CFManifest().apply {
  this.name = "myapp"
  this.instances = 1
  this.path = "target/someapp.jar"
}

Больше DSL

Расширяя базовую структуру:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
cf {
    name = "myapp"
    memory = 512(M)
    instances = 1
    path = "target/someapp.jar"
    routes {
        +"somehost.com"
        +"another.com/path"
    }
    envs {
        env["ENV_NAME1"] = "VALUE1"
        env["ENV_NAME2"] = "VALUE2"
    }
}

Маршруты и envs, в свою очередь, становятся методами класса CfManifest и выглядят так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
data class CfManifest(
        var name: String = "",
        var path: String? = null,
        var memory: MEM? = null,
        ...
        var routes: ROUTES? = null,
        var envs: ENVS = ENVS()
) {
 
    fun envs(block: ENVS.() -> Unit) {
        this.envs = ENVS().apply(block)
    }
 
    ...
 
    fun routes(block: ROUTES.() -> Unit) {
        this.routes = ROUTES().apply(block)
    }
}
 
data class ENVS(
        var env: MutableMap<String, String> = mutableMapOf()
)
 
data class ROUTES(
        private val routes: MutableList<String> = mutableListOf()
) {
    operator fun String.unaryPlus() {
        routes.add(this)
    }
}

Посмотрите, как метод routes принимает лямбда-выражение с типом получателя ROUTES , это позволяет мне определить выражение следующим образом:

1
2
3
4
5
6
7
8
cf {
    ...
    routes {
        +"somehost.com"
        +"another.com/path"
    }
    ...
}

Еще один трюк здесь — это способ добавления маршрута:

1
+"somehost.com"

который включается с использованием соглашения Kotlin, которое переводит конкретные имена методов в операторы , здесь метод unaryPlus. Круто для меня то, что этот оператор виден только в области действия ROUTES!

Еще одна особенность DSL, использующего возможности Kotlin, — это способ указания памяти, который состоит из двух частей — числа и модификатора, 2G, 500M и т. Д.

Это указывается в слегка измененном виде через DSL как 2 (G) и 500 (M).

Он реализован с использованием другого соглашения Kotlin, где, если у класса есть метод invoke экземпляры могут вызывать его следующим образом:

1
2
3
4
5
class ClassWithInvoke() {
    operator fun invoke(n: Int): String = "" + n
}
val c = ClassWithInvoke()
c(10)

Таким образом, реализация метода invoke в качестве функции расширения для Int в рамках класса CFManifest позволяет использовать этот тип DSL:

1
2
3
4
5
6
7
data class CfManifest(
        var name: String = "",
        ...
) {
    ...
    operator fun Int.invoke(m: MemModifier): MEM = MEM(this, m)
}

Это чисто эксперимент с моей стороны, я новичок в Kotlin, а также в Kotlin DSL, так что очень вероятно, что есть много вещей, которые можно улучшить в этой реализации, любые отзывы и предложения приветствуются. Вы можете поиграть с этим примером кода в моем репозитории github здесь

Ссылка: Приложение Cloud Foundry манифестируется с использованием Kotlin DSL от нашего партнера по JCG Биджу Кунджуммена в блоге all and sundry.