惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
WordPress大学
WordPress大学
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
罗磊的独立博客
S
SegmentFault 最新的问题
V
V2EX
V
Visual Studio Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
Y
Y Combinator Blog
MyScale Blog
MyScale Blog
D
Docker
Google DeepMind News
Google DeepMind News
Blog — PlanetScale
Blog — PlanetScale
M
Microsoft Research Blog - Microsoft Research
Martin Fowler
Martin Fowler
S
Secure Thoughts
B
Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Recent Announcements
Recent Announcements
MongoDB | Blog
MongoDB | Blog
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
T
True Tiger Recordings
GbyAI
GbyAI
P
Proofpoint News Feed
P
Privacy International News Feed
Jina AI
Jina AI
The Cloudflare Blog
I
Intezer
AWS News Blog
AWS News Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security Archives - TechRepublic
NISL@THU
NISL@THU
The Register - Security
The Register - Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Palo Alto Networks Blog
S
Schneier on Security
L
LINUX DO - 热门话题
C
CXSECURITY Database RSS Feed - CXSecurity.com
Security Latest
Security Latest
C
Cybersecurity and Infrastructure Security Agency CISA

Все публикации подряд на Хабре

Как «спят» вкладки в браузере Приоритет задач определяется не только ощущением срочности [Перевод] Махинации с прибылью Anthropic Project Loom: Virtual Threads, Scoped Values и preview #7 Structured Concurrency Мнения математиков о том, как ИИ опроверг гипотезу Эрдёша Слабоумие и отвага: как я за выходные сделала прототип ИИ-помощника для UX-дизайнера ИИ учит нас писать лучше. Или хуже? Как проектировать ИИ-инструменты, которые делают пользователей лучше «Раньше хотел каждый, сейчас и бесплатно не надо»: гаджеты, про которые мы все забыли ИИ-агенты в бизнесе: почему 80% компаний увольняют людей, но не получают ROI Как я строил ИИ-стартап, или Новые архитектурные риски 2026 4 интересных парадокса, рождающих жаркие дискуссии Рабочее место не-вайбкодера: настраиваем harness Когнитивный инжиниринг Feature Based Clean Architecture. Часть 1: Эволюция NestJS-приложения в неподдерживаемое состояние Как мы перестали бояться «пустых охватов» и сделали инфлюенс-маркетинг управляемым каналом роста Подключили B2B email-платформу к голосовым ассистентам через MCP. Архитектура, код, где ломается [Перевод] Почему AI-агенты ломаются на длинных задачах — и как обвязка помогает им дописывать приложения Облачно, возможны нейросети: кризис датасетов и ахиллесова пята систем машинного зрения — DIY-чтение на выходные Спустя 5 лет и $5 миллионов: почему создание нового языка для веб-разработки оказалось ошибкой Безопасная песочница Облачная LLM на 16 ГБ VRAM — часть 2: LangGraph Server, LangSmith и SDK Современный SSH-клиент для MS-DOS Как продвигать агентство недвижимости: от вывески до прямых эфиров MCP для GitHub + GitLab: инженерный гайд 2026 Вы платите OpenAI $20 в месяц, а он зарабатывает на вас ещё $100 млн за полтора месяца. И это только начало ИИ забирает работу «белых воротничков»: чему учить детей, чтобы выжить в будущем Практический ИИ-агент Python: LangGraph + Qdrant Как я делал ping и traceroute на iOS без entitlements — и почему это оказалось проще, чем UMP-консент для AdMob 4 MVP за 4 месяца, 30 холодных DM, 1 регистрация: building in public по-русски VPS-бастион: доступ к домашнему серверу без белого IP Kampus AI — нейросеть для генерации учебных работ для студентов и школьников Игры, помогающие продавать — примеры интересных рекламных акций с видеоиграми €500 в Telegram Ads принесли сделку на 350 000 ₽. Разбор B2B-кампании Чтение на выходные: «Разработка игр и теория развлечений» Рафа Костера Личный архив: сбор, бэкап, таймлайн фотографий INFOSTART TECH EVENT или INFOSTART A&PM EVENT — как понять, куда вам нужнее? Peer testing на основе Закона Линуса Релиз GitLab 19.0: ИИ-оркестрация, которая наконец-то догнала темп написания кода Как бизнесу оценить готовность к аттестации по новому Приказу ФСТЭК № 117 Технический гайд по сторис – часть 4: как мы добавили видео формат Представительство в арбитражном процессе: правовые различия между внешним защитником и инхаусом «Где новые фичи?» — Как AI-миграция легаси вернет IT-бюджет бизнесу Что нужно знать работнику про увольнение Новые требования Москвы к ЦИМ для АГР: готовый инструмент для проектировщиков в nanoCAD BIM Строительство WireGuard: простота и надёжность современного VPN-туннеля или секретное рукопожатие в тёмной комнате Выйдет ли GTA 6 в 2026 году, и чего ждать от игры Как меня назвали «невовлечённым», а я нашёл офшоры на Кипре Как LLM научила рекомендательную модель видеть больше, чем историю взаимодействий От хаоса к экосистеме: Модель зрелости комьюнити в бизнесе Свет, тьма, VEML7700 и Python Сказ о том, как мы процессы разработки в GRI меняли. Часть 2 Майский «В тренде VM»: громкие уязвимости в Linux, ActiveMQ, SharePoint и Acrobat Reader Статический анализ, заряженный ИИ: как LLM ищут уязвимости в коде и где их границы Блок “Процессы” и почему мы называем его нашим мини-n8n Как поменялся рынок интернет-рекламы: сравнение первых кварталов 2025 и 2026 годов: исследование click.ru Мониторинг Kerio Connect через Zabbix 7: разбор шаблона без агентов и regex по DAT 671 Allow в Claude Code за день: как родился сетап Spec-build 3 известные интересные задачи на логику Как айтишнику позаботиться о менталке и не перерабатывать OpenAI vs Anthropic: битва экс-коллег за корпоративного клиента и $1 трлн на IPO SEO для интернет-магазина в 2026: что поменялось и как с этим работать Сможете ли вы спроектировать Maven‑монорепозиторий для 5 микросервисов? 6 неудобных вопросов про американское произношение, которые айтишники боятся задать Неожиданная встреча: теория графов вновь помогла решить проблему в анализе Фурье Иллюзия трансформации: почему компании платят за спектакль вместо изменений AMD представила Ryzen 9 PRO 9965X3D и еще 5 процессоров, которые пойдут далеко не всем История IDE в Google Первые отзывы на новинки о System Design Влияние параметра planner_upper_limit_estimation на планы выполнения и профиль нагрузки PostgreSQL при использовании 1C Границы 100% разработки с агентами Быстрый OCR на основе Paddle Дооснащение любительской электровакуумной мастерской. Вакуумметр, течеискатель, полярископ Mythos: модель, о которой Anthropic не говорит. Реверс по жертвам — от 27-летней дыры в OpenBSD до побега из песочницы Как использовать Qwen3.7-Max и Grok Build 0.1 для ИИ-агентов в России Suricata IPS NFQueue with nDPI. Часть VI Важные изменения в защите информации в России: что нового? В чем секрет достоверного замедления биологического старения? Вредное ускорение: Умный светофор на перегруженных перекрестках Как сисадмин написал свою библиотеку для Jira на Ruby: история Rujira Сломанный найм: почему рынок труда превратился в казино и что с этим делать Физики нашли свидетельства того, что Вселенная не идеально однородна, вопреки стандартной модели космологии Вопросы на собеседованиях, к которым лучше готовиться заранее Что детектировал детектор таксофонных карт? Как работают выделенные ядра в облачном сервере: от планировщика Linux до тестов производительности Математика кластеров: разбираемся в умной кластеризации данных на примере нашей системы поиска аномалий в логах. Часть 1 Ответы с «деврел‑супервизии», вопрос седьмой: выгорание, когда от вас ждут вечный драйв и креатив История одного // todo, который ждал своего часа пол года Если пропустили Claude последние 3 месяца: топ-5 фич с юзкейсами и история про $400K в Bitcoin Проектируем с нуля калькулятор на FPGA. Части 4 и 5: Фреймворк и оборудование Почему 10× от AI могут дать только лояльные сотрудники Speech-to-LaTeX: распознавание математических выражений и предложений в LaTeX Что внутри портфолио продуктовых и ux/ui-дизайнеров из Т-Банка, Додо, Figma, Альфы, Revolut? Чем заменить Excel в 2026 году: обзор российского ПО и других аналогов Как Rust обрабатывает repr и ABI на границе с C: что ломается и почему 5 промтов, чтобы подготовить презентацию в нейросетях через SpeShu.AI Каггл «200 ёлочек 2025»: призы уже раздали, но мы и за идею задачу укладки порешаем. Часть 1 Как ФНС стала data-driven за 5 лет: минус треть штата, плюс 20 новых цифровых сервисов Как настроить кастомную авторизацию в FESB и сохранить стандартный заголовок Как CISO защищаются от прошлого, игнорируя будущее
Swagger-генерация на Android. Часть 1. Выбираем инструменты и решаем проблемы
kartollika ( · 2026-05-14 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение14 мин

Охват и читатели1.7K

Кейс

Почти любое мобильное приложение общается с бэкендом, а значит живёт по контракту — чаще всего в виде OpenAPI-схемы. Смотреть на неё удобнее всего в Swagger.

Пока проект маленький, изменения в API можно отслеживать вручную. Но по мере роста схемы и команды ручная проверка становится дорогой и начинает регулярно ломаться.

Автогенерация из OpenAPI решает проблему, но в многомодульном Android-проекте всплывают нюансы: где хранить код, как не тянуть лишнее, как вписать сгенерённый код в архитектуру.

Привет! Меня зовут Дима Максимов, я Android-разработчик в Дринкит. В этом цикле из 2 статей я расскажу о том, как настроить генерацию из Swagger в Kotlin-код, и о том, как обуздать автогенерацию в условиях многомодульного проекта.

Схема генерации. The best way

Сгенерировать код из Swagger просто в том случае, если:

  • у вас нет строгих ограничений по виду проекта;

  • сетевой код можно целиком сложить в одном месте.

С другой стороны – есть наш проект Дринкит. И тут мы сталкиваемся с несколькими сложностями одновременно:

1) Отдельные модули под каждую фичу. Согласно принципам Clean Architecture, каждый наш модуль имеет разделение на -api (domain), -impl (data) и presentation. В итоге проект имеет следующую структуру:

