Repository: cjbooms/fabrikt
Pull request #82 (2021-10-08 13:27:39)

ISSUE-81: Accept header should be picked from the content response
ISSUE-81: Accept header should be picked from the content response


Patch:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt
index 42d5b77..b69848c 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt
@@ -15,6 +15,7 @@ import com.squareup.kotlinpoet.ParameterSpec
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.asTypeName
+import java.util.function.Predicate
 
 object GeneratorUtils {
     /**
@@ -85,6 +86,15 @@ object GeneratorUtils {
         return this.primaryConstructor(constructor).addProperties(propertySpecs)
     }
 
+    fun <T> FunSpec.Builder.addOptionalParameter(
+        parameterSpec: ParameterSpec,
+        input: T,
+        predicate: Predicate<T>
+    ) = apply {
+        if (predicate.test(input))
+            this.addParameter(parameterSpec)
+    }
+
     fun functionName(resource: String, verb: String) = "$verb $resource".toKCodeName()
 
     fun Schema.toVarName() = this.name?.toKCodeName() ?: this.toClassName().simpleName.toKCodeName()
@@ -96,14 +106,19 @@ object GeneratorUtils {
     fun RequestBody.getPrimaryContentMediaType(): Map.Entry<String, MediaType>? =
         this.contentMediaTypes.entries.firstOrNull()
 
-    fun Response.getPrimaryAcceptMediaType(): Map.Entry<String, MediaType>? =
+    fun Response.getPrimaryContentMediaType(): Map.Entry<String, MediaType>? =
         this.contentMediaTypes.entries.firstOrNull()
 
-    fun Operation.getBodyResponses(): List<Response> =
-        this.responses.filter { it.key != "default" }.values.filter(Response::hasContentMediaTypes)
+    fun Response.hasMultipleContentMediaTypes(): Boolean = this.contentMediaTypes.entries.size > 1
+
+    fun Operation.firstResponse(): Response? = this.getBodyResponses().firstOrNull()
 
-    fun Operation.getPrimaryAcceptMediaType(): Map.Entry<String, MediaType>? =
-        this.getBodyResponses().map { response -> response.getPrimaryAcceptMediaType() }.firstOrNull()
+    fun Operation.getPrimaryContentMediaType(): Map.Entry<String, MediaType>? =
+        this.getBodyResponses().map { response -> response.getPrimaryContentMediaType() }.firstOrNull()
+
+    fun Operation.getPrimaryContentMediaTypeKey(): String? = this.firstResponse()?.getPrimaryContentMediaType()?.key
+
+    fun Operation.hasMultipleContentMediaTypes(): Boolean? = this.firstResponse()?.hasMultipleContentMediaTypes()
 
     fun Operation.getPathParams(): List<Parameter> = this.filterParams("path")
 
@@ -111,5 +126,8 @@ object GeneratorUtils {
 
     fun Operation.getHeaderParams(): List<Parameter> = this.filterParams("header")
 
+    private fun Operation.getBodyResponses(): List<Response> =
+        this.responses.filter { it.key != "default" }.values.filter(Response::hasContentMediaTypes)
+
     private fun Operation.filterParams(paramType: String): List<Parameter> = this.parameters.filter { it.`in` == paramType }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGeneratorUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGeneratorUtils.kt
index ee4bb97..cabb594 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGeneratorUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGeneratorUtils.kt
@@ -1,7 +1,7 @@
 package com.cjbooms.fabrikt.generators.client
 
 import com.cjbooms.fabrikt.configurations.Packages
-import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryAcceptMediaType
+import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaType
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator.Companion.toModelType
 import com.cjbooms.fabrikt.model.ClientType
@@ -12,6 +12,8 @@ import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.asTypeName
 
 object ClientGeneratorUtils {
+    const val ACCEPT_HEADER_VARIABLE_NAME = "acceptHeader"
+
     /**
      * It resolves the API operation response to its body type. It iterates over all non-default responses
      * by filtering those ones with content media. If multiple medias are found, then it will resolve to the schema
@@ -19,7 +21,7 @@ object ClientGeneratorUtils {
      * for the given operation.
      */
     fun Operation.toClientReturnType(packages: Packages): TypeName {
-        val returnType = this.getPrimaryAcceptMediaType()?.let {
+        val returnType = this.getPrimaryContentMediaType()?.let {
             toModelType(
                 packages.base,
                 KotlinTypeInfo.from(it.value.schema)
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpEnhancedClientGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpEnhancedClientGenerator.kt
index 8fcf0db..7f8d108 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpEnhancedClientGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpEnhancedClientGenerator.kt
@@ -2,11 +2,16 @@ package com.cjbooms.fabrikt.generators.client
 
 import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
 import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.GeneratorUtils.addOptionalParameter
+import com.cjbooms.fabrikt.generators.GeneratorUtils.firstResponse
 import com.cjbooms.fabrikt.generators.GeneratorUtils.functionName
+import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaTypeKey
+import com.cjbooms.fabrikt.generators.GeneratorUtils.hasMultipleContentMediaTypes
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toBodyParameterSpec
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toKCodeName
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toParameterSpec
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ACCEPT_HEADER_VARIABLE_NAME
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.enhancedClientName
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.simpleClientName
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.toClientReturnType
@@ -52,6 +57,12 @@ class OkHttpEnhancedClientGenerator(
                         )
                         .addParameters(operation.requestBody.toBodyParameterSpec(packages.base))
                         .addParameters(operation.parameters.map { it.toParameterSpec(packages.base) })
+                        .addOptionalParameter(
+                            ParameterSpec.builder(ACCEPT_HEADER_VARIABLE_NAME, String::class)
+                                .defaultValue("%S", operation.getPrimaryContentMediaTypeKey())
+                                .build(),
+                            operation,
+                        ) { it.hasMultipleContentMediaTypes() == true }
                         .addCode(
                             Resilience4jClientOperationStatement(
                                 packages,
@@ -168,10 +179,17 @@ class Resilience4jClientOperationStatement(
     }
 
     private fun CodeBlock.Builder.addClientCallStatement(): CodeBlock.Builder {
-        val params = operation.requestBody.toBodyParameterSpec(packages.base).plus(
+        val params = mutableListOf(
+            operation.requestBody.toBodyParameterSpec(packages.base),
             operation.parameters.map { it.toParameterSpec(packages.base) }
         )
-        this.add("apiClient.%N(%L)", functionName(resource, verb), params.joinToString(",") { it.name })
+        operation.firstResponse()?.let {
+            if (it.hasMultipleContentMediaTypes()) {
+                params.add(listOf(ParameterSpec.builder(ACCEPT_HEADER_VARIABLE_NAME, String::class).build()))
+            }
+        }
+
+        this.add("apiClient.%N(%L)", functionName(resource, verb), params.flatten().joinToString(",") { it.name })
         return this
     }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt
index eb962fa..d34bba1 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt
@@ -1,12 +1,15 @@
 package com.cjbooms.fabrikt.generators.client
 
 import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.GeneratorUtils.addOptionalParameter
+import com.cjbooms.fabrikt.generators.GeneratorUtils.firstResponse
 import com.cjbooms.fabrikt.generators.GeneratorUtils.functionName
-import com.cjbooms.fabrikt.generators.GeneratorUtils.getBodyResponses
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getHeaderParams
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getPathParams
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaType
+import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaTypeKey
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getQueryParams
+import com.cjbooms.fabrikt.generators.GeneratorUtils.hasMultipleContentMediaTypes
 import com.cjbooms.fabrikt.generators.GeneratorUtils.primaryPropertiesConstructor
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toBodyParameterSpec
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toBodyRequestSchema
@@ -15,6 +18,7 @@ import com.cjbooms.fabrikt.generators.GeneratorUtils.toKCodeName
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toParameterSpec
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toVarName
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ACCEPT_HEADER_VARIABLE_NAME
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.simpleClientName
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.toClientReturnType
 import com.cjbooms.fabrikt.model.ClientType
@@ -31,6 +35,7 @@ import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterSpec
 import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.asTypeName
@@ -39,7 +44,6 @@ class OkHttpSimpleClientGenerator(
     private val packages: Packages,
     private val api: SourceApi
 ) {
-
     fun generateDynamicClientCode(): Collection<ClientType> {
         return api.openApi3.routeToPaths().map { (resourceName, paths) ->
             val funcSpecs: List<FunSpec> = paths.flatMap { (resource, path) ->
@@ -54,6 +58,12 @@ class OkHttpSimpleClientGenerator(
                         )
                         .addParameters(operation.requestBody.toBodyParameterSpec(packages.base))
                         .addParameters(operation.parameters.map { it.toParameterSpec(packages.base) })
+                        .addOptionalParameter(
+                            ParameterSpec.builder(ACCEPT_HEADER_VARIABLE_NAME, String::class)
+                                .defaultValue("%S", operation.getPrimaryContentMediaTypeKey())
+                                .build(),
+                            operation,
+                        ) { it.hasMultipleContentMediaTypes() == true }
                         .addCode(
                             SimpleClientOperationStatement(
                                 packages,
@@ -170,9 +180,15 @@ data class SimpleClientOperationStatement(
         operation.getHeaderParams().map {
             this.add("\n.%T(%S, %N)", "header".toClassName(packages.client), it.name, it.name.toKCodeName())
         }
-        operation.getBodyResponses().firstOrNull()?.let {
-            this.add("\n.%T(%S, %S)", "header".toClassName(packages.client), "Accept", "application/json")
+
+        operation.firstResponse()?.let {
+            if (it.hasMultipleContentMediaTypes()) {
+                this.add("\n.%T(%S, %L)", "header".toClassName(packages.client), "Accept", ACCEPT_HEADER_VARIABLE_NAME)
+            } else {
+                this.add("\n.%T(%S, %S)", "header".toClassName(packages.client), "Accept", operation.getPrimaryContentMediaTypeKey())
+            }
         }
+
         return this.add("\n.build()\n")
     }
 


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
index 0a58195..a7bc03e 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
@@ -26,7 +26,8 @@ class OkHttpClientGeneratorTest {
 
     @Suppress("unused")
     private fun fullApiTestCases(): Stream<String> = Stream.of(
-        "okHttpClient"
+        "okHttpClient",
+        "okHttpClientMultiMediaType"
     )
 
     @BeforeEach
diff --git a/src/test/resources/examples/okHttpClient/api.yaml b/src/test/resources/examples/okHttpClient/api.yaml
index 251864e..34c712e 100644
--- a/src/test/resources/examples/okHttpClient/api.yaml
+++ b/src/test/resources/examples/okHttpClient/api.yaml
@@ -16,7 +16,7 @@ paths:
             Cache-Control:
               $ref: "#/components/headers/CacheControl"
           content:
-            application/json:
+            application/vnd.custom.media+json:
               schema:
                 $ref: "#/components/schemas/QueryResult"
 
diff --git a/src/test/resources/examples/okHttpClient/client/ApiClient.kt b/src/test/resources/examples/okHttpClient/client/ApiClient.kt
index 2426e41..047ccf9 100644
--- a/src/test/resources/examples/okHttpClient/client/ApiClient.kt
+++ b/src/test/resources/examples/okHttpClient/client/ApiClient.kt
@@ -43,7 +43,7 @@ class ExamplePathClient(
             .build()
 
         val httpHeaders: Headers = Headers.Builder()
-            .header("Accept", "application/json")
+            .header("Accept", "application/vnd.custom.media+json")
             .build()
 
         val request: Request = Request.Builder()
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/api.yaml b/src/test/resources/examples/okHttpClientMultiMediaType/api.yaml
new file mode 100644
index 0000000..1dd5aca
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/api.yaml
@@ -0,0 +1,157 @@
+openapi: "3.0.0"
+info:
+  title: "API Example"
+  version: "1.0"
+paths:
+  /example-path-1:
+    get:
+      summary: "GET example path 1"
+      parameters:
+        - $ref: "#/components/parameters/ListQueryParamExploded"
+        - $ref: "#/components/parameters/QueryParam2"
+      responses:
+        200:
+          description: "successful operation"
+          headers:
+            Cache-Control:
+              $ref: "#/components/headers/CacheControl"
+          content:
+            application/vnd.custom.media+xml:
+              schema:
+                $ref: "#/components/schemas/QueryResult"
+            application/vnd.custom.media+json:
+              schema:
+                $ref: "#/components/schemas/QueryResult"
+
+components:
+  parameters:
+    ListQueryParamExploded:
+      name: "explode_list_query_param"
+      in: "query"
+      schema:
+        type: "array"
+        items:
+          type: "string"
+      required: false
+      allowEmptyValue: false
+
+    QueryParam2:
+      name: "query_param2"
+      in: "query"
+      schema:
+        type: integer
+      required: false
+
+  headers:
+    CacheControl:
+      description: "The RFC7234 Cache Control header"
+      schema:
+        type: "string"
+      example: "must-revalidate, max-age=5"
+
+  schemas:
+    QueryResult:
+      type: "object"
+      required:
+        - "items"
+      properties:
+        items:
+          type: "array"
+          minItems: 0
+          items:
+            oneOf:
+              - $ref: "#/components/schemas/FirstModel"
+              - $ref: "#/components/schemas/SecondModel"
+              - $ref: "#/components/schemas/ThirdModel"
+    Content:
+      type: "object"
+      required:
+        - "id"
+        - "attr_1"
+        - "attr_2"
+        - "attr_3"
+        - "etag"
+        - "model_type"
+      properties:
+        id:
+          description: "The unique resource id"
+          type: "string"
+          readOnly: true
+        first_attr:
+          description: "The attribute 1"
+          type: "string"
+          format: "date-time"
+          example: "2016-01-27T10:52:46.406Z"
+          readOnly: true
+        second_attr:
+          description: "The attribute 2"
+          type: "string"
+          readOnly: true
+        third_attr:
+          type: "string"
+          enum:
+            - "enum_type_1"
+            - "enum_type_2"
+          description: "Enum types for attribute 3"
+          example: "enum_type_2"
+        etag:
+          type: "string"
+          description: "Etag value to be used in conjunction with If-Match headers for optimistic locking purposes"
+          readOnly: true
+        model_type:
+          type: "string"
+          description: "The model discrimination type"
+          enum:
+            - "first_model"
+            - "second_model"
+            - "third_model"
+          example: "third_model"
+      discriminator:
+        propertyName: "model_type"
+        mapping:
+          first_model: "#/components/schemas/FirstModel"
+          second_model: "#/components/schemas/SecondModel"
+          third_model: "#/components/schemas/ThirdModel"
+
+    FirstModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 1"
+              type: array
+              items:
+                type: "string"
+                minItems: 1
+                maxItems: 10
+              readOnly: true
+    SecondModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 2"
+              type: "string"
+              readOnly: true
+            extra_second_attr:
+              description: "The attribute 2 for model 2"
+              type: boolean
+              readOnly: true
+
+    ThirdModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 3"
+              type: "string"
+              format: "date-time"
+              example: "2016-01-27T10:52:46.406Z"
+              readOnly: true
+            extra_second_attr:
+              description: "The attribute 2 for model 3"
+              type: integer
+              readOnly: true
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
new file mode 100644
index 0000000..007c8e1
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
@@ -0,0 +1,54 @@
+package examples.okHttpClientMultiMediaType.client
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
+import examples.okHttpClientMultiMediaType.models.QueryResult
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.jvm.Throws
+
+@Suppress("unused")
+class ExamplePathClient(
+    private val objectMapper: ObjectMapper,
+    private val baseUrl: String,
+    private val client: OkHttpClient
+) {
+    /**
+     * GET example path 1
+     *
+     * @param explodeListQueryParam
+     * @param queryParam2
+     */
+    @Throws(ApiException::class)
+    fun getExamplePath1(
+        explodeListQueryParam: List<String>?,
+        queryParam2: Int?,
+        acceptHeader: String = "application/vnd.custom.media+xml"
+    ): ApiResponse<QueryResult?> {
+        val httpUrl: HttpUrl = "$baseUrl/example-path-1"
+            .toHttpUrl()
+            .newBuilder()
+            .queryParam("explode_list_query_param", explodeListQueryParam, true)
+            .queryParam("query_param2", queryParam2)
+            .build()
+
+        val httpHeaders: Headers = Headers.Builder()
+            .header("Accept", acceptHeader)
+            .build()
+
+        val request: Request = Request.Builder()
+            .url(httpUrl)
+            .headers(httpHeaders)
+            .get()
+            .build()
+
+        return request.execute(client, objectMapper, jacksonTypeRef())
+    }
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiModels.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiModels.kt
new file mode 100644
index 0000000..3358c7d
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiModels.kt
@@ -0,0 +1,26 @@
+package examples.okHttpClientMultiMediaType.client
+
+import okhttp3.Headers
+
+/**
+ * API 2xx success response returned by API call.
+ *
+ * @param <T> The type of data that is deserialized from response body
+ */
+data class ApiResponse<T>(val statusCode: Int, val headers: Headers, val data: T? = null)
+
+/**
+ * API non-2xx failure responses returned by API call.
+ */
+open class ApiException(override val message: String) : Exception(message)
+
+/**
+ * API 4xx failure responses returned by API call.
+ */
+data class ApiClientException(val statusCode: Int, val headers: Headers, override val message: String) : ApiException(message)
+
+/**
+ * API 5xx failure responses returned by API call.
+ */
+data class ApiServerException(val statusCode: Int, val headers: Headers, override val message: String) : ApiException(message)
+
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
new file mode 100644
index 0000000..85f00ab
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
@@ -0,0 +1,41 @@
+package examples.okHttpClientMultiMediaType.client
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import examples.okHttpClientMultiMediaType.models.QueryResult
+import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
+import okhttp3.OkHttpClient
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.jvm.Throws
+
+/**
+ * The circuit breaker registry should have the proper configuration to correctly action on circuit
+ * breaker transitions based on the client exceptions [ApiClientException], [ApiServerException] and
+ * [IOException].
+ *
+ * @see ApiClientException
+ * @see ApiServerException
+ */
+@Suppress("unused")
+class ExamplePathService(
+    private val circuitBreakerRegistry: CircuitBreakerRegistry,
+    objectMapper: ObjectMapper,
+    baseUrl: String,
+    client: OkHttpClient
+) {
+    var circuitBreakerName: String = "examplePathClient"
+
+    private val apiClient: ExamplePathClient = ExamplePathClient(objectMapper, baseUrl, client)
+
+    @Throws(ApiException::class)
+    fun getExamplePath1(
+        explodeListQueryParam: List<String>?,
+        queryParam2: Int?,
+        acceptHeader: String = "application/vnd.custom.media+xml"
+    ): ApiResponse<QueryResult?> =
+        withCircuitBreaker(circuitBreakerRegistry, circuitBreakerName) {
+            apiClient.getExamplePath1(explodeListQueryParam, queryParam2, acceptHeader)
+        }
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpResilience4jUtil.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpResilience4jUtil.kt
new file mode 100644
index 0000000..aac6956
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpResilience4jUtil.kt
@@ -0,0 +1,13 @@
+package examples.okHttpClientMultiMediaType.client
+
+import io.github.resilience4j.circuitbreaker.CircuitBreaker
+import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
+
+fun <T> withCircuitBreaker(
+    circuitBreakerRegistry: CircuitBreakerRegistry,
+    apiClientName: String,
+    apiCall: () -> ApiResponse<T>
+): ApiResponse<T> {
+    val circuitBreaker = circuitBreakerRegistry.circuitBreaker(apiClientName)
+    return CircuitBreaker.decorateSupplier(circuitBreaker, apiCall).get()
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpUtil.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpUtil.kt
new file mode 100644
index 0000000..fdff2fc
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpUtil.kt
@@ -0,0 +1,65 @@
+package examples.okHttpClientMultiMediaType.client
+
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.ObjectMapper
+import okhttp3.FormBody
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.ResponseBody
+
+@Suppress("unused")
+fun <T : Any> HttpUrl.Builder.queryParam(key: String, value: T?): HttpUrl.Builder = this.apply {
+    if (value != null) this.addQueryParameter(key, value.toString())
+}
+
+@Suppress("unused")
+fun <T : Any> FormBody.Builder.formParam(key: String, value: T?): FormBody.Builder = this.apply {
+    if (value != null) this.add(key, value.toString())
+}
+
+@Suppress("unused")
+fun HttpUrl.Builder.queryParam(key: String, values: List<String>?, explode: Boolean = true) = this.apply {
+    if (values != null) {
+        if (explode) values.forEach { addQueryParameter(key, it) }
+        else addQueryParameter(key, values.joinToString(","))
+    }
+}
+
+@Suppress("unused")
+fun Headers.Builder.header(key: String, value: String?): Headers.Builder = this.apply {
+    if (value != null) this.add(key, value)
+}
+
+@Throws(ApiException::class)
+fun <T> Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, typeRef: TypeReference<T>): ApiResponse<T?> =
+    client.newCall(this).execute().use { response ->
+        when {
+            response.isSuccessful ->
+                ApiResponse(response.code, response.headers, response.body?.deserialize(objectMapper, typeRef))
+            response.isBadRequest() ->
+                throw ApiClientException(response.code, response.headers, response.errorMessage())
+            response.isServerError() ->
+                throw ApiServerException(response.code, response.headers, response.errorMessage())
+            else -> throw ApiException("[${response.code}]: ${response.errorMessage()}")
+        }
+    }
+
+@Suppress("unused")
+fun String.pathParam(vararg params: Pair<String, Any>): String = params.asSequence()
+    .joinToString { param ->
+        this.replace(param.first, param.second.toString())
+    }
+
+fun <T> ResponseBody.deserialize(objectMapper: ObjectMapper, typeRef: TypeReference<T>): T? =
+    this.string().isNotBlankOrNull()?.let { objectMapper.readValue(it, typeRef) }
+
+fun String?.isNotBlankOrNull() = if (this.isNullOrBlank()) null else this
+
+private fun Response.errorMessage(): String = this.body?.string() ?: this.message
+
+private fun Response.isBadRequest(): Boolean = this.code in 400..499
+
+private fun Response.isServerError(): Boolean = this.code in 500..599
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/LoggingInterceptor.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/LoggingInterceptor.kt
new file mode 100644
index 0000000..40273e9
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/LoggingInterceptor.kt
@@ -0,0 +1,25 @@
+package examples.okHttpClientMultiMediaType.client
+
+import okhttp3.Interceptor
+import okhttp3.Response
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+class LoggingInterceptor : Interceptor {
+
+    private val logger: Logger = LoggerFactory.getLogger("GeneratedClient")
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val request = chain.request()
+
+        val t1 = System.nanoTime()
+        logger.info("Client Request: $request")
+
+        val response = chain.proceed(request)
+
+        val t2 = System.nanoTime()
+        logger.info("Client Response after ${(t2 - t1) / 100_000}ms: $response")
+
+        return response
+    }
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/OAuth.kt b/src/test/resources/examples/okHttpClientMultiMediaType/client/OAuth.kt
new file mode 100644
index 0000000..687fda4
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/client/OAuth.kt
@@ -0,0 +1,22 @@
+package examples.okHttpClientMultiMediaType.client
+
+import okhttp3.Authenticator
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+
+class OAuth2(val accessToken: () -> String) : Authenticator, Interceptor {
+
+    override fun authenticate(route: Route?, response: Response): Request =
+        response.request.newBuilder()
+            .header("Authorization", "Bearer ${accessToken().trim()}")
+            .build()
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val request = chain.request().newBuilder()
+            .header("Authorization", "Bearer ${accessToken().trim()}")
+            .build()
+        return chain.proceed(request)
+    }
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/models/Models.kt b/src/test/resources/examples/okHttpClientMultiMediaType/models/Models.kt
new file mode 100644
index 0000000..be2cfc2
--- /dev/null
+++ b/src/test/resources/examples/okHttpClientMultiMediaType/models/Models.kt
@@ -0,0 +1,153 @@
+package examples.okHttpClientMultiMediaType.models
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonValue
+import java.time.OffsetDateTime
+import javax.validation.Valid
+import javax.validation.constraints.NotNull
+import javax.validation.constraints.Size
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.collections.List
+
+data class QueryResult(
+    @param:JsonProperty("items")
+    @get:JsonProperty("items")
+    @get:NotNull
+    @get:Size(min = 0)
+    @get:Valid
+    val items: List<Content>
+)
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "model_type",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(
+        value = FirstModel::class,
+        name =
+        "first_model"
+    ),
+    JsonSubTypes.Type(
+        value = SecondModel::class,
+        name =
+        "second_model"
+    ),
+    JsonSubTypes.Type(value = ThirdModel::class, name = "third_model")
+)
+sealed class Content(
+    open val id: String? = null,
+    open val firstAttr: OffsetDateTime? = null,
+    open val secondAttr: String? = null,
+    open val thirdAttr: ContentThirdAttr? = null,
+    open val etag: String? = null
+) {
+    abstract val modelType: ContentModelType
+}
+
+enum class ContentThirdAttr(
+    @JsonValue
+    val value: String
+) {
+    ENUM_TYPE_1("enum_type_1"),
+
+    ENUM_TYPE_2("enum_type_2");
+}
+
+enum class ContentModelType(
+    @JsonValue
+    val value: String
+) {
+    FIRST_MODEL("first_model"),
+
+    SECOND_MODEL("second_model"),
+
+    THIRD_MODEL("third_model");
+}
+
+data class FirstModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ContentThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: List<String>? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag) {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ContentModelType = ContentModelType.FIRST_MODEL
+}
+
+data class SecondModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ContentThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: String? = null,
+    @param:JsonProperty("extra_second_attr")
+    @get:JsonProperty("extra_second_attr")
+    val extraSecondAttr: Boolean? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag) {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ContentModelType = ContentModelType.SECOND_MODEL
+}
+
+data class ThirdModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ContentThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("extra_second_attr")
+    @get:JsonProperty("extra_second_attr")
+    val extraSecondAttr: Int? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag) {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ContentModelType = ContentModelType.THIRD_MODEL
+}