Я с удовольствием работал и получал отличную поддержку по созданию 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. |