OpenAPI кодогенерация
Этот модуль генерирует код Kora из контракта OpenAPI с помощью OpenAPI Generator.
Из единого описания API можно создать декларативные обработчики HTTP-сервера или декларативные HTTP-клиенты,
а также модели запросов и ответов, мапперы, обработку авторизации и дополнительные аннотации.
Такой подход полезен, когда OpenAPI является источником истины для транспортного контракта, а код приложения должен автоматически ему следовать.
Если нужен пошаговый разбор перед справочным описанием, смотрите OpenAPI HTTP-сервер, продвинутый OpenAPI HTTP-сервер и OpenAPI HTTP-клиент.
Подключение¶
Зависимость генератора в build.gradle:
Зависимость плагина в build.gradle:
Работоспособность других версий плагина не гарантируется, поскольку API OpenAPI Generator может быть несовместимо на уровне кода.
Зависимость в build.gradle.kts:
Зависимость плагина в build.gradle.kts:
Работоспособность других версий плагина не гарантируется, поскольку API OpenAPI Generator может быть несовместимо на уровне кода.
Сгенерированному коду также требуется модуль HTTP-сервера или HTTP-клиента в зависимости от выбранного режима генерации.
Конфигурация¶
Настройте параметры плагина OpenAPI Generator:
- Параметры
Gradle-плагина описаны в документации плагина. - Параметр плагина
configOptionsописан в документации по конфигурации. - Параметр плагина
openapiNormalizerописан в документации по настройке.
Общие параметры¶
Помимо специфичных для Kora configOptions, GenerateTask принимает общие параметры OpenAPI Generator.
Они определяют, откуда читать контракт, куда помещать сгенерированные файлы, какие пакеты использовать и как предобрабатывать описание OpenAPI.
В проектах Kora эти параметры обычно задаются явно, поскольку сгенерированный код затем добавляется в обычную компиляцию проекта.
| Параметр | Описание |
|---|---|
generatorName |
Имя генератора (обязательный, без значения по умолчанию). Для Kora всегда указывайте kora. |
inputSpec |
Путь к файлу OpenAPI (обязательный, без значения по умолчанию). Обычно это файл в src/main/resources/openapi, например $projectDir/src/main/resources/openapi/openapi.yaml. |
outputDir |
Каталог для сгенерированных файлов (по умолчанию не указан, необязательный). В проектах Kora это обычно каталог в build, например $buildDir/generated/openapi, который добавляется в основной набор исходного кода (source set). |
apiPackage |
Пакет для сгенерированных интерфейсов API, контроллеров, классов delegate и мапперов (по умолчанию: org.openapitools.api). Рекомендуется указывать его явно, например ru.tinkoff.kora.example.openapi.api. |
modelPackage |
Пакет для моделей, сгенерированных из схем OpenAPI (по умолчанию: org.openapitools.model). Рекомендуется указывать его явно, например ru.tinkoff.kora.example.openapi.model. |
invokerPackage |
Вспомогательный пакет генератора (по умолчанию: org.openapitools.api). Рекомендуется указывать его явно рядом с apiPackage и modelPackage, например ru.tinkoff.kora.example.openapi.invoker. |
configOptions |
Специфичные для генератора параметры (по умолчанию: {}). Для Kora здесь задаются mode, clientConfigPrefix, enableServerValidation, interceptors и другие параметры, описанные ниже. |
globalProperties |
Ограничивает, какие сущности генерируются (по умолчанию: {}). Полезно, когда нужно сгенерировать только apis, только models или отдельные модели и операции. Используйте осторожно: обычным клиентам и серверам Kora, как правило, нужны классы API, модели и мапперы вместе. |
openapiNormalizer |
Предобрабатывает контракт OpenAPI перед генерацией (по умолчанию: {}). Часто используется, чтобы отключить стандартные преобразования через DISABLE_ALL, сгенерировать только выбранные операции через FILTER или управлять правилами вроде SIMPLIFY_ONEOF_ANYOF. |
importMappings |
Сопоставляет имя схемы с существующим классом (по умолчанию: {}). Полезно, когда модель написана вручную или приходит из другого модуля, например Money: "com.example.Money". |
typeMappings |
Сопоставляет тип OpenAPI Generator с типом языка (по умолчанию: {}). Используется для точечной замены типов, например замены OffsetDateTime на специфичный для проекта тип времени. |
schemaMappings |
Сопоставляет схему OpenAPI с внешним типом без генерации модели (по умолчанию: {}). Аналогично importMappings, но настраивается на уровне схемы и полезно для переиспользования общих DTO. |
skipValidateSpec |
Пропускает валидацию контракта OpenAPI перед генерацией (по умолчанию: false). В обычных сборках валидацию лучше оставлять включённой; используйте true только временно для внешних контрактов, которые нельзя быстро исправить. |
cleanupOutput |
Очищает outputDir перед генерацией (по умолчанию: false). Полезно, когда контракт часто меняется и файлы удалённых операций или моделей должны исчезать. Не указывайте в outputDir каталог с написанным вручную кодом. |
Пример с общими параметрами:
def openApiGenerateHttpClient = tasks.register("openApiGenerateHttpClient", GenerateTask) {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml"
outputDir = "$buildDir/generated/openapi/client"
def corePackage = "ru.tinkoff.kora.example.openapi"
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
skipValidateSpec = false
cleanupOutput = true
openapiNormalizer = [
DISABLE_ALL: "true",
FILTER: "tag:public|billing"
]
configOptions = [
mode: "java-client",
clientConfigPrefix: "httpClient.billing",
filterWithModels: "true"
]
}
val openApiGenerateHttpClient = tasks.register<GenerateTask>("openApiGenerateHttpClient") {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml"
outputDir = "$buildDir/generated/openapi/client"
val corePackage = "ru.tinkoff.kora.example.openapi"
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
skipValidateSpec = false
cleanupOutput = true
openapiNormalizer = mapOf(
"DISABLE_ALL" to "true",
"FILTER" to "tag:public|billing"
)
configOptions = mapOf(
"mode" to "kotlin-client",
"clientConfigPrefix" to "httpClient.billing",
"filterWithModels" to "true"
)
}
Используйте globalProperties только для узких задач генерации, например при извлечении нескольких моделей в промежуточный модуль:
Параметры нормализации¶
openapiNormalizer изменяет входной контракт OpenAPI перед генерацией. Это не параметр Kora, а общий механизм OpenAPI Generator.
Для Kora он особенно полезен, когда один большой контракт используется несколькими приложениями или когда контракт содержит неоднозначные для генерации кода конструкции.
| Правило | Описание |
|---|---|
DISABLE_ALL |
Отключает стандартные правила нормализации (по умолчанию: false). Начиная с OpenAPI Generator 7 некоторые правила включены по умолчанию, поэтому предсказуемая генерация часто начинается с DISABLE_ALL: "true", а затем явно включаются только нужные правила. |
FILTER |
Оставляет для генерации только выбранные операции (по умолчанию не указано, необязательно). Поддерживает один фильтр за раз: operationId:name1\|name2, method:get\|post или tag:public\|billing. Операции, которые не подходят, помечаются как x-internal: true и не генерируются. |
KEEP_ONLY_FIRST_TAG_IN_OPERATION |
Оставляет у операции только первый тег (по умолчанию: false). Полезно, когда у операций несколько тегов и они разбиваются на несколько классов API не так, как вы ожидаете. |
SET_TAGS_FOR_ALL_OPERATIONS |
Заменяет теги всех операций одним переданным значением (по умолчанию не указано, необязательно). Полезно, когда нужно принудительно получить один сгенерированный класс API. |
SET_TAGS_TO_OPERATIONID |
Устанавливает тег операции равным operationId, либо default, если operationId пуст (по умолчанию: false). Полезно для контрактов без пригодных тегов, когда нужна предсказуемая группировка операций. |
SET_TAGS_TO_VENDOR_EXTENSION |
Читает теги операций из указанного расширения, например x-tags (по умолчанию не указано, необязательно). Полезно, когда внешний контракт нельзя изменить, но в нём уже есть собственная группировка операций. |
FIX_DUPLICATED_OPERATIONID |
Добавляет числовой суффикс к повторяющимся значениям operationId (по умолчанию: false). Лучше исправить контракт, но это правило помогает временно сгенерировать код по внешнему описанию. |
SET_BEARER_AUTH_FOR_NAME |
Преобразует указанную схему безопасности в bearerAuth (по умолчанию не указано, необязательно). Полезно для внешних контрактов, где bearer-токен описан нестандартно, но в приложении его нужно обрабатывать как обычную схему bearer. |
REF_AS_PARENT_IN_ALLOF |
Помечает $ref внутри allOf как родительскую схему через x-parent: true (по умолчанию: false). Может помочь контрактам, которые моделируют наследование через allOf. |
SIMPLIFY_ONEOF_ANYOF |
Упрощает некоторые конструкции oneOf/anyOf, например переносит вариант null в nullable: true и убирает одиночные обёртки (включено по умолчанию в OpenAPI Generator 7, если не задан DISABLE_ALL). Для Kora это может менять форму сгенерированных моделей, поэтому включайте его осознанно. |
SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING |
Упрощает anyOf, составленный из string и строкового перечисления, до string (по умолчанию: false). Это может помочь с контрактами, где ограничение перечисления не важно для кода. |
SIMPLIFY_BOOLEAN_ENUM |
Преобразует булево перечисление в обычный boolean (включено по умолчанию в OpenAPI Generator 7, если не задан DISABLE_ALL). |
REFACTOR_ALLOF_WITH_PROPERTIES_ONLY |
Переносит свойства из схемы, содержащей одновременно allOf и properties, в отдельную схему внутри allOf (включено по умолчанию в OpenAPI Generator 7, если не задан DISABLE_ALL). Это может помочь наследованию, но строгие контракты стоит проверять после генерации. |
NORMALIZE_31SPEC |
Нормализует некоторые конструкции OpenAPI 3.1 в форму, которую генератор понимает лучше (по умолчанию: false). Полезно для контрактов 3.1, когда генерация не удаётся на новых формах схем. |
REMOVE_X_INTERNAL |
Удаляет x-internal: true из операций и моделей (по умолчанию: false). Используйте, только когда контракт уже содержит x-internal, но конкретная задача генерации должна принудительно вернуть такие операции. |
SET_CONTAINER_TO_NULLABLE |
Помечает типы-контейнеры array, set или map как nullable (по умолчанию не указано, необязательно). Используйте, только когда во внешнем контракте систематически отсутствует nullable у таких полей. |
SET_PRIMITIVE_TYPES_TO_NULLABLE |
Помечает примитивные типы string, integer, number или boolean как nullable (по умолчанию не указано, необязательно). Это существенно меняет сигнатуры моделей, поэтому применяйте его только к проблемным внешним контрактам. |
Пример генерации только публичной части контракта:
Сам по себе FILTER исключает только операции. Если после фильтрации нужно также удалить неиспользуемые модели, включите параметр Kora filterWithModels.
Для более сложного отбора обычно создают отдельные задачи генерации с разными значениями FILTER, например одну с tag:billing, а другую с operationId:createUser|getUser.
Пример нормализации тегов для контракта без удобной группировки:
Параметры моделей¶
Kora также поддерживает несколько configOptions, управляющих мапперами JSON и общей генерацией моделей.
Они не зависят от того, генерируется клиент или сервер.
| Параметр | Описание |
|---|---|
jsonAnnotation |
Аннотация-тег, используемая для внедрения мапперов JSON в сгенерированные мапперы запросов и ответов (по умолчанию: ru.tinkoff.kora.json.common.annotation.Json). |
objectType |
Тип для схем type: object без более точного описания. Java по умолчанию использует java.lang.Object, а Kotlin — kotlin.Any. Например, укажите com.fasterxml.jackson.databind.JsonNode, если приложение хочет обрабатывать произвольный JSON как дерево. |
disableHtmlEscaping |
Отключает экранирование HTML-символов в строках JSON (по умолчанию: false). Обычно значение по умолчанию оставляют. |
ignoreAnyOfInEnum |
Игнорирует anyOf при генерации перечислений (по умолчанию: false). Может помочь с контрактами, где перечисление описано через смешанные конструкции anyOf. |
discriminatorCaseSensitive |
Управляет чувствительностью к регистру при поиске значения дискриминатора для полиморфных (oneOf) моделей с дискриминатором (по умолчанию: true). Установите false, когда входящие значения дискриминатора могут отличаться регистром от определения в схеме. |
additionalModelTypeAnnotations |
Дополнительные аннотации на типах моделей (по умолчанию не указано, необязательно). Несколько аннотаций разделяются ;, например @Deprecated;@MyAnnotation. |
additionalEnumTypeAnnotations |
Дополнительные аннотации на типах перечислений (по умолчанию не указано, необязательно). Несколько аннотаций разделяются ;. |
Пример:
Несколько генераторов¶
В одном модуле можно зарегистрировать несколько задач GenerateTask, например чтобы сгенерировать два независимых контракта
или сгенерировать клиент для одного контракта и сервер для другого. Каждая задача пишет в один и тот же outputDir и добавляется в один и тот же набор исходного кода (source set),
поэтому единственное требование — чтобы сгенерированные пакеты не пересекались. Задайте каждой задаче собственные apiPackage/modelPackage/invokerPackage.
def openApiGeneratePetV2 = tasks.register("openApiGeneratePetV2", GenerateTask) {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV2.yaml"
outputDir = "$buildDir/generated/openapi"
def corePackage = "ru.tinkoff.kora.example.openapi.petV2" //(1)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = [mode: "java-client", clientConfigPrefix: "httpClient.petV2"]
}
sourceSets.main { java.srcDirs += openApiGeneratePetV2.get().outputDir }
compileJava.dependsOn openApiGeneratePetV2
def openApiGeneratePetV3 = tasks.register("openApiGeneratePetV3", GenerateTask) {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV3.yaml"
outputDir = "$buildDir/generated/openapi"
def corePackage = "ru.tinkoff.kora.example.openapi.petV3" //(2)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = [mode: "java-reactive-client", clientConfigPrefix: "httpClient.petV3"]
}
sourceSets.main { java.srcDirs += openApiGeneratePetV3.get().outputDir }
compileJava.dependsOn openApiGeneratePetV3
- Изолированный пакет для первого контракта
- Другой пакет для второго контракта, чтобы имена классов не конфликтовали
val openApiGeneratePetV2 = tasks.register<GenerateTask>("openApiGeneratePetV2") {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV2.yaml"
outputDir = "$buildDir/generated/openapi"
val corePackage = "ru.tinkoff.kora.example.openapi.petV2" //(1)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = mapOf("mode" to "kotlin-client", "clientConfigPrefix" to "httpClient.petV2")
}
kotlin.sourceSets.main { kotlin.srcDir(openApiGeneratePetV2.get().outputDir) }
tasks.withType<KspTask> { dependsOn(openApiGeneratePetV2) }
val openApiGeneratePetV3 = tasks.register<GenerateTask>("openApiGeneratePetV3") {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV3.yaml"
outputDir = "$buildDir/generated/openapi"
val corePackage = "ru.tinkoff.kora.example.openapi.petV3" //(2)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = mapOf("mode" to "kotlin-suspend-client", "clientConfigPrefix" to "httpClient.petV3")
}
kotlin.sourceSets.main { kotlin.srcDir(openApiGeneratePetV3.get().outputDir) }
tasks.withType<KspTask> { dependsOn(openApiGeneratePetV3) }
- Изолированный пакет для первого контракта
- Другой пакет для второго контракта, чтобы имена классов не конфликтовали
Клиент¶
Минимальная конфигурация плагина для создания декларативного HTTP-клиента:
Для клиентов configOptions.mode поддерживает java-client, java-async-client и java-reactive-client.
Остальные параметры клиента описаны ниже в разделах про авторизацию, перехватчики, теги, модели и неявные заголовки.
def openApiGenerateHttpClient = tasks.register("openApiGenerateHttpClient", GenerateTask) {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
def corePackage = "ru.tinkoff.kora.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = [
DISABLE_ALL: "true"
]
configOptions = [
mode: "java-client", //(6)!
clientConfigPrefix: "httpClient.myclient" //(7)!
]
}
sourceSets.main { java.srcDirs += openApiGenerateHttpClient.get().outputDir } //(8)!
compileJava.dependsOn openApiGenerateHttpClient //(9)!
- Путь к файлу
OpenAPI, по которому создаются классы - Каталог, где создаются сгенерированные файлы
- Пакет для делегатов, контроллеров и мапперов
- Пакет для моделей и DTO
- Вспомогательный пакет генератора
- Режим плагина
- Префикс пути конфигурации клиента
- Регистрирует сгенерированные классы как исходный код проекта
- Ставит компиляцию кода в зависимость от генерации классов HTTP-клиента: сначала генерация, затем компиляция
Для клиентов configOptions.mode поддерживает kotlin-client и kotlin-suspend-client.
Остальные параметры клиента описаны ниже в разделах про авторизацию, перехватчики, теги, модели и неявные заголовки.
val openApiGenerateHttpClient = tasks.register<GenerateTask>("openApiGenerateHttpClient") {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
val corePackage = "ru.tinkoff.kora.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = mapOf(
"DISABLE_ALL" to "true"
)
configOptions = mapOf(
"mode" to "kotlin-client", //(6)!
"clientConfigPrefix" to "httpClient.myclient" //(7)!
)
}
kotlin.sourceSets.main { kotlin.srcDir(openApiGenerateHttpClient.get().outputDir) } //(8)!
tasks.withType<KspTask> { dependsOn(openApiGenerateHttpClient) } //(9)!
- Путь к файлу
OpenAPI, по которому создаются классы - Каталог, где создаются сгенерированные файлы
- Пакет для делегатов, контроллеров и мапперов
- Пакет для моделей и DTO
- Вспомогательный пакет генератора
- Режим плагина
- Префикс пути конфигурации клиента
- Регистрирует сгенерированные классы как исходный код проекта
- Ставит компиляцию кода в зависимость от генерации классов HTTP-клиента: сначала генерация, затем компиляция
После генерации HTTP-клиент доступен для внедрения зависимостей через сгенерированный интерфейс.
Использование клиента¶
Для каждого тега API генератор создаёт интерфейс, аннотированный @HttpClient, с именем по тегу (например PetApi).
Он внедряется в компоненты как любой другой клиент Kora, без дополнительной регистрации:
Сгенерированный клиент читает конфигурацию по пути, заданному clientConfigPrefix, за которым следует имя сгенерированного интерфейса.
Для clientConfigPrefix = "httpClient.petV2" и интерфейса PetApi блок конфигурации — httpClient.petV2.PetApi.
Полный набор параметров клиента (url, requestTimeout, блоки для отдельных операций, telemetry) описан в документации по HTTP-клиенту:
httpClient.petV2.PetApi {
url = "https://localhost:8443" //(1)!
requestTimeout = "10s" //(2)!
getValuesConfig { //(3)!
requestTimeout = "20s"
}
telemetry.logging.enabled = true
}
- Базовый URL целевой службы
- Таймаут запроса по умолчанию для всех операций
- Блок переопределения для отдельной операции, названный по
operationId(здесьgetValues)
httpClient:
petV2:
PetApi:
url: "https://localhost:8443" #(1)!
requestTimeout: "10s" #(2)!
getValuesConfig: #(3)!
requestTimeout: "20s"
telemetry:
logging:
enabled: true
- Базовый URL целевой службы
- Таймаут запроса по умолчанию для всех операций
- Блок переопределения для отдельной операции, названный по
operationId(здесьgetValues)
Сигнатуры методов клиента зависят от выбранного mode:
| Режим | Пример возвращаемого типа |
|---|---|
java-client |
PetApiResponses.GetPetByIdApiResponse (блокирующее значение) |
java-async-client |
CompletionStage<PetApiResponses.GetPetByIdApiResponse> |
java-reactive-client |
Mono<PetApiResponses.GetPetByIdApiResponse> (требует reactor-core) |
kotlin-client |
PetApiResponses.GetPetByIdApiResponse (блокирующее значение) |
kotlin-suspend-client |
suspend fun ...: PetApiResponses.GetPetByIdApiResponse |
Каждый метод возвращает запечатанную (sealed) обёртку *ApiResponses, подтипы которой кодируют HTTP-статус, так же как это делают делегаты сервера.
Авторизация клиента¶
Если контракт OpenAPI описывает securitySchemes, генератор создаёт модуль ApiSecurity с компонентами для авторизации клиента.
Для apiKey и basic генерируются компоненты, читающие конфигурацию. Для bearer и oauth ожидается соответствующий помеченный тегом компонент HttpClientTokenProvider.
securityConfigPrefix задаёт общий префикс конфигурации авторизации. Если префикс не указан, путём конфигурации становится имя из securitySchemes.
Если у операции несколько схем авторизации, можно указать primaryAuth; иначе генератор выбирает одну из схем и пишет предупреждение в лог.
Если включён authAllowMultiple, генератор создаёт составной перехватчик, применяющий несколько схем авторизации последовательно.
Если включён authAsMethodArgument, данные авторизации добавляются в сигнатуру метода клиента вместо сгенерированного перехватчика.
apiKey и basic¶
Для схем apiKey и basic генератор создаёт читатели конфигурации @DefaultComponent и перехватчики, поэтому не требуется никаких компонентов — только значения конфигурации.
Путь конфигурации — это securityConfigPrefix, за которым следует имя схемы (или просто имя схемы, если securityConfigPrefix не задан).
Схема apiKey читает одну строку; схема basic читает объект username/password:
openapiAuth {
apiKeyAuth = "MyAuthApiKey" //(1)!
basicAuth { //(2)!
username = "user"
password = "password"
}
}
- Схема
apiKeyapiKeyAuth: значение, отправляемое сгенерированнымApiKeyHttpClientInterceptorв заголовке/параметре запроса/куки, объявленных схемой - Схема
basicbasicAuth: учётные данные, оборачиваемые сгенерированнымBasicAuthHttpClientInterceptor
openapiAuth:
apiKeyAuth: "MyAuthApiKey" #(1)!
basicAuth: #(2)!
username: "user"
password: "password"
- Схема
apiKeyapiKeyAuth: значение, отправляемое сгенерированнымApiKeyHttpClientInterceptorв заголовке/параметре запроса/куки, объявленных схемой - Схема
basicbasicAuth: учётные данные, оборачиваемые сгенерированнымBasicAuthHttpClientInterceptor
bearer и oauth¶
Для схем bearer и oauth генератор ожидает компонент HttpClientTokenProvider, помеченный сгенерированным классом-маркером ApiSecurity
(например ApiSecurity.BearerAuth). Генератор автоматически оборачивает его в BearerAuthHttpClientInterceptor, поэтому предоставить нужно только провайдер токенов:
@Module
public interface ClientAuthModule {
@Tag(ApiSecurity.BearerAuth.class) //(1)!
default HttpClientTokenProvider bearerTokenProvider() {
return request -> CompletableFuture.completedFuture("my-token"); //(2)!
}
}
- Тег должен совпадать со сгенерированным классом-маркером для схемы
- Реальные реализации обычно здесь получают или обновляют токен
@Module
interface ClientAuthModule {
@Tag(ApiSecurity.BearerAuth::class) //(1)!
fun bearerTokenProvider(): HttpClientTokenProvider {
return HttpClientTokenProvider { CompletableFuture.completedFuture("my-token") } //(2)!
}
}
- Тег должен совпадать со сгенерированным классом-маркером для схемы
- Реальные реализации обычно здесь получают или обновляют токен
Несколько схем¶
Когда операция объявляет несколько схем безопасности, primaryAuth выбирает, какую из них применять; иначе генератор выбирает одну и пишет предупреждение в лог.
Чтобы применить несколько схем к одному запросу, включите authAllowMultiple — генератор построит составной перехватчик, выполняющий каждую схему последовательно.
Чтобы передавать учётные данные явно на каждый вызов вместо перехватчика, включите authAsMethodArgument — значение авторизации станет аргументом метода клиента:
configOptions = [
mode: "java-client",
securityConfigPrefix: "openapiAuth",
primaryAuth: "apiKeyAuth", //(1)!
authAllowMultiple: "false", //(2)!
authAsMethodArgument: "false" //(3)!
]
- Схема, применяемая, когда у операции их перечислено несколько
- Применять каждую объявленную схему через составной перехватчик
- Добавить значение авторизации как аргумент метода вместо перехватчика
configOptions = mapOf(
"mode" to "kotlin-client",
"securityConfigPrefix" to "openapiAuth",
"primaryAuth" to "apiKeyAuth", //(1)!
"authAllowMultiple" to "false", //(2)!
"authAsMethodArgument" to "false" //(3)!
)
- Схема, применяемая, когда у операции их перечислено несколько
- Применять каждую объявленную схему через составной перехватчик
- Добавить значение авторизации как аргумент метода вместо перехватчика
Дополнительные аннотации¶
Параметр additionalContractAnnotations добавляет аннотации над сгенерированными методами клиента или контроллера сервера.
Значение — это объект JSON, где ключ — тег API из контракта или * для всех операций, а значение — массив объектов с полем annotation.
Перехватчики¶
Сгенерированные клиенты, аннотированные @HttpClient, можно также аннотировать перехватчиками.
Значение — это объект JSON, где ключ — тег API из контракта, а значение — массив объектов с полями type и tag.
Оба поля можно указать вместе или указать только одно из них:
type— класс реализации конкретного перехватчикаtag— теги перехватчика: строка или массив строк
Задайте configOptions.interceptors:
Теги¶
Сгенерированным клиентам, аннотированным @HttpClient, можно передать параметры httpClientTag и telemetryTag.
Значение — это объект JSON, где ключ — тег API из контракта, а значение — объект с полями httpClientTag и telemetryTag.
Задайте configOptions.tags:
Неявные заголовки¶
По умолчанию заголовки из операции OpenAPI становятся аргументами сгенерированного метода.
Если некоторые заголовки предоставляются инфраструктурой, а не кодом приложения, их можно сделать неявными.
implicitHeaders = trueделает неявными все заголовки из операцийOpenAPI.implicitHeadersRegexделает неявными только заголовки, имена которых соответствуют регулярному выражению.
Неявный заголовок убирается из сигнатуры метода, но остаётся в аннотациях OpenAPI в сгенерированном коде.
Это сохраняет заголовок в документации контракта, не требуя от кода приложения передавать его вручную.
Модели¶
Генератор создаёт модели запросов и ответов из схем OpenAPI.
Необязательные поля используют @Nullable в Java и nullable-тип T? в Kotlin.
Для схем с наследованием и дискриминатором Java может генерировать sealed interface, а Kotlin — sealed interface / классы в зависимости от схемы.
Необязательные nullable-поля¶
Если поле одновременно имеет nullable: true и отсутствует в списке required, по умолчанию оно генерируется как обычное необязательное поле.
Если нужно различать три состояния — поле отсутствует в JSON, поле присутствует со значением null и поле присутствует со значением — включите enableJsonNullable.
В этом случае поле генерируется как JsonNullable.
forceIncludeOptional и forceIncludeNonRequired управляют сериализацией необязательных полей:
forceIncludeOptionalустанавливает@JsonInclude(Always)для полей сnullable: trueиrequired: falseвместо использованияJsonNullable.forceIncludeNonRequiredустанавливает@JsonInclude(Always)для всех полей сrequired: false.
forceIncludeOptional нельзя включить вместе с enableJsonNullable, поскольку оба режима решают одну и ту же задачу разными способами.
Фильтрация моделей¶
OpenAPI Generator может фильтровать операции через openapiNormalizer.FILTER.
Если дополнительно включён filterWithModels, генератор Kora пытается исключить неиспользуемые модели, оставшиеся после фильтрации операций.
Это полезно для больших контрактов, где приложение генерирует только часть API.
Сервер¶
Минимальная конфигурация плагина для создания обработчиков HTTP-сервера:
Для серверов configOptions.mode поддерживает java-server, java-async-server и java-reactive-server.
Остальные параметры сервера описаны ниже в разделах про валидацию, классы delegate, перехватчики, модели и неявные заголовки.
def openApiGenerateHttpServer = tasks.register("openApiGenerateHttpServer", GenerateTask) {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
def corePackage = "ru.tinkoff.kora.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = [
DISABLE_ALL: "true"
]
configOptions = [
mode: "java-server", //(6)!
]
}
sourceSets.main { java.srcDirs += openApiGenerateHttpServer.get().outputDir } //(7)!
compileJava.dependsOn openApiGenerateHttpServer //(8)!
- Путь к файлу
OpenAPI, по которому создаются классы - Каталог, где создаются сгенерированные файлы
- Пакет для делегатов, контроллеров и мапперов
- Пакет для моделей и DTO
- Вспомогательный пакет генератора
- Режим плагина
- Регистрирует сгенерированные классы как исходный код проекта
- Ставит компиляцию кода в зависимость от генерации классов HTTP-сервера: сначала генерация, затем компиляция
Для серверов configOptions.mode поддерживает kotlin-server и kotlin-suspend-server.
Остальные параметры сервера описаны ниже в разделах про валидацию, классы delegate, перехватчики, модели и неявные заголовки.
val openApiGenerateHttpServer = tasks.register<GenerateTask>("openApiGenerateHttpServer") {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
val corePackage = "ru.tinkoff.kora.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = mapOf(
"DISABLE_ALL" to "true"
)
configOptions = mapOf(
"mode" to "kotlin-server" //(6)!
)
}
kotlin.sourceSets.main { kotlin.srcDir(openApiGenerateHttpServer.get().outputDir) } //(7)!
tasks.withType<KspTask> { dependsOn(openApiGenerateHttpServer) } //(8)!
- Путь к файлу
OpenAPI, по которому создаются классы - Каталог, где создаются сгенерированные файлы
- Пакет для делегатов, контроллеров и мапперов
- Пакет для моделей и DTO
- Вспомогательный пакет генератора
- Режим плагина
- Регистрирует сгенерированные классы как исходный код проекта
- Ставит компиляцию кода в зависимость от генерации классов HTTP-сервера: сначала генерация, затем компиляция
После генерации обработчики регистрируются автоматически.
Валидация¶
Чтобы генерировать модели и контроллеры с аннотациями из модуля валидации, задайте enableServerValidation:
Когда enableServerValidation включён, генератор добавляет аннотации валидации к моделям и параметрам методов сервера,
а также добавляет @Validate к методам контроллера с валидируемыми параметрами.
enableServerValidationInterceptor управляет добавлением ValidationHttpServerInterceptor, который преобразует ошибки валидации в HTTP-ответы.
Если enableServerValidationInterceptor не указан явно, он считается включённым, когда включена валидация сервера.
Если указано enableServerValidationInterceptor = false, аннотации валидации остаются, но стандартный перехватчик ответа не добавляется.
Реализация делегата¶
Генератор сервера создаёт контроллер и контракт delegate, в котором пользователь реализует логику приложения.
По умолчанию delegateMethodBodyMode = none, поэтому методы контракта delegate не получают стандартного тела и должны быть реализованы приложением.
Если задано delegateMethodBodyMode = throwException, методы получают тело, выбрасывающее исключение, и генератор также создаёт модуль
с реализацией контракта delegate по умолчанию. Этот режим полезен, когда приложение нужно собрать до того, как реализованы все операции, или когда собственные реализации подключаются постепенно.
Ответы делегата¶
Каждый сгенерированный метод delegate возвращает запечатанную (sealed) обёртку *ApiResponses, подтипы которой кодируют HTTP-статус, объявленный в контракте.
Для операции getPetById с ответами 200 и 404 генератор создаёт PetApiResponses.GetPetByIdApiResponse с подтипами
GetPetById200ApiResponse (несущим тело через content()) и GetPetById404ApiResponse. Реализация возвращает подтип, соответствующий результату:
Возвращаемый тип зависит от mode: java-server возвращает значение напрямую (показано здесь), java-async-server возвращает CompletionStage<...>, java-reactive-server возвращает Mono<...>:
@Component
public final class PetDelegate implements PetApiDelegate {
private final Map<Long, Pet> petMap = new ConcurrentHashMap<>();
@Override
public PetApiResponses.GetPetByIdApiResponse getPetById(long petId) {
var pet = petMap.get(petId);
if (pet == null) {
return new PetApiResponses.GetPetByIdApiResponse.GetPetById404ApiResponse(); //(1)!
}
return new PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse(pet); //(2)!
}
@Override
public PetApiResponses.AddPetApiResponse addPet(Pet body) {
petMap.put(body.id(), body);
return new PetApiResponses.AddPetApiResponse.AddPet200ApiResponse(body);
}
}
- Подтип статуса
404, без тела - Подтип статуса
200, несущий тело ответа
Возвращаемый тип зависит от mode: kotlin-server возвращает значение напрямую (показано здесь), kotlin-suspend-server использует suspend-метод:
@Component
class PetDelegate : PetApiDelegate {
private val petMap = ConcurrentHashMap<Long, Pet>()
override fun getPetById(petId: Long): PetApiResponses.GetPetByIdApiResponse {
val pet = petMap[petId]
return if (pet == null) {
PetApiResponses.GetPetByIdApiResponse.GetPetById404ApiResponse() //(1)!
} else {
PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse(pet) //(2)!
}
}
override fun addPet(pet: Pet): PetApiResponses.AddPetApiResponse {
petMap[pet.id] = pet
return PetApiResponses.AddPetApiResponse.AddPet200ApiResponse(pet)
}
}
- Подтип статуса
404, без тела - Подтип статуса
200, несущий тело ответа
Исходный запрос¶
По умолчанию метод delegate получает только параметры, объявленные в контракте. Если реализации нужен доступ к исходному запросу
(например, чтобы прочитать инфраструктурный заголовок или удалённый адрес), включите requestInDelegateParams. Тогда генератор добавляет
HttpServerRequest первым параметром каждого метода delegate. Это параметр только для сервера.
- Добавляет
HttpServerRequest _serverRequestпервым аргументом каждого метода делегата
Префикс пути контроллера¶
prefixPath добавляет базовый путь в начало каждого маршрута сгенерированного контроллера HTTP-сервера. Это полезно, когда все операции должны обслуживаться под общим
сегментом (например /api/v1), которого нет в путях OpenAPI.
Перехватчики¶
Сгенерированные контроллеры, аннотированные @HttpController, можно также аннотировать перехватчиками.
Значение — это объект JSON, где ключ — тег API из контракта, а значение — объект с полями type и tag.
Оба поля можно указать вместе или указать только одно из них:
type— класс реализации конкретного перехватчикаtag— теги перехватчика: строка или массив строк
Задайте configOptions.interceptors:
Авторизация¶
Когда контракт OpenAPI описывает securitySchemes, генератор сервера создаёт модуль ApiSecurity с одним классом-маркером на каждую схему:
ApiSecurity.BearerAuth, ApiSecurity.BasicAuth, ApiSecurity.ApiKeyAuth и ApiSecurity.OAuth
(обрабатывающие Basic/ApiKey/Bearer/OAuth).
Для каждой схемы приложение должно предоставить компонент HttpServerPrincipalExtractor, помеченный соответствующим классом-маркером.
Извлекатель получает запрос и разобранное значение учётных данных и возвращает аутентифицированный Principal:
@Module
public interface AuthModule {
@Tag(ApiSecurity.BearerAuth.class)
default HttpServerPrincipalExtractor<Principal> bearerHttpServerPrincipalExtractor() {
return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name"));
}
@Tag(ApiSecurity.BasicAuth.class)
default HttpServerPrincipalExtractor<Principal> basicHttpServerPrincipalExtractor() {
return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name"));
}
@Tag(ApiSecurity.ApiKeyAuth.class)
default HttpServerPrincipalExtractor<Principal> apiKeyHttpServerPrincipalExtractor() {
return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name"));
}
@Tag(ApiSecurity.OAuth.class)
default HttpServerPrincipalExtractor<PrincipalWithScopes> oauthHttpServerPrincipalExtractor() { //(1)!
return (request, value) -> CompletableFuture.completedFuture(new UserPrincipal("name"));
}
}
- Схемы
OAuthобъявляют области доступа (scopes), поэтому извлекатель возвращаетPrincipalWithScopes
@Module
interface AuthModule {
@Tag(ApiSecurity.BearerAuth::class)
fun bearerHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<Principal> {
return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) }
}
@Tag(ApiSecurity.BasicAuth::class)
fun basicHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<Principal> {
return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) }
}
@Tag(ApiSecurity.ApiKeyAuth::class)
fun apiKeyHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<Principal> {
return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) }
}
@Tag(ApiSecurity.OAuth::class)
fun oauthHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<PrincipalWithScopes> { //(1)!
return HttpServerPrincipalExtractor { _, _ -> CompletableFuture.completedFuture(UserPrincipal("name")) }
}
}
- Схемы
OAuthобъявляют области доступа (scopes), поэтому извлекатель возвращаетPrincipalWithScopes
Для OAuth возвращаемый principal должен реализовывать PrincipalWithScopes, чтобы сгенерированный контроллер мог проверять области доступа, объявленные для каждой операции.
Извлекатель нужен только тем схемам, которые контракт действительно использует; класс-маркер существует для каждой объявленной схемы:
Рекомендации¶
Совет
Если что-то не генерируется плагином или поведение отличается от ожидаемого либо от других версий, внимательно проверьте конфигурацию плагина и изучите настройки, поскольку они могут влиять на то, как генерируются классы.
Начиная с версии плагина 7.0.0, правило SIMPLIFY_ONEOF_ANYOF, включённое по умолчанию в openapiNormalizer,
может приводить к некоторым неочевидным результатам генератора.