root
├── infra
│   ├── lib_1
│   ├── lib_2
│   └── ...
└── contexts
    ├── common
    │   ├── cart
    │   │   ├── domain-api
    │   │   └── domain-impl
    │   └── ...
    ├── cart
    │   ├── domain-api
    │   ├── domain-impl
    │   └── presentation
    ├── product
    │   ├── domain-api
    │   ├── domain-impl
    │   └── view
    └── ...

И в каждом модуле примерно такая структура:

cart
├── cart-api
│   ├── build.gradle
│   └── src/main/kotlin/ru/drinkit/common/cart
│       ├── Cart.kt
│       ├── CartItem.kt
│       ├── ConsumeCartRepository.kt
│       └── ... other cart related domain classes
└── cart-impl
    ├── build.gradle
    └── src/main/kotlin/ru/drinkit/common/cart
		├── ConsumeCartRepositoryImpl.kt
        ├── api
        │   └── CartApi.kt
        └── dto
            ├── CartDto.kt
            ├── CartLineDto.kt
            └── ... other cart related DTOs

То есть в конкретном модуле лежат DTO-модели и сетевой интерфейс только конкретного домена. Для корзины — про корзину, для карточки продукта — про карточку. Для соблюдения нашей архитектуры необходимо найти способ генерировать код из Swagger в модули -impl.

