diff --git a/buildSrc/src/main/java/com/yelp/codegen/gradle/PublishingVersions.kt b/buildSrc/src/main/java/com/yelp/codegen/gradle/PublishingVersions.kt index bc522e20..2c912ffa 100644 --- a/buildSrc/src/main/java/com/yelp/codegen/gradle/PublishingVersions.kt +++ b/buildSrc/src/main/java/com/yelp/codegen/gradle/PublishingVersions.kt @@ -1,7 +1,7 @@ @file:Suppress("Unused") object PublishingVersions { - const val PLUGIN_VERSION = "1.1.1" + const val PLUGIN_VERSION = "1.2.0-SNAPSHOT" const val PLUGIN_GROUP = "com.yelp.codegen" const val PLUGIN_ARTIFACT = "plugin" } diff --git a/plugin/src/main/java/com/yelp/codegen/KotlinGenerator.kt b/plugin/src/main/java/com/yelp/codegen/KotlinGenerator.kt index 241fb04e..e1570c94 100644 --- a/plugin/src/main/java/com/yelp/codegen/KotlinGenerator.kt +++ b/plugin/src/main/java/com/yelp/codegen/KotlinGenerator.kt @@ -28,7 +28,11 @@ class KotlinGenerator : SharedCodegen() { * This number represents the version of the kotlin template * Please note that is independent from the Plugin version */ - @JvmStatic val VERSION = 11 + @JvmStatic val VERSION = 12 + + // Vendor Extension use to generate the list of top level headers + const val HAS_OPERATION_HEADERS = "hasOperationHeaders" + const val OPERATION_HEADERS = "operationHeaders" } private val apiDocPath = "docs/" @@ -404,7 +408,6 @@ class KotlinGenerator : SharedCodegen() { codegenOperation.imports.add("retrofit2.http.Headers") codegenOperation.vendorExtensions[X_OPERATION_ID] = operation?.operationId - getHeadersToIgnore().forEach { headerName -> ignoreHeaderParameter(headerName, codegenOperation) } @@ -413,6 +416,7 @@ class KotlinGenerator : SharedCodegen() { if (!basePath.isNullOrBlank()) { codegenOperation.path = codegenOperation.path.removePrefix("/") } + processTopLevelHeaders(codegenOperation) return codegenOperation } @@ -451,4 +455,30 @@ class KotlinGenerator : SharedCodegen() { override fun removeNonNameElementToCamelCase(name: String?): String { return super.removeNonNameElementToCamelCase(name, "[-_:;#\\[\\]]") } + + /** + * Function to check if there are Headers that should be applied at the Top level on Retrofit + * with the @Headers annotation. This method will populate the `hasOperationHeaders` and `operationHeaders` + * vendor extensions to support the mustache template. + */ + internal fun processTopLevelHeaders(operation: CodegenOperation) { + val topLevelHeaders = mutableListOf>() + + // Send the X-Operation-Id header only if a custom operation ID was set. + val operationId = operation.vendorExtensions[X_OPERATION_ID] as String? + if (!operationId.isNullOrBlank()) { + topLevelHeaders.add(HEADER_X_OPERATION_ID to operationId) + } + + // Send the Content-Type header for the first `consume` mediaType specified. + val firstContentType = operation.consumes?.firstOrNull() + if (operation.formParams.isNullOrEmpty() && firstContentType != null) { + firstContentType["mediaType"]?.let { + topLevelHeaders.add(HEADER_CONTENT_TYPE to it) + } + } + + operation.vendorExtensions[HAS_OPERATION_HEADERS] = topLevelHeaders.isNotEmpty() + operation.vendorExtensions[OPERATION_HEADERS] = topLevelHeaders + } } diff --git a/plugin/src/main/java/com/yelp/codegen/SharedCodegen.kt b/plugin/src/main/java/com/yelp/codegen/SharedCodegen.kt index c7531a49..554c7ab4 100644 --- a/plugin/src/main/java/com/yelp/codegen/SharedCodegen.kt +++ b/plugin/src/main/java/com/yelp/codegen/SharedCodegen.kt @@ -32,11 +32,16 @@ const val ARTIFACT_ID = "artifact_id" const val GROUP_ID = "group_id" const val HEADERS_TO_IGNORE = "headers_to_ignore" +// Vendor Extensions Names internal const val X_NULLABLE = "x-nullable" internal const val X_MODEL = "x-model" internal const val X_OPERATION_ID = "x-operation-id" internal const val X_UNSAFE_OPERATION = "x-unsafe-operation" +// Headers Names +internal const val HEADER_X_OPERATION_ID = "X-Operation-Id" +internal const val HEADER_CONTENT_TYPE = "Content-Type" + abstract class SharedCodegen : DefaultCodegen(), CodegenConfig { // Reference to the Swagger Specs diff --git a/plugin/src/main/resources/kotlin/retrofit2/api.mustache b/plugin/src/main/resources/kotlin/retrofit2/api.mustache index 237123cd..86a48545 100644 --- a/plugin/src/main/resources/kotlin/retrofit2/api.mustache +++ b/plugin/src/main/resources/kotlin/retrofit2/api.mustache @@ -25,19 +25,12 @@ interface {{classname}} { {{#isMultipart}}@retrofit2.http.Multipart{{/isMultipart}}{{^isMultipart}}@retrofit2.http.FormUrlEncoded{{/isMultipart}} {{/-first}} {{/formParams}} - {{^formParams}} - {{#prioritizedContentTypes}} - {{#-first}} - @Headers({ - "Content-Type:{{{mediaType}}}" - }) - {{/-first}} - {{/prioritizedContentTypes}} - {{/formParams}} - @Headers({{#vendorExtensions.x-operation-id}}{{{newline}}} "X-Operation-ID: {{vendorExtensions.x-operation-id}}"{{/vendorExtensions.x-operation-id}}{{^formParams}}{{#prioritizedContentTypes}}{{#-first}}, - "Content-Type:{{{mediaType}}}"{{/-first}}{{/prioritizedContentTypes}}{{/formParams}} - ) - + {{#vendorExtensions.hasOperationHeaders}} + @Headers( + {{#vendorExtensions.operationHeaders}}"{{first}}: {{second}}"{{^-last}}, + {{/-last}}{{#-last}} + ){{/-last}}{{/vendorExtensions.operationHeaders}} + {{/vendorExtensions.hasOperationHeaders}} @{{httpMethod}}("{{{path}}}"){{#vendorExtensions.x-unsafe-operation}}{{#isDeprecated}} @Deprecated(message = "Deprecated and unsafe to use"){{/isDeprecated}}{{^isDeprecated}} @Deprecated(message = "Unsafe to use"){{/isDeprecated}} diff --git a/plugin/src/test/java/com/yelp/codegen/KotlinGeneratorTest.kt b/plugin/src/test/java/com/yelp/codegen/KotlinGeneratorTest.kt index 3cca8fd4..b45cf25f 100644 --- a/plugin/src/test/java/com/yelp/codegen/KotlinGeneratorTest.kt +++ b/plugin/src/test/java/com/yelp/codegen/KotlinGeneratorTest.kt @@ -1,6 +1,8 @@ package com.yelp.codegen import io.swagger.codegen.CodegenModel +import io.swagger.codegen.CodegenOperation +import io.swagger.codegen.CodegenParameter import io.swagger.codegen.CodegenProperty import io.swagger.models.Info import io.swagger.models.Operation @@ -353,4 +355,89 @@ class KotlinGeneratorTest { assertEquals("42.0.0", swagger.info.version) assertEquals("/v2", generator.basePath) } + + @Test + fun processTopLevelHeaders_withNoHeaders_hasOperationHeadersIsFalse() { + val generator = KotlinGenerator() + val operation = CodegenOperation() + operation.vendorExtensions = mutableMapOf() + + generator.processTopLevelHeaders(operation) + + assertEquals(false, operation.vendorExtensions["hasOperationHeaders"]) + } + + @Test + fun processTopLevelHeaders_withOperationId_hasXOperationIdHeader() { + val testOperationId = "aTestOperationId" + val generator = KotlinGenerator() + val operation = CodegenOperation() + operation.vendorExtensions = mutableMapOf(X_OPERATION_ID to (testOperationId as Any)) + + generator.processTopLevelHeaders(operation) + + assertEquals(true, operation.vendorExtensions["hasOperationHeaders"]) + val headerMap = operation.vendorExtensions["operationHeaders"] as List<*> + assertEquals(1, headerMap.size) + val firstPair = headerMap[0] as Pair<*, *> + assertEquals(HEADER_X_OPERATION_ID, firstPair.first as String) + assertEquals(testOperationId, firstPair.second as String) + } + + @Test + fun processTopLevelHeaders_withConsumes_hasContentTypeHeader() { + val generator = KotlinGenerator() + val operation = CodegenOperation() + operation.vendorExtensions = mutableMapOf() + operation.consumes = listOf( + mapOf("mediaType" to "application/json") + ) + + generator.processTopLevelHeaders(operation) + + assertEquals(true, operation.vendorExtensions["hasOperationHeaders"]) + val headerMap = operation.vendorExtensions["operationHeaders"] as List<*> + assertEquals(1, headerMap.size) + val firstPair = headerMap[0] as Pair<*, *> + assertEquals(HEADER_CONTENT_TYPE, firstPair.first as String) + assertEquals("application/json", firstPair.second as String) + } + + @Test + fun processTopLevelHeaders_withFormParams_hasNoContentTypeHeader() { + val generator = KotlinGenerator() + val operation = CodegenOperation() + operation.vendorExtensions = mutableMapOf() + operation.formParams = listOf(CodegenParameter()) + operation.consumes = listOf( + mapOf("mediaType" to "application/json") + ) + + generator.processTopLevelHeaders(operation) + + assertEquals(false, operation.vendorExtensions["hasOperationHeaders"]) + } + + @Test + fun processTopLevelHeaders_withConsumesAndOperationId_hasTwoHeaders() { + val testOperationId = "aTestOperationId" + val generator = KotlinGenerator() + val operation = CodegenOperation() + operation.vendorExtensions = mutableMapOf(X_OPERATION_ID to (testOperationId as Any)) + operation.consumes = listOf( + mapOf("mediaType" to "application/json") + ) + + generator.processTopLevelHeaders(operation) + + assertEquals(true, operation.vendorExtensions["hasOperationHeaders"]) + val headerMap = operation.vendorExtensions["operationHeaders"] as List<*> + assertEquals(2, headerMap.size) + val firstPair = headerMap[0] as Pair<*, *> + assertEquals(HEADER_X_OPERATION_ID, firstPair.first as String) + assertEquals(testOperationId, firstPair.second as String) + val secondPair = headerMap[1] as Pair<*, *> + assertEquals(HEADER_CONTENT_TYPE, secondPair.first as String) + assertEquals("application/json", secondPair.second as String) + } } diff --git a/samples/generated-code/build.gradle b/samples/generated-code/build.gradle index df66f5b5..8b7c4236 100644 --- a/samples/generated-code/build.gradle +++ b/samples/generated-code/build.gradle @@ -10,7 +10,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:3.4.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.41" - classpath "com.yelp.codegen:plugin:1.1.1" + classpath "com.yelp.codegen:plugin:1.2.0-SNAPSHOT" } } diff --git a/samples/groovy-android/build.gradle b/samples/groovy-android/build.gradle index 8bd3b9a0..2646c557 100644 --- a/samples/groovy-android/build.gradle +++ b/samples/groovy-android/build.gradle @@ -10,7 +10,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:3.4.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.41" - classpath "com.yelp.codegen:plugin:1.1.1" + classpath "com.yelp.codegen:plugin:1.2.0-SNAPSHOT" } } diff --git a/samples/junit-tests/build.gradle b/samples/junit-tests/build.gradle index c1b8f199..b57c8957 100644 --- a/samples/junit-tests/build.gradle +++ b/samples/junit-tests/build.gradle @@ -10,7 +10,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:3.4.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.41" - classpath "com.yelp.codegen:plugin:1.1.1" + classpath "com.yelp.codegen:plugin:1.2.0-SNAPSHOT" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.0.0-RC16" } } diff --git a/samples/kotlin-android/build.gradle.kts b/samples/kotlin-android/build.gradle.kts index 4d44bc7a..0d01f594 100644 --- a/samples/kotlin-android/build.gradle.kts +++ b/samples/kotlin-android/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("com.android.library") version "3.4.2" kotlin("android") version "1.3.41" - id("com.yelp.codegen.plugin") version "1.1.1" + id("com.yelp.codegen.plugin") version "1.2.0-SNAPSHOT" } android {