Привет! Меня зовут Андрей, я iOS-разработчик приложения «Пункт Ozon». С помощью него сотрудники пунктов выдачи Ozon выдают посылки, принимают возвраты, проводят инвентаризации.

Мы хотим упростить работу с OpenAPI-спецификациями, внедрив кодогенерацию для автоматического создания кода на Swift из YAML- или JSON-файла спецификации. Это позволяет автоматизировать создание DTO, сделать их единообразными и повысить эффективность разработки. Кодогенератор также упрощает сетевой слой, генерируя методы API, наборы параметров, заголовки и т.д.
На WWDC `23 Apple представила свой Open Source-генератор Swift OpenAPI Generator, который как раз и решает эту задачу. Давайте рассмотрим, готов ли в текущем виде Swift OpenAPI Generator для решения нашей проблемы.
Какую проблему хотим решить с OpenAPI Generator в целом?
DTO, он же Data Transfer Object, мы создаём, чтобы передать сериализованные данные, полученные от бэкенда, в другие части приложения. Обычно алгоритм такой:
-
Находим в Swagger нужный нам метод;
-
Смотрим структуру моделей запроса и ответа;
-
Создаём DTO, придумывая ему имя;
-
Создаем Input — входящие данные для запроса, придумывая ему имя;
-
Создаем Output — полученные данные при выполнении запроса, придумывая ему имя.
enum ChatsDTO { struct Input: Encodable { let startDate: Date let endDate: Date } struct Output: Decodable { struct Chat: Decodable { let id: Int let unreadCount: Int let lastMessage: String } @LossyArray private(set) var chats: [Chat] } }
Такой подход имеет ряд проблем. Давайте рассмотрим их
-
Рутинная работа. Необходимо найти модель, придумать название, правильно перенести;
-
Переиспользование. Бэкенд создает модели и может переиспользовать их в различных запросах. В нашем же подходе при разработке мобильного приложения так просто переиспользовать модели не получается, потому что чаще всего одинаковые модели находятся в разных DTO;
-
Имена. Если нескольким разработчикам сразу нужны одинаковые DTO, им нужно синхронизироваться и создавать DTO заранее, чтобы не было различий в имени и реализации;
-
Актуальность. Нет возможности автоматически обновлять наши DTO при обновлении спецификации, нужно вручную сравнивать спецификацию и реализацию;
-
Если мы хотим найти конкретную реализацию определенного запроса из Swagger среди наших DTO, нужно несколько раз сопоставлять имена, что, согласитесь, неудобно и отнимает время.
Но есть и плюс:
-
при реализации DTO мы выбираем только те поля, которые собираемся использовать. Это помогает уменьшить количество ошибок сериализации при изменении контракта бэкендом.
А вот реализация методов для обращения к API имеет проблему дублирования — приходится несколько раз реализовывать один и тот же метод в разных классах.
Для решения этих проблем попробуем внедрить Swift OpenAPI Generator. Генератор позволит сохранить структуру, заведенную бэкендом, сохранить названия и автоматически соберёт Swift-код, удобный нам. Также это позволит иметь единую реализацию методов API, которую можно использовать в разных частях приложения.
Что из себя представляет Apple Swift OpenAPI Generator
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.
Спецификация
openapi: 3.0.1 info: title: Swagger Petstore description: 'This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.' termsOfService: http://swagger.io/terms/ contact: email: apiteam@swagger.io license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html version: 1.0.6 externalDocs: description: Find out more about Swagger url: http://swagger.io servers: - url: https://petstore.swagger.io/v2 - url: http://petstore.swagger.io/v2 tags: - name: pet description: Everything about your Pets externalDocs: description: Find out more url: http://swagger.io - name: store description: Access to Petstore orders - name: user description: Operations about user externalDocs: description: Find out more about our store url: http://swagger.io paths: /pet/{petId}/uploadImage: post: tags: - pet summary: uploads an image operationId: uploadFile parameters: - name: petId in: path description: ID of pet to update required: true schema: type: integer format: int64 requestBody: content: multipart/form-data: schema: properties: additionalMetadata: type: string description: Additional data to pass to server file: type: string description: file to upload format: binary responses: 200: description: successful operation content: application/json: schema: $ref: '#/components/schemas/ApiResponse' security: - petstore_auth: - write:pets - read:pets /pet: put: tags: - pet summary: Update an existing pet operationId: updatePet requestBody: description: Pet object that needs to be added to the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' required: true responses: 400: description: Invalid ID supplied content: {} 404: description: Pet not found content: {} 405: description: Validation exception content: {} security: - petstore_auth: - write:pets - read:pets x-codegen-request-body-name: body post: tags: - pet summary: Add a new pet to the store operationId: addPet requestBody: description: Pet object that needs to be added to the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' required: true responses: 405: description: Invalid input content: {} security: - petstore_auth: - write:pets - read:pets x-codegen-request-body-name: body /pet/findByStatus: get: tags: - pet summary: Finds Pets by status description: Multiple status values can be provided with comma separated strings operationId: findPetsByStatus parameters: - name: status in: query description: Status values that need to be considered for filter required: true style: form explode: true schema: type: array items: type: string default: available enum: - available - pending - sold responses: 200: description: successful operation content: application/json: schema: type: array items: $ref: '#/components/schemas/Pet' application/xml: schema: type: array items: $ref: '#/components/schemas/Pet' 400: description: Invalid status value content: {} security: - petstore_auth: - write:pets - read:pets /pet/findByTags: get: tags: - pet summary: Finds Pets by tags description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. operationId: findPetsByTags parameters: - name: tags in: query description: Tags to filter by required: true style: form explode: true schema: type: array items: type: string responses: 200: description: successful operation content: application/json: schema: type: array items: $ref: '#/components/schemas/Pet' application/xml: schema: type: array items: $ref: '#/components/schemas/Pet' 400: description: Invalid tag value content: {} deprecated: true security: - petstore_auth: - write:pets - read:pets /pet/{petId}: get: tags: - pet summary: Find pet by ID description: Returns a single pet operationId: getPetById parameters: - name: petId in: path description: ID of pet to return required: true schema: type: integer format: int64 responses: 200: description: successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' 400: description: Invalid ID supplied content: {} 404: description: Pet not found content: {} security: - api_key: [] post: tags: - pet summary: Updates a pet in the store with form data operationId: updatePetWithForm parameters: - name: petId in: path description: ID of pet that needs to be updated required: true schema: type: integer format: int64 requestBody: content: application/x-www-form-urlencoded: schema: properties: name: type: string description: Updated name of the pet status: type: string description: Updated status of the pet responses: 405: description: Invalid input content: {} security: - petstore_auth: - write:pets - read:pets delete: tags: - pet summary: Deletes a pet operationId: deletePet parameters: - name: api_key in: header schema: type: string - name: petId in: path description: Pet id to delete required: true schema: type: integer format: int64 responses: 400: description: Invalid ID supplied content: {} 404: description: Pet not found content: {} security: - petstore_auth: - write:pets - read:pets /store/order: post: tags: - store summary: Place an order for a pet operationId: placeOrder requestBody: description: order placed for purchasing the pet content: application/json: schema: $ref: '#/components/schemas/Order' required: true responses: 200: description: successful operation content: application/json: schema: $ref: '#/components/schemas/Order' application/xml: schema: $ref: '#/components/schemas/Order' 400: description: Invalid Order content: {} x-codegen-request-body-name: body /store/order/{orderId}: get: tags: - store summary: Find purchase order by ID description: For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions operationId: getOrderById parameters: - name: orderId in: path description: ID of pet that needs to be fetched required: true schema: maximum: 10 minimum: 1 type: integer format: int64 responses: 200: description: successful operation content: application/json: schema: $ref: '#/components/schemas/Order' application/xml: schema: $ref: '#/components/schemas/Order' 400: description: Invalid ID supplied content: {} 404: description: Order not found content: {} delete: tags: - store summary: Delete purchase order by ID description: For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors operationId: deleteOrder parameters: - name: orderId in: path description: ID of the order that needs to be deleted required: true schema: minimum: 1 type: integer format: int64 responses: 400: description: Invalid ID supplied content: {} 404: description: Order not found content: {} /store/inventory: get: tags: - store summary: Returns pet inventories by status description: Returns a map of status codes to quantities operationId: getInventory responses: 200: description: successful operation content: application/json: schema: type: object additionalProperties: type: integer format: int32 security: - api_key: [] /user/createWithArray: post: tags: - user summary: Creates list of users with given input array operationId: createUsersWithArrayInput requestBody: description: List of user object content: application/json: schema: type: array items: $ref: '#/components/schemas/User' required: true responses: default: description: successful operation content: {} x-codegen-request-body-name: body /user/createWithList: post: tags: - user summary: Creates list of users with given input array operationId: createUsersWithListInput requestBody: description: List of user object content: application/json: schema: type: array items: $ref: '#/components/schemas/User' required: true responses: default: description: successful operation content: {} x-codegen-request-body-name: body /user/{username}: get: tags: - user summary: Get user by user name operationId: getUserByName parameters: - name: username in: path description: 'The name that needs to be fetched. Use user1 for testing. ' required: true schema: type: string responses: 200: description: successful operation content: application/json: schema: $ref: '#/components/schemas/User' application/xml: schema: $ref: '#/components/schemas/User' 400: description: Invalid username supplied content: {} 404: description: User not found content: {} put: tags: - user summary: Updated user description: This can only be done by the logged in user. operationId: updateUser parameters: - name: username in: path description: name that need to be updated required: true schema: type: string requestBody: description: Updated user object content: application/json: schema: $ref: '#/components/schemas/User' required: true responses: 400: description: Invalid user supplied content: {} 404: description: User not found content: {} x-codegen-request-body-name: body delete: tags: - user summary: Delete user description: This can only be done by the logged in user. operationId: deleteUser parameters: - name: username in: path description: The name that needs to be deleted required: true schema: type: string responses: 400: description: Invalid username supplied content: {} 404: description: User not found content: {} /user/login: get: tags: - user summary: Logs user into the system operationId: loginUser parameters: - name: username in: query description: The user name for login required: true schema: type: string - name: password in: query description: The password for login in clear text required: true schema: type: string responses: 200: description: successful operation headers: X-Rate-Limit: description: calls per hour allowed by the user schema: type: integer format: int32 X-Expires-After: description: date in UTC when token expires schema: type: string format: date-time content: application/json: schema: type: string application/xml: schema: type: string 400: description: Invalid username/password supplied content: {} /user/logout: get: tags: - user summary: Logs out current logged in user session operationId: logoutUser responses: default: description: successful operation content: {} /user: post: tags: - user summary: Create user description: This can only be done by the logged in user. operationId: createUser requestBody: description: Created user object content: application/json: schema: $ref: '#/components/schemas/User' required: true responses: default: description: successful operation content: {} x-codegen-request-body-name: body components: schemas: ApiResponse: type: object properties: code: type: integer format: int32 type: type: string message: type: string Category: type: object properties: id: type: integer format: int64 name: type: string xml: name: Category Pet: required: - photoUrls type: object properties: id: type: integer format: int64 category: $ref: '#/components/schemas/Category' name: type: string example: doggie photoUrls: type: array xml: wrapped: true items: type: string xml: name: photoUrl tags: type: array xml: wrapped: true items: $ref: '#/components/schemas/Tag' status: type: string description: pet status in the store enum: - available - pending - sold xml: name: Pet Tag: type: object properties: id: type: integer format: int64 name: type: string xml: name: Tag Order: type: object properties: id: type: integer format: int64 petId: type: integer format: int64 quantity: type: integer format: int32 shipDate: type: string format: date-time status: type: string description: Order Status enum: - placed - approved - delivered complete: type: boolean xml: name: Order User: type: object properties: id: type: integer format: int64 username: type: string firstName: type: string lastName: type: string email: type: string password: type: string phone: type: string userStatus: type: integer description: User Status format: int32 xml: name: User securitySchemes: api_key: type: apiKey name: api_key in: header petstore_auth: type: oauth2 flows: implicit: authorizationUrl: https://petstore.swagger.io/oauth/authorize scopes: read:pets: read your pets write:pets: modify pets in your account
Давайте начнем внедрять генератор в наш пустой проект. Для этого создадим SPM-пакет и подключим необходимые зависимости, следуя документации:
// swift-tools-version: 5.8 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "PetsModule", platforms:[ .iOS(.v14), ], products:[ .library(name: "PetsModule", targets: ["PetsModule"]), ], dependencies:[ .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.1.0")), .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.0")), ], targets: [ .target( name: "PetsModule", dependencies:[ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), ], plugins:[ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"), ] ), ] )
Для работы плагина необходимо использовать две зависимости:
-
OpenAPIRuntime. Он включает в себя Core-логику и необходим для работы генератора.
-
OpenAPIGenerator. Собственно, сам генератор.
Подключим плагин с помощью.plugin, просто указав название пакета.
Теперь нам надо подготовить входные данные. Плагин требует два YAML-файла: файл спецификации, который должен называться openapi.yaml, и файл конфигурации, который должен называться openapi-generator-config. Файл спецификации был подготовлен выше. Давайте подготовим файл конфигурации.
Файл конфигурации выглядит таким образом:
generate: - types - client
В нём мы указываем, какие сущности нам нужно сгенерировать:
-
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 довольно типовая и выглядит так:
/// Find pet by ID /// /// Returns a single pet /// /// - Remark: HTTP `GET /pet/{petId}`. /// - Remark: Generated from `#/paths//pet/{petId}/get(getPetById)`. public func getPetById(_ input: Operations.getPetById.Input) async throws -> Operations.getPetById.Output { try await client.send( input: input, forOperation: Operations.getPetById.id, serializer: { input in let path = try converter.renderedRequestPath( template: "/pet/{}", parameters: [input.path.petId] ) var request: OpenAPIRuntime.Request = .init(path: path, method: .get) suppressMutabilityWarning(&request) try converter.setHeaderFieldAsText( in: &request.headerFields, name: "accept", value: "application/json" ) return request }, deserializer: { response in switch response.statusCode { case 200: let headers: Operations.getPetById.Output.Ok.Headers = .init() let contentType = converter.extractContentTypeIfPresent( in: response.headerFields ) let body: Operations.getPetById.Output.Ok.Body if try contentType == nil || converter.isMatchingContentType( received: contentType, expectedRaw: "application/json" ) { body = try converter.getResponseBodyAsJSON( Components.Schemas.Pet.self, from: response.body, transforming: { value in .json(value) } ) } else { throw converter.makeUnexpectedContentTypeError(contentType: contentType) } return .ok(.init(headers: headers, body: body)) case 400: let headers: Operations.getPetById.Output.BadRequest.Headers = .init() return .badRequest(.init(headers: headers, body: nil)) case 404: let headers: Operations.getPetById.Output.NotFound.Headers = .init() return .notFound(.init(headers: headers, body: nil)) default: return .undocumented(statusCode: response.statusCode, .init()) } } ) }
Все реализовано с помощью async/await.
Как применять на практике
Для начала создадим недостающие компоненты для работы сгенерированного клиента в демонстрационных целях:
import Foundation import OpenAPIRuntime import PetsModule final class NetworkClientFactory { private init() { } static func makePetsClient() -> PetsModule.Client { PetsModule.Client( serverURL: try! PetsModule.Servers.server1(), configuration: Configuration(dateTranscoder: .iso8601), transport: NetworkClientTransport(), middlewares: [NetworkClientMiddleware()] ) } } private final class NetworkClientTransport: ClientTransport { private let session = URLSession.shared func send(_ request: OpenAPIRuntime.Request, baseURL: URL, operationID: String) async throws -> OpenAPIRuntime.Response { var urlString = baseURL.absoluteString + request.path if let query = request.query { urlString += "?\(query)" } let newUrl = URL(string: urlString)! var urlRequest = URLRequest(url: newUrl) urlRequest.httpMethod = request.method.rawValue urlRequest.httpBody = request.body urlRequest.allHTTPHeaderFields = Dictionary(uniqueKeysWithValues: request.headerFields.map { (key: $0.name, value: $0.value) }) let (bodyData, urlResponse) = try await session.data(for: urlRequest) let code = (urlResponse as? HTTPURLResponse)?.statusCode ?? 0 let headers = (urlResponse as? HTTPURLResponse)?.allHeaderFields.map { HeaderField(name: $0.key as! String, value: $0.value as! String)} ?? [] return OpenAPIRuntime.Response(statusCode: code, headerFields: headers, body: bodyData) } } private final class NetworkClientMiddleware: ClientMiddleware { func intercept(_ request: OpenAPIRuntime.Request, baseURL: URL, operationID: String, next: @Sendable (OpenAPIRuntime.Request, URL) async throws -> OpenAPIRuntime.Response) async throws -> OpenAPIRuntime.Response { print("Intercepting: \(request), baseURL: \(baseURL)") return try await next(request, baseURL) } }
Таким образом, мы подготовили минимально необходимый набор классов, который включает создание запроса, отправку запроса, создание модели ответа и перехват запроса с помощью 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/. Этот генератор расширяемый, менее требователен к окружению, а также не требует добавления лишних зависимостей в проект. Мы в дальнейшем тоже рассмотрим возможность его внедрения в наш проект.
ссылка на оригинал статьи https://habr.com/ru/articles/769624/
Добавить комментарий