2) Вторая проблема про разделение ответственности даже в условиях одинаковых контекстов. Например, в блоке со структурой проекта можно заметить два одинаковых модуля cart: в common и в contexts. И это не ошибка!

Дело в скоупах использования. Например, классы и интерфейсы из contexts:common:cart могут использоваться разными частями приложения. Часто они «живут» в скоупе всего приложения.

При этом модуль contexts:feature отвечает за конкретную фичу и экран. В нём часто требуются запросы, которые нужны только для того, чтобы нарисовать UI и получить специфичные для экрана данные.

Схема архитектуры многомодульности приложения

Схема архитектуры многомодульности приложения

Легко представить ситуацию, когда разным частям приложения нужна возможность отправлять запросы в сеть про корзину: добавить или удалить продукт, посчитать что-нибудь. Если такая возможность нужна многим частям приложения, мы помещаем контракт в contexts:common:some-feature.

Но на экране корзины мы отображаем блок с рекомендациям продуктов. Такой запрос нужен только на экране корзины, а значит он будет лежать в domain-impl в contexts:feature. Знает про него только экран корзины.

Получается, нам нужен механизм фильтрации API-запросов и DTO-моделей из OpenAPI схемы для конкретного модуля. То есть, проектируя экран, мы должны знать, какие запросы нужны, и генерировать только их.

