Пару недель назад на работе мы определили необходимость определения списка дат для представления, когда рабочий процесс Bitrise должен запускаться для приложения. Чтобы добавить контекст к этому, у нас есть два мобильных приложения (Android и iOS), которые сопоставляются с двумя приложениями в Bitrise. Каждое из этих приложений имеет разные рабочие процессы на Bitrise (для сборки, запуска тестов, линтинга, вырезания кандидатов на выпуск и т. Д.).
Вначале мы думали о создании класса данных Kotlin, который будет содержать все необходимые свойства для определения запуска, например:
а затем определяем наш «календарь» как список экземпляров этого класса:
Этого должно быть достаточно, но верно также и то, что синтаксис может быть гораздо более понятным для человека и менее подверженным ошибкам. По этой причине мы подумали о написании DSL для рендеринга чего-то вроде:
Дружественные людям свидания
Первое, что мы искали, - это более плавное определение дат. Для этого мы создали 12 функций расширения (по 1 в месяц).
Эти новые функции будут расширять тип Int
и ожидать еще один Int
в качестве параметра для значения года:
В этой функции this
- день месяца, а параметр - год. Это позволит нам определить LocalDate следующим образом:
24 October 2019
потому что функции были объявлены как инфиксные.
Календарь Bitrise DSL
Давайте подумаем о структурах данных, которые нам нужны для хранения данных нашего календаря. Календарь - это в основном список приложений, у которых есть ряд рабочих процессов, которые мы хотим запускать в определенные даты. Давайте сопоставим это с кодом:
DSL - это не что иное, как функции в контексте определенного домена. Нашим типом верхнего уровня для DSL является Calendar
. Чтобы начать его создание, мы можем создать функцию календаря, которая будет возвращать Calendar.
Обратите внимание, что эта функция будет ожидать другую функцию в качестве параметра с самим Calendar в качестве получателя. Итак, внутри лямбды this
будет календарь.
fun calendar(f: Calendar.() -> Unit): Calendar = Calendar().also(f)
Реализация просто создает экземпляр и применяет к нему лямбду. Мы используем также функцию Kotlin stdlib, чтобы сохранить ее как однострочную.
Что мы можем делать в календаре? Определение приложений (для которых мы позже определим рабочие процессы и триггеры). Как и в предыдущей функции, эта функция получит лямбду, а также дополнительный параметр для имени приложения.
Реализация почти такая же, создается экземпляр приложения, но также добавляется вновь созданное приложение в список приложений календаря:
После всего этого мы можем начать ощущать синтаксис DSL:
val calendar: Calendar = calendar { app(BitriseApp.ANDROID) { } }
Нам просто нужно сделать то же самое для класса App
:
и для Workflow
:
Наконец-то у нас появился крутой синтаксис, с которого мы хотели начать.
Объем вызовов получателя
Мы можем сделать дальнейшие улучшения, чтобы избежать того, чтобы пользователи DSL делали вещи, которые могут привести к путанице, например:
С точки зрения читателя это выглядит как приложение, содержащее рабочий процесс, содержащий приложение? Это не имеет смысла. Путаница возникает из-за того, что внутренний вызов приложения использует не this
, а this@calendar
. Хорошая новость в том, что Котлин спасает жизнь! Требуется явное объявление this@calendar
(на случай, если это будет иметь смысл по какой-либо причине). Давай сделаем это:
Первый шаг включает создание аннотации, которая сама аннотируется @DslMarker
:
@DslMarker private annotation class CalendarDsl
Затем нам также нужно аннотировать все задействованные классы в DSL с помощью @CalendarDsl
, и все готово. С этим изменением компилятор жалуется на вызовы функций, не предназначенных для внутреннего получателя:
забавное приложение (bitriseApp: BitriseApp, f: App. () - ›Unit): приложение не может быть вызвано в этом контексте неявным получателем. При необходимости используйте явный
fun trigger (date: LocalDate): триггер не может быть вызван в этом контексте неявным получателем. При необходимости используйте явный
Счастливые DSL в Котлине 🎉