Пару недель назад на работе мы определили необходимость определения списка дат для представления, когда рабочий процесс 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 в Котлине 🎉