Обобщая требования:

  1. Генерировать код в условиях многомодульности мы будем только в -impl модулях.

  2. Перед тем, как генерировать, нам нужно отфильтровать только то, что модулю необходимо, и ничего более.

Выбор инструмента

Посмотрим, чем пользуются в сообществе. Первое, что находим — openapi-generator и swagger-codegen. В свою очередь, openapi-generator — это форк swagger-codegen. Да, на деле это одно и то же, но всё-таки мы возьмём openapi-generator — у него больше звёздочек.

Мне ещё рассказывали про мультиплатформенную библиотеку moko-network. У библиотеки всего 150 звёздочек, но интересно, что несколько человек упомянули её в ходе обсуждения. Прикладываю её сюда — возможно, вам пригодится.

Я быстро оценил возможности каждого, уровень поддержки, наличие документации и число ресурсов про troubleshooting на StackOverflow и Github. В итоге выбрал openapi-generator.

Библиотеку можно использовать через CLI или через Gradle Plugin. Последний — обёртка CLI, но параметры для запуска можно удобно задать через Gradle Task openApiGenerate:

// build.gradle

openApiGenerate {
  generatorName = "kotlin"
  inputSpec = "openapi.json"
  modelPackage = "ru.drinkit.dto"
  apiPackage = "ru.drinkit.api"

  additionalProperties = [
      "library": "jvm-retrofit2",
      "serializationLibrary": "kotlinx_serialization",
      "useCoroutines": "true",
      "dateLibrary": "kotlinx-datetime",
  ]
}

У генератора много способов кастомизации. Все возможные параметры можно найти на странице с документацией.

Первая итерация. Генерация кода в одном месте

Начнём адаптацию с простого. Положим json-схему в корень проекта, а таску из блока выше определим в app-модуле. Запустим генератор через командную строку:

./gradlew openApiGenerate

По выполнении таски мы найдём в директории build/generate-resources… Целый Gradle-МОДУЛЬ!

Да, openapi-generator по умолчанию генерит целый модуль. Если вам нужно просто сгенерировать всё в одном модуле, который подключится к остальным — это отличное решение. Меняете outputDirectory, добавляете модуль в settings.gradle — и вуаля: модуль собирается в иерархии проекта.

Из дополнительного, что здесь генерируется по умолчанию:
  1. Папка .openapi-generate с различной метаинформацией.

  2. Документация на классы.

  3. Тесты.

Вам всё это не нужно? Просто отключите генерацию дополнительных элементов:

openApiGenerate {
	// Отключает тесты на модели и API
    generateModelTests = false
    generateApiTests = false
  
	// Задаёт конфиг с файлами, которые не надо генерировать
	// Например, строка `!**/src/main/java/**/*` в файле указывает, 
	// что должно генерироваться только содержимое `src/main/java/**`
    ignoreFileOverride = "openapi-generator-ignore"
}

Но результат нас не устраивает. Нам-то нужно генерировать код в разных модулях, предварительно отфильтровав нужные модулю сетевые запросы.

Проще сначала реализовать фильтрацию с генерацией в одном месте. Потом расширим это решение на многомодульность.

Изучим сгенерированный код

А какой код генерируется способом по умолчанию из openapi-generator? По правде говоря, не очень читаемый и совсем неудобный для использования.

DTO-модели все на одно лицо — у всех один и тот же префикс CoffeeApiContacts:

Внутри же код хорош, пусть у него и есть недостатки, про которые расскажу дальше:

/**
 *
 * Please note:
 * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * Do not edit this file manually.
 *
 */

@file:Suppress(
    "ArrayInDataClass",
    "EnumEntryName",
    "RemoveRedundantQualifierName",
    "UnusedImport"
)

package ru.drinkit.dto


import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Contextual

/**
 * 
 *
 * @param id 
 * @param isoCode 
 * @param isoAlpha3 
 */
@Serializable

data class CoffeeApiContractsMobileChainCurrencyDto (

		@Contextual @SerialName(value = "id")
    val id: java.util.UUID,

    @SerialName(value = "isoCode")
    val isoCode: kotlin.Int,

    @SerialName(value = "isoAlpha3")
    val isoAlpha3: kotlin.String

) {


}

