Привет! Меня зовут Андрей, я iOS-разработчик приложения «Пункт Ozon». С помощью него сотрудники пунктов выдачи Ozon выдают посылки, принимают возвраты, проводят инвентаризации.  Мы хотим упростить работу с OpenAPI-спецификациями, внедрив кодогенерацию для автоматического создания кода на Swift из YAML- или JSON-файла спецификации. Это позволяет автоматизировать создание DTO, сделать их единообразными и повысить эффективность разработки. Кодогенератор также упрощает сетевой слой, генерируя методы API, наборы параметров, заголовки и т.д. На WWDC `23 Apple представила свой Open Source-генератор Swift OpenAPI Generator, который как раз и решает эту задачу. Давайте рассмотрим, готов ли в текущем виде Swift OpenAPI Generator для решения нашей проблемы. DTO, он же Data Transfer Object, мы создаём, чтобы передать сериализованные данные, полученные от бэкенда, в другие части приложения. Обычно алгоритм такой: Находим в Swagger нужный нам метод; Смотрим структуру моделей запроса и ответа; Создаём DTO, придумывая ему имя; Создаем Input — входящие данные для запроса, придумывая ему имя; Создаем Output — полученные данные при выполнении запроса, придумывая ему имя. Такой подход имеет ряд проблем. Давайте рассмотрим их Рутинная работа. Необходимо найти модель, придумать название, правильно перенести; Переиспользование. Бэкенд создает модели и может переиспользовать их в различных запросах. В нашем же подходе при разработке мобильного приложения так просто переиспользовать модели не получается, потому что чаще всего одинаковые модели находятся в разных DTO; Имена. Если нескольким разработчикам сразу нужны одинаковые DTO, им нужно синхронизироваться и создавать DTO заранее, чтобы не было различий в имени и реализации; Актуальность. Нет возможности автоматически обновлять наши DTO при обновлении спецификации, нужно вручную сравнивать спецификацию и реализацию; Если мы хотим найти конкретную реализацию определенного запроса из Swagger среди наших DTO, нужно несколько раз сопоставлять имена, что, согласитесь, неудобно и отнимает время. Но есть и плюс: при реализации DTO мы выбираем только те поля, которые собираемся использовать. Это помогает уменьшить количество ошибок сериализации при изменении контракта бэкендом. А вот реализация методов для обращения к API имеет проблему дублирования — приходится несколько раз реализовывать один и тот же метод в разных классах. Для решения этих проблем попробуем внедрить Swift OpenAPI Generator. Генератор позволит сохранить структуру, заведенную бэкендом, сохранить названия и автоматически соберёт Swift-код, удобный нам. Также это позволит иметь единую реализацию методов API, которую можно использовать в разных частях приложения. Swift OpenAPI Generator — это кодогенератор, позволяющий по входящей OpenAPI-спецификации генерировать готовый сетевой клиент, включающий реализацию методов API, моделей входных и выходных данных. Также он имеет возможность сгенерировать реализацию сервера для такого API, используя Swift и фреймворк Vapor. Проект разделён на несколько подпроектов: генератор и runtime. Документация достаточно скудна, но для внедрения этого инструмента, на первый взгляд, хватает. Стоит отметить, что Swift OpenAPI Generator требовал iOS 15.0 для работы, но затем с обновлением минимальная версия iOS была понижена до 13.0. Это ещё один случай улучшения обратной совместимости от Apple, как было с async/await.  Генерация основывается на mustache-шаблонах, в которых указываем структуру сгенерированного файла. Просмотрев исходный код генератора и рантайма, обнаружил первую проблему генератора — невозможность повлиять на процесс генерации. Например, Нельзя сделать декодирование массивов lossy. То есть, если один из элементов массива не был сериализован и бросил ошибку, весь массив данных не будет сериализован; Client генерируется только в стиле async/await, нельзя его сгенерировать в стиле «до async/await»; Нельзя скорректировать шаблоны генерации; Нельзя удалить неиспользуемые поля или методы. Если контракт с бэкендом по каким-то причинам поменялся, наличие в коде неиспользуемых полей повышает вероятность ошибок сериализации. К тому же, лишний код попадает в сборку приложения, увеличивая ее размер; Не поддерживаются схемы, в которых есть ссылки на сторонние JSON-спецификации. Swift OpenAPI Generator можно наиболее удобно внедрить в проект несколькими способами: Как SPM Plugin; Как отдельный target. Эти способы очень хорошо укладываются в модульную архитектуру по правилу «одна спецификация — один модуль». Какие в этом плюсы? У каждого модуля имеется собственное пространство имён; Модуль импортируется «по требованию»; Модули становятся более изолированными и независимыми. Рассмотрим подключение как SPM plugin, потому что, на мой взгляд, это наиболее простой способ, решающий нашу задачу. Для тестирования воспользуемся публичным API: https://petstore.swagger.io/. Здесь можно получить Swagger-спецификацию, которую можно конвертировать в OpenAPI-спецификацию. Использовать будем Xcode 14.3.1, а OpenAPI Generator версии 0.1.10.  Давайте начнем внедрять генератор в наш пустой проект. Для этого создадим SPM-пакет и подключим необходимые зависимости, следуя документации: Для работы плагина необходимо использовать две зависимости: OpenAPIRuntime. Он включает в себя Core-логику и необходим для работы генератора. OpenAPIGenerator. Собственно, сам генератор. Подключим плагин с помощью.plugin, просто указав название пакета.  Теперь нам надо подготовить входные данные. Плагин требует два YAML-файла: файл спецификации, который должен называться openapi.yaml, и файл конфигурации, который должен называться openapi-generator-config. Файл спецификации был подготовлен выше. Давайте подготовим файл конфигурации. Файл конфигурации выглядит таким образом: В нём мы указываем, какие сущности нам нужно сгенерировать: Types активирует генерацию DTO; Client активирует генерацию абстрактного сетевого слоя, который будет использовать сгенерированные DTO; Server генерирует готовую реализацию сервера по спецификации API. Нам нужны именно types и client. Таким образом, наш модуль будет выглядеть так: Давайте соберем наш пустой проект, в котором будет подключен PetsModule. Сборка такого проекта заняла 105 секунд при использовании MacBook Pro M1, Xcode 14.3.1. Это вторая проблема нового генератора — очень долгое время сборки зависимостей. На схеме изображён таймлайн процессов сборки. Из основного: Сборка компонентов генератора «с нуля» занимает 1.5 минуты; Генерация моделей по спецификации занимает 5,7 секунды; При замене спецификации пересборка компонентов генератора не происходит. Время тратится непосредственно на генерацию. Например, генерация моделей по YAML размером в 18 КБ занимает примерно 5 секунд, из которых 3 секунды компилировались сгенерированные файлы. Если подать на вход генератору файл размером в 109 КБ, генерация займет уже около 26 секунд, из которых 20 секунд занимает компиляция сгенерированных файлов. Итак, генерация и компиляция произошли. Здесь проявляется третья проблема генератора. Генератор не поддерживает спецификации, которые позволяют работать с разными content type, и даёт предупреждение об этом: warning: Feature "Multiple content types" is not supported Также обратите внимание, что multipart/form-data тоже не поддерживается. Вернёмся к результатам генерации. Какие файлы мы получили? Мы получаем файлы: Client.swift. В нём сгенерирован абстрактный клиент для работы с API; Server.swift. В нём сгенерирован сервер для выдачи результатов по нашему API. В нашем случае он пуст, т.к. Server мы не генерировали; Types.swift. В нём сгенерированы модели для создания запросов на сервер и сериализации ответов. Эти файлы расположены в Derived Data по такому пути: /Users/{username}/Library/Developer/Xcode/DerivedData/{projectName}/SourcePackages/plugins/petsmodule.output/PetsModule/OpenAPIGenerator/GeneratedSources/ Это четвёртая проблема генератора. Получение доступа к ним из Xcode может вызвать сложности, т.к. по ⌘+⇧+O к ним не добраться, а по ⌘+клик Xcode может и не найти. С другой стороны, файлы не лежат в проекте и не создают merge conflict. Итак, рассмотрим файл Types.swift. Какова его структура? В этой схеме изображена структура полученного файла. В APIProtocol объявлены API-методы, их входные данные и выходные. Затем в разделе Servers перечислены все базовые серверы, которые были указаны в спецификации. В разделе Components реализованы все модели-компоненты. В Operations — каждый метод и соответствующий ему Input и Output. Теперь перейдем к реализации Client.swift. В файле реализована структура Client, которая основывается на универсальном HTTP-клиенте, транспортный слой которого вы должны реализовать сами, например, на основе стандартной URLSession. Также есть поддержка так называемых Middleware — возможности отлавливать запросы и корректировать их при необходимости, например, добавлять авторизационный токен. Также с помощью Middleware можно легко выдавать Mock-ответы на запросы, что пригодится при тестировании Реализация методов API довольно типовая и выглядит так: Все реализовано с помощью async/await. Для начала создадим недостающие компоненты для работы сгенерированного клиента в демонстрационных целях: Таким образом, мы подготовили минимально необходимый набор классов, который включает создание запроса, отправку запроса, создание модели ответа и перехват запроса с помощью Middleware. Также создана фабрика клиентов. Рассмотрим всю систему в целом. Если рассмотреть код детальнее, то видно, что для выполнения запроса на сервере нам нужно подготовить Input, который состоит из query, body, headers и другие параметров, и обработать Output таким образом, каким нам нужно. Обратите внимание, что все сущности строго типизированы, а это позволяет писать более безопасный код. Использование кодогенератора для клиента API по спецификации может существенно сократить время разработки и избавить от ряда проблем и ошибок. Это помогает уменьшить количество рутины и ускорить разработку, так как основной код для общения с сервером будет сгенерирован по единым правилам. Рассмотрим преимущества этого генератора: Генерирует DTO строго по спецификации, количество ошибок разработчиков уменьшается, уменьшается количество самописного кода; Создаёт готовый клиент для обращений к API; Имеет механизм, позволяющий добавлять любые заголовки: авторизационные заголовки, какую-то дополнительную информацию (протокол ClientMiddleware); Есть возможность кастомизировать декодирование дат (протокол DateTranscoder); Можно организовать модульную архитектуру. Однако он имеет и ряд недостатков: Нет поддержки Lossy Array. Для нас поддержка Lossy Array — это важно, т.к. позволяет сохранять работоспособность приложения при невалидных данных; Нет поддержки multipart/form-data. Для нас необходимо поддерживать такой content-type, поэтому его отсутствие является минусом; Нет поддержки внешних спецификаций (JSON). Это тоже важный момент, т.к. для упрощения спецификаций есть возможность добавлять ссылки на другие спецификации, а генератор не может это обработать; Нет возможности скорректировать шаблоны генерации. Мы бы могли добавить property wrapper, который добавляет Lossy Decoding, другие обертки, но не имеем возможности повлиять на генерацию. Таким образом, считаю, что продукт пока не готов для того, чтобы мы встроили его в наш проект. Отвечая на вопрос «готов ли OpenAPI Generator для продуктивного кода?», скажу, что нет, продукт еще сыроват и стоит дождаться дальнейших обновлений. Однако если у вас несколько разных API, вы свободно можете несколько из них перевести на кодогенерацию. Наиболее хорошо инструмент себя проявит тогда, когда вам надо собрать MVP какого-то проекта или функциональности, которая нужна либо для демонстрации, либо для какого-то теста. Если этот инструмент вам не подходит, предлагаю рассмотреть альтернативу — https://openapi-generator.tech/. Этот генератор расширяемый, менее требователен к окружению, а также не требует добавления лишних зависимостей в проект. Мы в дальнейшем тоже рассмотрим возможность его внедрения в наш проект.  Ведущий разработчик iOS-приложений в Ozon Tech