Что хочется исправить в финальном варианте в DTO:

  1. Убрать приписку «Do not edit this file manually». В целом, она полезная, но мы решили убрать — надо разобраться, откуда она настраивается.

  2. Убрать бесполезные комментарии про поля data-класса.

  3. Suppress для детекта можно сделать общим, используя однострочную запись @file:Suppress("all").

  4. Некоторые типы по умолчанию мы хотим заменить. Например, поле id — это тип java.until.UUID. Мы же в проекте практически всегда используем String для такого. Если использовать java-библиотеку java.until.UUID, то в будущем при миграции на KMP (если такая произойдет) это сильно нас замедлит.

  5. Ну и, конечно, убрать приписки в названии классов. Вместо CoffeeApiContractsMobileChainCurrencyDto модель должна называться ChainCurrencyDto.

Теперь перейдём к API-интерфейсу. У Retrofit интерфейса тоже всё не так красиво, как хотелось бы

interface MobileApi {
  /**
   * POST api/v1/CalculateOrder
   * Calculates order total cost and costs for each order line
   *
   * Responses:
   *  - 200: Success
   *  - 404: Some entities not found
   *  - 429: Rate limit exceeded
   *
   * @param clientversion client version (default to "3.0.0")
   * @param clientid client id (default to "caa")
   * @param locale  (optional)
   * @param coffeeApiContractsMobileOrdersCalculateOrderRequestDto  (optional)
   * @return [CoffeeApiContractsMobileOrdersCalculateOrderResponseDto]
   */
  @POST("api/v1/CalculateOrder")
  suspend fun apiV1CalculateOrderPost(
    @Header("clientversion") clientversion: kotlin.String = "3.0.0",
    @Header("clientid") clientid: kotlin.String = "caa",
    @Query("locale") locale: kotlin.String? = null,
    @Body coffeeApiContractsMobileOrdersCalculateOrderRequestDto: CoffeeApiContractsMobileOrdersCalculateOrderRequestDto? = null
  ): Response<CoffeeApiContractsMobileOrdersCalculateOrderResponseDto>
  
  // .. other API methods
}

Что хочется изменить:

  1. ВСЕ методы находятся в одном файле MobileApi. Это особенность нашего бэкенда. Но менять его структуру дорого, поэтому придётся придумать хитрое решение на клиенте.

  2. Название REST-метода также выглядит неуместно. Метод apiV1CalculateOrderPost() должен называться calculateOrder(). Это тоже неудобство, связанное с устройством бэкенда.

  3. Названия передающихся в параметры DTO — длинные и неудобные. Об этом в пятом пункте про DTO.

  4. Некоторые параметры — лишние, поскольку часть из них добавляется в каждом сетевом запросе через механизм okhttp3.Interceptor. Автоматически добавляются clientVersion, clientId, locale и ещё несколько.

  5. Документацию тоже уберем, для нас она не несет никакой практической пользы.

Получается, самое трудное — справиться с форматом OpenAPI.json, который отдаёт бэкенд. Рассмотрим его проблемы подробнее и узнаем, как с ними смириться справиться.

Проблемы с нашим OpenAPI

Посмотрим на openapi.json вблизи. Схема по стандарту OpenAPI состоит из 4 частей. Самые важные из них — paths и components:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Coffee.Api"
  },
  "paths": [
    ...
  ],
  "components": [
    ...
  ]
}

Изучив их, найдём, что нам мешает сгенерировать «правильный» код. Узнаем, как решить эти проблемы.

Методы в OpenAPI-схеме

В paths лежат REST-методы из контракта бэкенда. Исторически тут лежит всё для киоска, для Payment, для колл-центра и прочее. Нам нужно только то, что помечено /api/v*/ — это методы для клиентского приложения:

{
  "paths": {
    "/api/v1/GetUnits": {},
    "/api/v1/CalculateOrder": {},
    "/api/v2/CalculateOrder": {},
	  ...
  }
}

Внутри каждого метода — его детальное описание:

  • тип REST-метода с описанными входными значениями в parameters;

  • наличие Body-объекта для запроса в requestBody;

  • тег для группировки их в Swagger-интерфейcе;

  • возможные ответы в responses.

"/api/v1/CalculateOrder": {
  "post": {
	  "tags":["Mobile"],
    "summary": "Calculates order total cost and costs for each order line",
    "parameters": [
      { "name": "Locale", ... }
    ],
    "requestBody": {
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Coffee.Api.Contracts.Mobile.Orders.CalculateOrderRequestDto"
          }
        }
      }
    },
    "responses": {
      "200": { "description": "Success", ... }
    },
    "security": [...]
  }
}

Поле tag нужно для разделения методов на группы в интерфейсе Swagger. Кроме того, в интерфейсе с таким тегом будет сгенерирован код endpoint'ов.

Как мы видим, название метода в Kotlin, которое выглядит как apiV1CalculateOrderPost, генерируется по принципу path+method:

/api/v1/CalculateOrder + post = apiV1CalculateOrderPost

А ещё в requestBody у моделей очень длинный неймспейс, который превращается в название класса:

/components/schemas/Coffee.Api.Contracts.Mobile.Orders.CalculateOrderRequestDto -> CoffeeApiContractsMobileOrdersCalculateOrderRequestDto

Модели в OpenAPI-схеме

Что насчёт моделей и что там не так? В целом, все точно так же:

"Coffee.Api.Contracts.Mobile.Menus.ProductDto": {
  "required": [
    "id",
    "code",
    ...
  ],
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "prices": { "type": "array"... },
    "code": { "type": "integer"... },
    "sizeType": { "$ref": "#/components/schemas/Coffee.Api.Contracts.Mobile.Menus.ProductSizeTypeDto"... },
    ...
  }
}

Неймспейс бы сократить, да и некоторые типы параметров не соответствуют тому, что мы используем на клиенте. В примере выше — id типа uuid, хотя мы преобразуем это в String.

Не хватает скрипта, который оригинальный JSON исправит, учитывая требования, перечисленные в этом разделе. Но есть ли другие опции? В теории — да, но, возможно, они более дорогие:

  1. Заставить бэкендеров переписать контракты, но в больших командах это может быть трудно или даже невозможно, если такая структура была заложена изначально. Повезёт, если у вас получится.

  2. Создать свой генератор. Библиотека поддерживает множество генераторов (в нашем случае — kotlin), но мысль «написать cвоё» однажды придёт естественным образом.

    Можно ли эту мысль реализовать? Да, но процесс будет таким сложным, что лезть совершенно не захочется. Если коротко, то:

    - нужно завести проект;
    - подключить зависимость на openapi-generator;
    - реализовать генератор, разобравшись, как это сделать;
    - собрать Jar;
    - использовать его через CLI.

В общем, оценив «за» и «против» каждого из вариантов, я решил быстро написать Python-скрипт для предобработки JSON. И не ошибся!

Создаём Python-скрипт для предобработки JSON

Скрипты для GitHub Action и другие подобные вещи мы пишем на Python. И в отличие от остальных скриптов, этот — достаточно сложный. Причём и в теории, и на практике.

«Питоном» я не владею, но способен прочитать код и понять, адекватный ли он. Так что самое время заиспользовать ✨✨ LLM ✨✨

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

Скрипт будет генерировать код в отдельном месте, куда сложим и сам swagger json. Например, в папке projectDir/swagger-codegen.

Напомню, что требуется от скрипта:

  1. Фильтрация JSON только по REST-методу.

  2. Перечисление моделей, связанных только с REST-методом.

  3. Упрощение имён методов и моделей.

Через несколько итераций с LLM и уточнения того, как JSON должен выглядеть – получаем скрипт предпроцессинга: https://gist.github.com/kartollikaa/a7cd2aae259fd3dad9aced838b174c53

Как запустить? Можно вызывать напрямую через терминал:

# Пример как запустить скрипт через терминал
python3 filter_openapi_json.py openapi.json "/api/v1/GetRecentOrders"

Чтобы удобнее использовать скрипт и сделать сразу всё за одно нажатие, я решил добавить bash-файл для запуска:

  1. В начале происходит валидация входных данных из параметра командной строки.

  2. Он же запускает скрипт filter_openapi_json.py, передав ему необходимый endpoint.

  3. В папке swagger-codegen/ появляется файл filtered_openapi.json с подготовленной openapi схемой.

  4. Этот файл мы передаём в команду ./gradlew openApiGenerate.

Сам bash-скрипт я привёл ниже. Его уже просто можно брать и запускать:

#!/bin/bash

endpoint="${1}"

if [ -z "$endpoint" ]; then
  echo "Usage: $0 <endpoint>"
  echo "For example $0 \\"/api/v1/GetRecentOrders\\""
  exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

python3 "$SCRIPT_DIR/filter_openapi_json.py" "$SCRIPT_DIR/openapi.json" "$endpoint"

$SCRIPT_DIR/.././gradlew openApiGenerate --project-dir $SCRIPT_DIR/.. --input=$SCRIPT_DIR/filtered_openapi.json --rerun-tasks
rm "$SCRIPT_DIR/filtered_openapi.json"

Настройка генератора через Gradle-плагин

У openapi-generator есть удобный Gradle-плагин. Он нужен, чтобы сконфигурировать генератор под наши требования.

В скрипте выше и вызывается Gradle-таска openApiGenerate. Её нужно только правильно настроить.

Добавим в build.gradle конфигурацию генерации с помощью одноимённого блока openApiGenerate. Параметров много, их описания есть на странице документации. Я же расскажу про самые важные для нас.

openApiGenerate {
  generatorName = "kotlin"
  ignoreFileOverride = "${projectDir.path}/../swagger-codegen/.openapi-generator-ignore"
  outputDir = "${projectDir.path}/../swagger-codegen/generated/"
  templateDir = "${projectDir.path}/../swagger-codegen/templates"
  invokerPackage = "ru.drinkit"
  modelPackage = "ru.drinkit.dto"
  apiPackage = "ru.drinkit.api"
  generateModelTests = false
  generateApiTests = false
  generateApiDocumentation = false
  generateModelDocumentation = false

  typeMappings = [
      "string+uuid": "kotlin.String",
      "string+date-time": "java.util.Date"
  ]

  additionalProperties = [
      "library": "jvm-retrofit2",
      "serializationLibrary": "kotlinx_serialization",
      "useCoroutines": "true",
      "dateLibrary": "kotlinx-datetime",
  ]
}
  1. ignoreFileOverride – выделяет файлы, которые создавать не нужно: лишние директории и файлы, которые создаются для нового модуля.

    /**/docs/
    /**/gradle/wrapper/
    /**/org/openapitools/client/infrastructure/
    /**/.openapi-generator-ignore
    /**/build.gradle
    /**/gradlew
    /**/gradlew.bat
    /**/README.md
    /**/settings.gradle
    /**/proguard-rules.pro
  2. generatorName — используемый генератор. В нашем случае — Kotlin, поскольку мы генерируем Kotlin-код.

  3. typeMappings — преобразователь типов. Например, он отдаёт вам бэкенд ID в виде UUID, а вы договорились на клиентах, что это будет String. Ещё можно по-разному преобразовывать даты — главное определить сериализатор, если вы делаете что-то нестандартное.

  4. additionalProperties — для настройки используемых инструментов. Некоторые генераторы поддерживают дополнительные свойства. Например, Kotlin-генератору можно сообщить, какие библиотеки использовать для описания API, сериализации, времени, корутин и т.д.

  5. templateDir для определения своих шаблонов генерации. О них ниже.

Шаблоны для генерации

В таске openApiGenerate есть параметр templateDir. Он для нас особенно важен.

Код генератора мы извне никак не отредактируем. Однако, мы можем изменить, как будет выглядеть сгенерированный класс. Для этого используются «шаблоны», по которым генерируются код. Написаны они на языке Mustache, а выглядят примерно так:

@file:Suppress("all")
package {{apiPackage}}

import retrofit2.http.*
{{#imports}}{{#-first}}import {{modelPackage}}.*{{/-first}}{{/imports}}

{{#operations}}
public interface {{classname}} {
{{#operation}}
  @{{httpMethod}}("{{path}}")
  public suspend fun {{#lambda.camelcase}}{{operationId}}{{/lambda.camelcase}}(
  {{#hasPathParams}}
  {{#pathParams}}
    @Path("{{baseName}}") {{paramName}}: String,
  {{/pathParams}}
  {{/hasPathParams}}
  {{#hasQueryParams}}
  {{#queryParams}}
    @Query("{{baseName}}") {{paramName}}: {{dataType}}{{^required}}? = null{{/required}},
  {{/queryParams}}
  {{/hasQueryParams}}
  {{#hasBodyParam}}
    {{#bodyParam}}@Body {{#lambda.camelcase}}{{baseName}}{{/lambda.camelcase}}: {{baseType}}{{/bodyParam}},
  {{/hasBodyParam}}
  ): {{{returnType}}}{{^returnType}}Unit{{/returnType}}
{{/operation}}
}
{{/operations}}

Шаблоны для Kotlin лежат в официальном репозитории. Можно подробнее ознакомиться с файлами, по которым создаются... файлы (шаблоны?) и что-то поменять.

Например, можно убрать licenceInfo. Создадим в директории templateDir файл licenceInfo.mustache. В нём будет пусто, а при запуске генератора мы заметим пропажу этого блока:

licenseInfo отвечает за блок с комментарием в начале файла

licenseInfo отвечает за блок с комментарием в начале файла

Поскольку этот блок интегрируется в начало, добавим в шаблон строку:

@file:Suppress("all")

И теперь файлы не подвержены ошибкам со стороны детекта:

Вместо licenceInfo мы добавили игнорирование всех ошибок detekt

Вместо licenceInfo мы добавили игнорирование всех ошибок detekt

С помощью шаблонов можно сильно изменить вид ваших файлов. Например, отформатировать параметры с новых строк, убрать импорты, целые блоки кода, документацию или просто отчистить шаблон от ненужных библиотек.

Вид API-файла после того, как мы использовали шаблон

Вид API-файла после того, как мы использовали шаблон

Вид DTO-файла после того, как мы использовали шаблон

Вид DTO-файла после того, как мы использовали шаблон

Запускаем

На этом первая итерация — всё. Проверим работоспособность, вызвав в терминале:

./swagger-codegen/openapi_generate.sh "/api/v1/GetCustomerMenuView"

Получим множество сгенерированных файлов:

Внешний вид API-интерфейса:

package ru.drinkit.api

import retrofit2.http.*
import ru.drinkit.dto.*

public interface MobileApi {
  @GET("api/v2/GetCustomerMenuView")
  public suspend fun apiV2GetCustomerMenuViewGet(
    @Query("countryId") countryId: kotlin.Int? = null,
    @Query("unitId") unitId: kotlin.String? = null,
    @Query("collapseMenu") collapseMenu: kotlin.Boolean? = null,
  ): GetMenuViewResponseV2
}

Внешний вид DTO-моделей:

package ru.drinkit.dto

import ru.drinkit.dto.MenuViewDto
import ru.drinkit.dto.ProductRecommendationBundleDto
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Contextual

@Serializable
data class GetMenuViewResponseV2 (
    @SerialName(value = "menuView")
    val menuView: MenuViewDto,
    @SerialName(value = "productRecommendationBundle")
    val productRecommendationBundle: ProductRecommendationBundleDto? = null,
)

Всё уже выглядит хорошо, но над множеством вещей ещё надо поработать:

  1. Нейминг методов. Имя API метода выглядит как apiV2GetCustomerMenuViewGet(). Нужно получить getCustomerMenuView().

  2. Нейминг интерфейса. Сейчас название API интерфейса — MobileApi. Хочется, чтобы название отражало домен, для которого мы генерируем код. Например OrderApi.

  3. Декомпозиция API. Разнести методы по доменным интерфейсам (или хотя бы по tags , чтобы не держать весь контракт в одном файле.

  4. Многомодульность. Научить генерацию работать в многомодульном проекте: генерить код «куда надо», переиспользовать общие модели и не дублировать их.

И другие сложности — о них всех пойдёт речь во второй части. А пока отдаём команде инструмент для временного использования. Приступаем к его улучшению и реализации поддержки многомодульности.

Заключение

В первой итерации мы научились брать сырой openapi.json, забирать из него только нужный эндпоинт со всеми связанными моделями и генерить из этого Kotlin код через openapi-generator с кастомными шаблонами.

Этого уже достаточно, чтобы пользоваться инструментом руками — но пока всё генерится в одно место и многое приходится делать вручную.

Во второй части научим кодогенерацию работать в нашем многомодульном проекте, попутно разбираясь с сложностями: коллизиями имён, версионированием эндпоинтов, неймингами и прочими сложностями.

А с первой частью на этом всё. Пишите ваши мысли в комментарии, закидывайте плюсик в карму и подписывайтесь на Telegram-канал Dodo Engineering, чтобы оставаться в курсе последних новостей нашей команды.

Также заглядывайте в мой Telegram-канал «Android в тесте и маленький капучино», где я пишу про мобильную разработку