Repository: cjbooms/fabrikt
Pull request #222 (2023-07-10 08:15:50)

Feature/client open feign
Hi there,

We really like FabriKt ❤️!  But we were missing our preferred client, OpenFeign. 
This PR therefore introduces the support OpenFeign as a client. To be more specific, this PR enables to generate interfaces for OpenFeign targets like:
```
@Suppress("unused")
interface ExamplePath1Client {
    /**
     * GET example path 1
     *
     * @param explodeListQueryParam
     * @param queryParam2
     */
    @RequestLine("GET /example-path-1?explode_list_query_param={explodeListQueryParam}&query_param2={queryParam2}")
    @Headers("Accept: application/vnd.custom.media+json")
    fun getExamplePath1(
        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
        @Param("queryParam2") queryParam2: Int? = null,
        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
    ): QueryResult
}
```

It would then be up to the consumer of this library to build the OpenFeign client and set this interface as a target.  

To determine which client needs to be generated, I've followed the option structure as with controllers:
<img width="1142" alt="image" src="https://github.com/cjbooms/fabrikt/assets/1792525/87bf5e7a-1b02-469d-bd0c-89a97b1c28d3">

Since the generated interface does not include the client itself, I didn't add the support yet for `RESILIENCE4J ` with OpenFeign. I did add a new option for OpenFeign to have the interface methods generated with suspend modifier so that the CoroutineFeign could be used ([reference](https://github.com/OpenFeign/feign/tree/master/example-github-with-coroutine)).

This PR also addresses an issue that we found. If you had a different reponse content types for success and error repsonses, Fabrikt would set the return type of the client method to JsonNode. I understand that this necessary when you have multiple reponse content type if the status is in the successful range. However, when the response is not in the successful range, both clients will throw an error (i.e., `ApiException` and `FeignException`). I've therefore adjusted it to only set the response type to `JsonNode` if multiple content types are found for successful responses (see commit https://github.com/cjbooms/fabrikt/pull/222/commits/4b65287466b6c2ce0449a80e34aac0287effc4d3).
 I've also added a test case for this `differentErrorResponse`.


cjbooms: This is a really high quality contribution. Kudos to all involved. 💯 
dpnolte: Thanks @cjbooms!! It is also a joy to with such nicely structured library 🚀 . I also think that using KotlinPoet is really smart to reduce the risk of having invalid Kotlin code generated.

I've updated the PR as per your comments.
cjbooms: 👍 
cjbooms: LGTM - I can cut a release today and let you play around with it locally for a bit. If there are any issues we can cut new hot-fix releases until you are happy

Patch:
diff --git a/README.md b/README.md
index 887decf..ab983c3 100644
--- a/README.md
+++ b/README.md
@@ -193,7 +193,12 @@ This section documents the available CLI parameters for controlling what gets ge
 | * `--base-package`           | The base package which all code will be generated under. |
 |   `--http-client-opts`       | Select the options for the http client code that you want to be generated. |
 |                              | CHOOSE ANY OF: |
-|                              |   `RESILIENCE4J` - Generates a fault tolerance service for the client using the following library "io.github.resilience4j:resilience4j-all:+" |
+|                              |   `RESILIENCE4J` - Generates a fault tolerance service for the client using the following library "io.github.resilience4j:resilience4j-all:+" (only for OkHttp clients) |
+|                              |   `SUSPEND_MODIFIER` - This option adds the suspend modifier to the generated client functions (only for OpenFeign clients) |
+|   `--http-client-target`     | Optionally select the target client that you want to be generated. Defaults to OK_HTTP |
+|                              | CHOOSE ONE OF: |
+|                              |   `OK_HTTP` - Generate OkHttp client. |
+|                              |   `OPEN_FEIGN` - Generate OpenFeign client. |
 |   `--http-controller-opts`   | Select the options for the controllers that you want to be generated. |
 |                              | CHOOSE ANY OF: |
 |                              |   `SUSPEND_MODIFIER` - This option adds the suspend modifier to the generated controller functions |
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
index 36bde80..f34424b 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
@@ -29,6 +29,7 @@ object CodeGen {
             codeGenArgs.controllerTarget,
             codeGenArgs.modelOptions,
             codeGenArgs.clientOptions,
+            codeGenArgs.clientTarget,
             codeGenArgs.typeOverrides,
             codeGenArgs.srcPath,
             codeGenArgs.resourcesPath,
@@ -46,12 +47,13 @@ object CodeGen {
         controllerTarget: ControllerCodeGenTargetType,
         modelOptions: Set<ModelCodeGenOptionType>,
         clientOptions: Set<ClientCodeGenOptionType>,
+        clientTarget: ClientCodeGenTargetType,
         typeOverrides: Set<CodeGenTypeOverride>,
         srcPath: Path,
         resourcesPath: Path,
         validationLibrary: ValidationLibrary
     ) {
-        MutableSettings.updateSettings(codeGenTypes, controllerOptions, controllerTarget, modelOptions, clientOptions, typeOverrides, validationLibrary)
+        MutableSettings.updateSettings(codeGenTypes, controllerOptions, controllerTarget, modelOptions, clientOptions, clientTarget, typeOverrides, validationLibrary)
 
         val suppliedApi = pathToApi.toFile().readText()
         val baseDir = pathToApi.parent
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
index 8648045..b4f30f8 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
@@ -106,6 +106,13 @@ class CodeGenArgs {
     )
     var clientOptions: Set<ClientCodeGenOptionType> = emptySet()
 
+    @Parameter(
+        names = ["--http-client-target"],
+        description = "Optionally select the target client that you want to be generated. Defaults to OK_HTTP",
+        converter = ClientCodeGenTargetConverter::class
+    )
+    var clientTarget: ClientCodeGenTargetType = ClientCodeGenTargetType.OK_HTTP
+
     @Parameter(
         names = ["--src-path"],
         description = "Allows the path for generated source files to be overridden. Defaults to `src/main/kotlin`",
@@ -157,6 +164,10 @@ class ClientCodeGenOptionConverter : IStringConverter<ClientCodeGenOptionType> {
         convertToEnumValue(value)
 }
 
+class ClientCodeGenTargetConverter : IStringConverter<ClientCodeGenTargetType> {
+    override fun convert(value: String): ClientCodeGenTargetType = convertToEnumValue(value)
+}
+
 class ValidationLibraryOptionConverter : IStringConverter<ValidationLibrary> {
     override fun convert(value: String): ValidationLibrary = convertToEnumValue(value)
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
index 7a9aefe..0bdf7ea 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -22,7 +22,15 @@ enum class CodeGenerationType(val description: String) {
 }
 
 enum class ClientCodeGenOptionType(private val description: String) {
-    RESILIENCE4J("Generates a fault tolerance service for the client using the following library \"io.github.resilience4j:resilience4j-all:+\"");
+    RESILIENCE4J("Generates a fault tolerance service for the client using the following library \"io.github.resilience4j:resilience4j-all:+\" (only for OkHttp clients)"),
+    SUSPEND_MODIFIER("This option adds the suspend modifier to the generated client functions (only for OpenFeign clients)");
+
+    override fun toString() = "`${super.toString()}` - $description"
+}
+
+enum class ClientCodeGenTargetType(val description: String) {
+    OK_HTTP("Generate OkHttp client."),
+    OPEN_FEIGN("Generate OpenFeign client.");
 
     override fun toString() = "`${super.toString()}` - $description"
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
index 5186e29..4da27b7 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
@@ -7,6 +7,7 @@ import com.cjbooms.fabrikt.cli.CodeGenerationType.QUARKUS_REFLECTION_CONFIG
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.MutableSettings
 import com.cjbooms.fabrikt.generators.client.OkHttpClientGenerator
+import com.cjbooms.fabrikt.generators.client.OpenFeignInterfaceGenerator
 import com.cjbooms.fabrikt.generators.controller.MicronautControllerInterfaceGenerator
 import com.cjbooms.fabrikt.generators.controller.SpringControllerInterfaceGenerator
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
@@ -45,8 +46,14 @@ class CodeGenerator(
         sourceSet(controllers().files).plus(sourceSet(models().files))
 
     private fun generateClient(): Collection<GeneratedFile> {
-        val lib = OkHttpClientGenerator(packages, sourceApi, srcPath).generateLibrary(MutableSettings.clientOptions())
-        return sourceSet(client().files).plus(lib).plus(sourceSet(models().files))
+        val clientGenerator = when (MutableSettings.clientTarget()) {
+            ClientCodeGenTargetType.OK_HTTP -> OkHttpClientGenerator(packages, sourceApi, srcPath)
+            ClientCodeGenTargetType.OPEN_FEIGN -> OpenFeignInterfaceGenerator(packages, sourceApi)
+        }
+        val options = MutableSettings.clientOptions()
+        val clientFiles = clientGenerator.generate(options).files
+        val libFiles = clientGenerator.generateLibrary(options)
+        return sourceSet(clientFiles).plus(libFiles).plus(sourceSet(models().files))
     }
 
     private fun generateQuarkusReflectionResource(): Collection<GeneratedFile> = resourceSet(resources(models()))
@@ -78,7 +85,4 @@ class CodeGenerator(
             }
         return generator.generate()
     }
-
-    private fun client(): Clients =
-        OkHttpClientGenerator(packages, sourceApi, srcPath).generate(MutableSettings.clientOptions())
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt
index a8d7461..687806a 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt
@@ -131,8 +131,8 @@ object GeneratorUtils {
 
     fun Operation.hasMultipleContentMediaTypes(): Boolean? = this.firstResponse()?.hasMultipleContentMediaTypes()
 
-    fun Operation.hasMultipleResponseSchemas(): Boolean =
-            getBodyResponses().flatMap { it.contentMediaTypes.values }.map { it.schema.name }.distinct().size > 1
+    fun Operation.hasMultipleSuccessResponseSchemas(): Boolean =
+            getBodySuccessResponses().flatMap { it.contentMediaTypes.values }.map { it.schema.name }.distinct().size > 1
 
     fun Operation.getPathParams(): List<Parameter> = this.filterParams("path")
 
@@ -143,6 +143,12 @@ object GeneratorUtils {
     private fun Operation.getBodyResponses(): List<Response> =
         this.responses.filter { it.key != "default" }.values.filter(Response::hasContentMediaTypes)
 
+    private fun Operation.getBodySuccessResponses(): List<Response> =
+        getSuccessResponses().values.filter(Response::hasContentMediaTypes)
+
+    private fun Operation.getSuccessResponses(): Map<String, Response> =
+        this.responses.filter { it.key.toIntOrNull()?.let { status -> status in 200..399 } ?: false }
+
     private fun Operation.filterParams(paramType: String): List<Parameter> = this.parameters.filter { it.`in` == paramType }
 
     /**
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
index 1d253a3..f3b85e2 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
@@ -8,6 +8,7 @@ object MutableSettings {
     private lateinit var controllerTarget: ControllerCodeGenTargetType
     private lateinit var modelOptions: MutableSet<ModelCodeGenOptionType>
     private lateinit var clientOptions: MutableSet<ClientCodeGenOptionType>
+    private lateinit var clientTarget: ClientCodeGenTargetType
     private lateinit var typeOverrides: MutableSet<CodeGenTypeOverride>
     private lateinit var validationLibrary: ValidationLibrary
 
@@ -17,6 +18,7 @@ object MutableSettings {
         controllerTarget: ControllerCodeGenTargetType = ControllerCodeGenTargetType.SPRING,
         modelOptions: Set<ModelCodeGenOptionType> = emptySet(),
         clientOptions: Set<ClientCodeGenOptionType> = emptySet(),
+        clientTarget: ClientCodeGenTargetType = ClientCodeGenTargetType.OK_HTTP,
         typeOverrides: Set<CodeGenTypeOverride> = emptySet(),
         validationLibrary: ValidationLibrary = ValidationLibrary.JAVAX_VALIDATION
     ) {
@@ -25,6 +27,7 @@ object MutableSettings {
         this.controllerTarget = controllerTarget
         this.modelOptions = modelOptions.toMutableSet()
         this.clientOptions = clientOptions.toMutableSet()
+        this.clientTarget = clientTarget
         this.typeOverrides = typeOverrides.toMutableSet()
         this.validationLibrary = validationLibrary
     }
@@ -37,6 +40,7 @@ object MutableSettings {
     fun controllerTarget() = this.controllerTarget
     fun modelOptions() = this.modelOptions.toSet()
     fun clientOptions() = this.clientOptions.toSet()
+    fun clientTarget() = this.clientTarget
     fun typeOverrides() = this.typeOverrides.toSet()
     fun validationLibrary() = this.validationLibrary
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGenerator.kt
new file mode 100644
index 0000000..94733de
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGenerator.kt
@@ -0,0 +1,10 @@
+package com.cjbooms.fabrikt.generators.client
+
+import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
+import com.cjbooms.fabrikt.model.Clients
+import com.cjbooms.fabrikt.model.GeneratedFile
+
+interface ClientGenerator {
+    fun generate(options: Set<ClientCodeGenOptionType>): Clients
+    fun generateLibrary(options: Set<ClientCodeGenOptionType>): Collection<GeneratedFile>
+}
\ No newline at end of file
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 b26137a..31b9892 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGeneratorUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/ClientGeneratorUtils.kt
@@ -5,7 +5,7 @@ import com.cjbooms.fabrikt.generators.GeneratorUtils
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaType
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaTypeKey
 import com.cjbooms.fabrikt.generators.GeneratorUtils.hasMultipleContentMediaTypes
-import com.cjbooms.fabrikt.generators.GeneratorUtils.hasMultipleResponseSchemas
+import com.cjbooms.fabrikt.generators.GeneratorUtils.hasMultipleSuccessResponseSchemas
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toIncomingParameters
 import com.cjbooms.fabrikt.generators.OasDefault
@@ -18,6 +18,7 @@ import com.cjbooms.fabrikt.model.RequestParameter
 import com.fasterxml.jackson.databind.JsonNode
 import com.reprezen.kaizen.oasparser.model3.Operation
 import com.reprezen.kaizen.oasparser.model3.Path
+import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.TypeName
@@ -34,20 +35,21 @@ object ClientGeneratorUtils {
      * If there are several possible response schemas, then the return type is JsonNode, so they are all covered.
      * If no response body is found, Unit is returned.
      */
-    fun Operation.toClientReturnType(packages: Packages): TypeName {
-        val returnType =
-                if (hasMultipleResponseSchemas()) {
-                    JsonNode::class.asTypeName()
-                } else {
-                    this.getPrimaryContentMediaType()?.let {
-                        toModelType(
-                                packages.base,
-                                KotlinTypeInfo.from(it.value.schema)
-                        )
-                    } ?: Unit::class.asTypeName()
-                }
+    fun Operation.getReturnType(packages: Packages): TypeName {
+        return if (hasMultipleSuccessResponseSchemas()) {
+                JsonNode::class.asTypeName()
+            } else {
+                this.getPrimaryContentMediaType()?.let {
+                    toModelType(
+                        packages.base,
+                        KotlinTypeInfo.from(it.value.schema)
+                    )
+                } ?: Unit::class.asTypeName()
+            }
+    }
 
-        return "ApiResponse".toClassName(packages.client).parameterizedBy(returnType)
+    fun Operation.toClientReturnType(packages: Packages): TypeName {
+        return "ApiResponse".toClassName(packages.client).parameterizedBy(getReturnType(packages))
     }
 
     fun simpleClientName(resourceName: String) = "$resourceName${ClientType.SIMPLE_CLIENT_SUFFIX}"
@@ -83,13 +85,19 @@ object ClientGeneratorUtils {
         )
     }
 
-    fun FunSpec.Builder.addIncomingParameters(parameters: List<IncomingParameter>): FunSpec.Builder {
+    fun FunSpec.Builder.addIncomingParameters(
+        parameters: List<IncomingParameter>,
+        annotateRequestParameterWith: ((parameter: RequestParameter) -> AnnotationSpec?)? = null
+    ): FunSpec.Builder {
         val specs = parameters.map {
             val builder = it.toParameterSpecBuilder()
             if (it is RequestParameter) {
                 if (it.defaultValue != null) OasDefault.from(it.typeInfo, it.type, it.defaultValue)
                     ?.let { builder.defaultValue(it.getDefault()) }
                 else if (!it.isRequired) builder.defaultValue("null")
+                annotateRequestParameterWith?.invoke(it)?.let { annotationSpec ->
+                    builder.addAnnotation(annotationSpec)
+                }
             }
             builder.build()
         }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpClientGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpClientGenerator.kt
index c874123..d10b147 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpClientGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpClientGenerator.kt
@@ -11,18 +11,18 @@ class OkHttpClientGenerator(
     packages: Packages,
     api: SourceApi,
     srcPath: Path,
-) {
+) : ClientGenerator {
     private val simpleClientGenerator = OkHttpSimpleClientGenerator(packages, api, srcPath)
     private val enhancedClientGenerator = OkHttpEnhancedClientGenerator(packages, api, srcPath)
 
-    fun generate(options: Set<ClientCodeGenOptionType>): Clients {
+    override fun generate(options: Set<ClientCodeGenOptionType>): Clients {
         val simpleClient = simpleClientGenerator.generateDynamicClientCode()
         val enhancedClient = enhancedClientGenerator.generateDynamicClientCode(options)
 
         return Clients(enhancedClient.plus(simpleClient).toSet())
     }
 
-    fun generateLibrary(options: Set<ClientCodeGenOptionType>): Collection<GeneratedFile> {
+    override fun generateLibrary(options: Set<ClientCodeGenOptionType>): Collection<GeneratedFile> {
         val simpleClientLibrary = simpleClientGenerator.generateLibrary()
         val enhancedClientLibrary = enhancedClientGenerator.generateLibrary(options)
 
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OpenFeignInterfaceGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OpenFeignInterfaceGenerator.kt
new file mode 100644
index 0000000..9a5a66f
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OpenFeignInterfaceGenerator.kt
@@ -0,0 +1,269 @@
+package com.cjbooms.fabrikt.generators.client
+
+import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
+import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.GeneratorUtils.functionName
+import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaType
+import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
+import com.cjbooms.fabrikt.generators.TypeFactory
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ADDITIONAL_HEADERS_PARAMETER_NAME
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.addIncomingParameters
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.deriveClientParameters
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.getReturnType
+import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.simpleClientName
+import com.cjbooms.fabrikt.generators.client.metadata.OpenFeignAnnotations
+import com.cjbooms.fabrikt.model.ClientType
+import com.cjbooms.fabrikt.model.Clients
+import com.cjbooms.fabrikt.model.GeneratedFile
+import com.cjbooms.fabrikt.model.HeaderParam
+import com.cjbooms.fabrikt.model.IncomingParameter
+import com.cjbooms.fabrikt.model.KotlinTypeInfo
+import com.cjbooms.fabrikt.model.PathParam
+import com.cjbooms.fabrikt.model.QueryParam
+import com.cjbooms.fabrikt.model.RequestParameter
+import com.cjbooms.fabrikt.model.SourceApi
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.routeToPaths
+import com.cjbooms.fabrikt.util.toUpperCase
+import com.reprezen.kaizen.oasparser.model3.Operation
+import com.reprezen.kaizen.oasparser.model3.Path
+import com.squareup.kotlinpoet.AnnotationSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asTypeName
+import com.squareup.kotlinpoet.buildCodeBlock
+
+class OpenFeignInterfaceGenerator(
+    private val packages: Packages,
+    private val api: SourceApi,
+) : ClientGenerator {
+    override fun generate(options: Set<ClientCodeGenOptionType>): Clients {
+        val clientTypes = api.openApi3.routeToPaths().map { (resourceName, paths) ->
+            val funcSpecs: List<FunSpec> = paths.flatMap { (resource, path) ->
+                path.operations.map { (verb, operation) ->
+                    buildFunction(path, resource, operation, verb, options)
+                }
+            }
+
+            val clientType = TypeSpec.interfaceBuilder(simpleClientName(resourceName))
+                .addAnnotation(AnnotationSpec.builder(Suppress::class).addMember("%S", "unused").build())
+                .addFunctions(funcSpecs)
+                .build()
+
+            ClientType(clientType, packages.base)
+        }.toSet()
+
+        return Clients(clientTypes)
+    }
+
+    override fun generateLibrary(options: Set<ClientCodeGenOptionType>): Collection<GeneratedFile> = emptyList()
+
+    private fun buildFunction(
+        path: Path,
+        resource: String,
+        operation: Operation,
+        verb: String,
+        options: Set<ClientCodeGenOptionType>,
+    ): FunSpec {
+        val parameters = deriveClientParameters(path, operation, packages.base)
+        return FunSpec
+            .builder(functionName(operation, resource, verb))
+            .addModifiers(KModifier.ABSTRACT)
+            .addKdoc(operation.toKdoc(parameters))
+            .addRequestLineAnnotation(resource, verb, parameters)
+            .addHeadersAnnotation(operation, parameters)
+            .addSuspendModifier(options)
+            .addIncomingParameters(
+                parameters,
+                annotateRequestParameterWith = { parameter ->
+                    OpenFeignAnnotations.paramBuilder()
+                        .addMember("%S", parameter.name)
+                        .build()
+                },
+            )
+            .addParameter(
+                ParameterSpec.builder(
+                    ADDITIONAL_HEADERS_PARAMETER_NAME,
+                    TypeFactory.createMapOfStringToType(String::class.asTypeName()),
+                )
+                    .addAnnotation(OpenFeignAnnotations.HEADER_MAP)
+                    .defaultValue("emptyMap()")
+                    .build(),
+            )
+            .returns(operation.getReturnType(packages))
+            .build()
+    }
+
+    /**
+     * Adds OpenFeign's @RequestLine annotation to the function spec
+     */
+    private fun FunSpec.Builder.addRequestLineAnnotation(
+        resource: String,
+        verb: String,
+        parameters: List<IncomingParameter>,
+    ): FunSpec.Builder {
+        val annotation = RequestLineBuilder(resource, verb, parameters).build()
+        addAnnotation(annotation)
+        return this
+    }
+
+    private class RequestLineBuilder(
+        private val resource: String,
+        private val verb: String,
+        private val parameters: List<IncomingParameter>,
+    ) {
+        /**
+         * Gives the url path so that it can be used as input for OpenFeign's @RequestLine.
+         * For query and path variables, the deduplicated parameter name will be used instead of the original name.
+         */
+        private fun buildUrlPath(): String {
+            val (pathParameters, queryParameters) = parameters.getPathAndQueryParameters()
+            val resolvedResource = resourceWithDeduplicatedParamNames(resource, pathParameters)
+            return if (queryParameters.isNotEmpty()) {
+                "$resolvedResource?${queryParameters.joinToString("&") { "${it.originalName}={${it.name}}" }}"
+            } else {
+                resolvedResource
+            }
+        }
+
+        private fun List<IncomingParameter>.getPathAndQueryParameters():
+            Pair<List<RequestParameter>, List<RequestParameter>> {
+            val queryParameters = mutableListOf<RequestParameter>()
+            val pathVariables = mutableListOf<RequestParameter>()
+            for (parameter in this) {
+                if (parameter is RequestParameter) {
+                    when (parameter.parameterLocation) {
+                        is QueryParam -> queryParameters.add(parameter)
+                        is PathParam -> pathVariables.add(parameter)
+                        else -> {
+                            // ignore
+                        }
+                    }
+                }
+            }
+            return Pair(pathVariables, queryParameters)
+        }
+
+        private fun resourceWithDeduplicatedParamNames(
+            resource: String,
+            pathParameters: List<RequestParameter>,
+        ): String {
+            var resolvedResource = resource
+            pathParameters.forEach { parameter ->
+                if (parameter.originalName != parameter.name) {
+                    resolvedResource = resolvedResource.replace("{${parameter.originalName}}", "{${parameter.name}}")
+                }
+            }
+            return resolvedResource
+        }
+
+        fun build(): AnnotationSpec {
+            val urlPath = buildUrlPath()
+            return OpenFeignAnnotations.requestLineBuilder()
+                .addMember("%S", "${verb.toUpperCase()} $urlPath")
+                .build()
+        }
+    }
+
+    /**
+     * Adds OpenFeign @Headers annotation to the function spec if any headers are detected.
+     * Formatting:
+     *  - If only one header is used, it will be formatted like @Headers("Accept: application/json").
+     *  - If multiple headers are used, it will be formatted as an array like
+     *    @Headers(["Brand: {brand}", "Accept: application/json"]).
+     * Defined as constant when:
+     * - If the defined header in the OAS spec is an enum with only one value, the header will be set as a constant.
+     * - If it is the added accepted header
+     * Accept header is added when:
+     * - If no Accept header is specified yet, then it will be added based on the primary content media type
+     *   as specified in the response.
+     */
+    private fun FunSpec.Builder.addHeadersAnnotation(
+        operation: Operation,
+        parameters: List<IncomingParameter>,
+    ): FunSpec.Builder {
+        HeadersAnnotationBuilder(operation, parameters).build()?.let { annotation ->
+            addAnnotation(annotation)
+        }
+
+        return this
+    }
+
+    private class HeadersAnnotationBuilder(
+        private val operation: Operation,
+        private val parameters: List<IncomingParameter>,
+    ) {
+        fun build(): AnnotationSpec? {
+            val headersValueParts = mutableListOf<String>()
+            var acceptHeaderExists = false
+            val headerParameters = parameters.getHeaderParameters()
+            for (parameter in headerParameters) {
+                headersValueParts.add(buildHeadersAnnotationValue(parameter))
+                acceptHeaderExists =
+                    acceptHeaderExists || parameter.name == ClientGeneratorUtils.ACCEPT_HEADER_NAME
+            }
+            // Add default accept header
+            if (!acceptHeaderExists) {
+                getDefaultAcceptHeaderAnnotationValue()?.let {
+                    headersValueParts.add(it)
+                }
+            }
+
+            return if (headersValueParts.isNotEmpty()) {
+                val format = if (headersValueParts.size == 1) "%L" else "value = [%L]"
+                OpenFeignAnnotations.headersBuilder()
+                    .addMember(format, headersValueParts.joinToString(", "))
+                    .build()
+            } else {
+                null
+            }
+        }
+
+        private fun List<IncomingParameter>.getHeaderParameters(): List<RequestParameter> {
+            val headerRequestParameters = mutableListOf<RequestParameter>()
+            for (parameter in this) {
+                if (parameter is RequestParameter && parameter.parameterLocation is HeaderParam) {
+                    headerRequestParameters.add(parameter)
+                }
+            }
+            return headerRequestParameters
+        }
+
+        private fun buildHeadersAnnotationValue(parameter: RequestParameter): String {
+            val parameterValue =
+                if (
+                    parameter.typeInfo is KotlinTypeInfo.Enum &&
+                    parameter.typeInfo.entries.size == 1
+                ) {
+                    parameter.typeInfo.entries.first()
+                } else {
+                    "{${parameter.name}}"
+                }
+            return buildCodeBlock {
+                add("%S", "${parameter.originalName}: $parameterValue")
+            }.toString()
+        }
+
+        private fun getDefaultAcceptHeaderAnnotationValue(): String? {
+            return operation.getPrimaryContentMediaType()?.key?.let { mediaType ->
+                buildCodeBlock {
+                    add(
+                        "%S",
+                        "${ClientGeneratorUtils.ACCEPT_HEADER_NAME}: $mediaType",
+                    )
+                }.toString()
+            }
+        }
+    }
+
+    /**
+     * Adds suspend as modified to the func spec so that i can be used with CoroutineFeign
+     */
+    private fun FunSpec.Builder.addSuspendModifier(options: Set<ClientCodeGenOptionType>): FunSpec.Builder {
+        if (options.contains(ClientCodeGenOptionType.SUSPEND_MODIFIER)) {
+            this.addModifiers(KModifier.SUSPEND)
+        }
+        return this
+    }
+}
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/metadata/OpenFeign.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/metadata/OpenFeign.kt
new file mode 100644
index 0000000..fdff24a
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/metadata/OpenFeign.kt
@@ -0,0 +1,38 @@
+package com.cjbooms.fabrikt.generators.client.metadata
+
+import com.squareup.kotlinpoet.AnnotationSpec
+import com.squareup.kotlinpoet.ClassName
+
+object OpenFeignImports {
+
+    private object Packages {
+        const val FEIGN = "feign"
+    }
+
+    val REQUEST_LINE = ClassName(Packages.FEIGN, "RequestLine")
+
+    val HEADERS = ClassName(Packages.FEIGN, "Headers")
+
+    val HEADER_MAP = ClassName(Packages.FEIGN, "HeaderMap")
+
+    val PARAM = ClassName(Packages.FEIGN, "Param")
+}
+
+object OpenFeignAnnotations {
+    val HEADER_MAP: AnnotationSpec =
+        AnnotationSpec
+            .builder(OpenFeignImports.HEADER_MAP)
+            .build()
+
+    fun requestLineBuilder(): AnnotationSpec.Builder =
+        AnnotationSpec
+            .builder(OpenFeignImports.REQUEST_LINE)
+
+    fun headersBuilder(): AnnotationSpec.Builder =
+        AnnotationSpec
+            .builder(OpenFeignImports.HEADERS)
+
+    fun paramBuilder(): AnnotationSpec.Builder =
+        AnnotationSpec
+            .builder(OpenFeignImports.PARAM)
+}


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 23dadc9..de4b1a2 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
@@ -1,6 +1,7 @@
 package com.cjbooms.fabrikt.generators
 
 import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
+import com.cjbooms.fabrikt.cli.ClientCodeGenTargetType
 import com.cjbooms.fabrikt.cli.CodeGenerationType
 import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
 import com.cjbooms.fabrikt.configurations.Packages
@@ -28,7 +29,7 @@ class OkHttpClientGeneratorTest {
     @Suppress("unused")
     private fun fullApiTestCases(): Stream<String> = Stream.of(
         "okHttpClient",
-        "okHttpClientMultiMediaType",
+        "multiMediaType",
         "okHttpClientPostWithoutRequestBody",
         "pathLevelParameters",
         "parameterNameClash",
@@ -38,6 +39,7 @@ class OkHttpClientGeneratorTest {
     fun init() {
         MutableSettings.updateSettings(
             genTypes = setOf(CodeGenerationType.CLIENT),
+            clientTarget = ClientCodeGenTargetType.OK_HTTP,
             modelOptions = setOf(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS),
         )
     }
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt
new file mode 100644
index 0000000..e5c3dff
--- /dev/null
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OpenFeignClientGeneratorTest.kt
@@ -0,0 +1,109 @@
+package com.cjbooms.fabrikt.generators
+
+import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
+import com.cjbooms.fabrikt.cli.ClientCodeGenTargetType
+import com.cjbooms.fabrikt.cli.CodeGenerationType
+import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
+import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.client.OpenFeignInterfaceGenerator
+import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
+import com.cjbooms.fabrikt.model.ClientType
+import com.cjbooms.fabrikt.model.Models
+import com.cjbooms.fabrikt.model.SourceApi
+import com.cjbooms.fabrikt.util.Linter
+import com.cjbooms.fabrikt.util.ResourceHelper.readTextResource
+import com.squareup.kotlinpoet.FileSpec
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.stream.Stream
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class OpenFeignClientGeneratorTest {
+
+    @Suppress("unused")
+    private fun fullApiTestCases(): Stream<String> = Stream.of(
+        "openFeignClient",
+        "multiMediaType",
+        "pathLevelParameters",
+        "parameterNameClash",
+    )
+
+    @BeforeEach
+    fun init() {
+        MutableSettings.updateSettings(
+            genTypes = setOf(CodeGenerationType.CLIENT),
+            clientTarget = ClientCodeGenTargetType.OPEN_FEIGN,
+            modelOptions = setOf(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS),
+        )
+    }
+
+    @ParameterizedTest
+    @MethodSource("fullApiTestCases")
+    fun `correct Open Feign interfaces are generated from a full API definition`(testCaseName: String) {
+        runTestCase(testCaseName)
+    }
+
+    @Test
+    fun `correct Open Feign interfaces are generated with suspend modifier`() {
+        runTestCase(
+            testCaseName = "openFeignClient",
+            clientFileName = "SuspendableOpenFeignClient.kt",
+            options = setOf(ClientCodeGenOptionType.SUSPEND_MODIFIER),
+        )
+    }
+
+    private fun runTestCase(
+        testCaseName: String,
+        clientFileName: String = "OpenFeignClient.kt",
+        options: Set<ClientCodeGenOptionType> = emptySet(),
+    ) {
+        val packages = Packages("examples.$testCaseName")
+        val sourceApi = SourceApi(readTextResource("/examples/$testCaseName/api.yaml"))
+
+        val expectedModel = readTextResource("/examples/$testCaseName/models/Models.kt")
+        val expectedClient = readTextResource("/examples/$testCaseName/client/$clientFileName")
+
+        val models = JacksonModelGenerator(
+            packages,
+            sourceApi,
+        ).generate().toSingleFile()
+        val clientCode = OpenFeignInterfaceGenerator(
+            packages,
+            sourceApi,
+        )
+            .generate(options)
+            .clients
+            .toSingleFile()
+
+        assertThat(clientCode).isEqualTo(expectedClient)
+        assertThat(models).isEqualTo(expectedModel)
+    }
+
+    private fun Collection<ClientType>.toSingleFile(): String {
+        val destPackage = if (this.isNotEmpty()) first().destinationPackage else ""
+        val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
+        this.forEach {
+            val builder = singleFileBuilder
+                .addType(it.spec)
+            builder.build()
+        }
+        return Linter.lintString(singleFileBuilder.build().toString())
+    }
+
+    private fun Models.toSingleFile(): String {
+        val destPackage = if (models.isNotEmpty()) models.first().destinationPackage else ""
+        val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
+        models
+            .sortedBy { it.spec.name }
+            .forEach {
+                val builder = singleFileBuilder
+                    .addType(it.spec)
+                builder.build()
+            }
+        return Linter.lintString(singleFileBuilder.build().toString())
+    }
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/api.yaml b/src/test/resources/examples/multiMediaType/api.yaml
similarity index 91%
rename from src/test/resources/examples/okHttpClientMultiMediaType/api.yaml
rename to src/test/resources/examples/multiMediaType/api.yaml
index 3f775e0..7e24be8 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/api.yaml
+++ b/src/test/resources/examples/multiMediaType/api.yaml
@@ -69,6 +69,21 @@ paths:
             application/vnd.custom.media+json:
               schema:
                 $ref: '#/components/schemas/OtherQueryResult'
+  /different-success-and-error-response-schema:
+    get:
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/SuccessResponse"
+        400:
+          description: bad request
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/ErrorResponse"
 
 components:
   parameters:
@@ -239,4 +254,14 @@ components:
         extra_second_attr:
           description: "The attribute 2 for model 3"
           type: integer
-          readOnly: true
\ No newline at end of file
+          readOnly: true
+    SuccessResponse:
+      type: object
+      properties:
+        successMessage:
+          type: string
+    ErrorResponse:
+      type: object
+      properties:
+        errorMessage:
+          type: string
\ No newline at end of file
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt b/src/test/resources/examples/multiMediaType/client/ApiClient.kt
similarity index 78%
rename from src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
rename to src/test/resources/examples/multiMediaType/client/ApiClient.kt
index 8e41834..061995d 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiClient.kt
+++ b/src/test/resources/examples/multiMediaType/client/ApiClient.kt
@@ -1,10 +1,11 @@
-package examples.okHttpClientMultiMediaType.client
+package examples.multiMediaType.client
 
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
-import examples.okHttpClientMultiMediaType.models.ContentType
-import examples.okHttpClientMultiMediaType.models.QueryResult
+import examples.multiMediaType.models.ContentType
+import examples.multiMediaType.models.QueryResult
+import examples.multiMediaType.models.SuccessResponse
 import okhttp3.Headers
 import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -137,3 +138,36 @@ class MultipleResponseSchemasClient(
         return request.execute(client, objectMapper, jacksonTypeRef())
     }
 }
+
+@Suppress("unused")
+class DifferentSuccessAndErrorResponseSchemaClient(
+    private val objectMapper: ObjectMapper,
+    private val baseUrl: String,
+    private val client: OkHttpClient
+) {
+    /**
+     *
+     */
+    @Throws(ApiException::class)
+    fun getDifferentSuccessAndErrorResponseSchema(
+        additionalHeaders: Map<String, String> =
+            emptyMap()
+    ): ApiResponse<SuccessResponse> {
+        val httpUrl: HttpUrl = "$baseUrl/different-success-and-error-response-schema"
+            .toHttpUrl()
+            .newBuilder()
+            .build()
+
+        val headerBuilder = Headers.Builder()
+        additionalHeaders.forEach { headerBuilder.header(it.key, it.value) }
+        val httpHeaders: Headers = headerBuilder.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/multiMediaType/client/ApiModels.kt
similarity index 93%
rename from src/test/resources/examples/okHttpClientMultiMediaType/client/ApiModels.kt
rename to src/test/resources/examples/multiMediaType/client/ApiModels.kt
index a4d9699..f5ea731 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiModels.kt
+++ b/src/test/resources/examples/multiMediaType/client/ApiModels.kt
@@ -1,4 +1,4 @@
-package examples.okHttpClientMultiMediaType.client
+package examples.multiMediaType.client
 
 import okhttp3.Headers
 
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt b/src/test/resources/examples/multiMediaType/client/ApiService.kt
similarity index 73%
rename from src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
rename to src/test/resources/examples/multiMediaType/client/ApiService.kt
index d9153b6..b4310d3 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/ApiService.kt
+++ b/src/test/resources/examples/multiMediaType/client/ApiService.kt
@@ -1,9 +1,10 @@
-package examples.okHttpClientMultiMediaType.client
+package examples.multiMediaType.client
 
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
-import examples.okHttpClientMultiMediaType.models.ContentType
-import examples.okHttpClientMultiMediaType.models.QueryResult
+import examples.multiMediaType.models.ContentType
+import examples.multiMediaType.models.QueryResult
+import examples.multiMediaType.models.SuccessResponse
 import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
 import okhttp3.OkHttpClient
 import kotlin.Int
@@ -108,3 +109,33 @@ class MultipleResponseSchemasService(
             apiClient.getMultipleResponseSchemas(accept, additionalHeaders)
         }
 }
+
+/**
+ * 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 DifferentSuccessAndErrorResponseSchemaService(
+    private val circuitBreakerRegistry: CircuitBreakerRegistry,
+    objectMapper: ObjectMapper,
+    baseUrl: String,
+    client: OkHttpClient
+) {
+    var circuitBreakerName: String = "differentSuccessAndErrorResponseSchemaClient"
+
+    private val apiClient: DifferentSuccessAndErrorResponseSchemaClient =
+        DifferentSuccessAndErrorResponseSchemaClient(objectMapper, baseUrl, client)
+
+    @Throws(ApiException::class)
+    fun getDifferentSuccessAndErrorResponseSchema(
+        additionalHeaders: Map<String, String> =
+            emptyMap()
+    ): ApiResponse<SuccessResponse> =
+        withCircuitBreaker(circuitBreakerRegistry, circuitBreakerName) {
+            apiClient.getDifferentSuccessAndErrorResponseSchema(additionalHeaders)
+        }
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpResilience4jUtil.kt b/src/test/resources/examples/multiMediaType/client/HttpResilience4jUtil.kt
similarity index 89%
rename from src/test/resources/examples/okHttpClientMultiMediaType/client/HttpResilience4jUtil.kt
rename to src/test/resources/examples/multiMediaType/client/HttpResilience4jUtil.kt
index aac6956..c9b65d9 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpResilience4jUtil.kt
+++ b/src/test/resources/examples/multiMediaType/client/HttpResilience4jUtil.kt
@@ -1,4 +1,4 @@
-package examples.okHttpClientMultiMediaType.client
+package examples.multiMediaType.client
 
 import io.github.resilience4j.circuitbreaker.CircuitBreaker
 import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpUtil.kt b/src/test/resources/examples/multiMediaType/client/HttpUtil.kt
similarity index 98%
rename from src/test/resources/examples/okHttpClientMultiMediaType/client/HttpUtil.kt
rename to src/test/resources/examples/multiMediaType/client/HttpUtil.kt
index 2b93018..9979101 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/HttpUtil.kt
+++ b/src/test/resources/examples/multiMediaType/client/HttpUtil.kt
@@ -1,4 +1,4 @@
-package examples.okHttpClientMultiMediaType.client
+package examples.multiMediaType.client
 
 import com.fasterxml.jackson.core.type.TypeReference
 import com.fasterxml.jackson.databind.ObjectMapper
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/client/OAuth.kt b/src/test/resources/examples/multiMediaType/client/OAuth.kt
similarity index 92%
rename from src/test/resources/examples/okHttpClientMultiMediaType/client/OAuth.kt
rename to src/test/resources/examples/multiMediaType/client/OAuth.kt
index 687fda4..1201b4d 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/client/OAuth.kt
+++ b/src/test/resources/examples/multiMediaType/client/OAuth.kt
@@ -1,4 +1,4 @@
-package examples.okHttpClientMultiMediaType.client
+package examples.multiMediaType.client
 
 import okhttp3.Authenticator
 import okhttp3.Interceptor
diff --git a/src/test/resources/examples/multiMediaType/client/OpenFeignClient.kt b/src/test/resources/examples/multiMediaType/client/OpenFeignClient.kt
new file mode 100644
index 0000000..eafcd53
--- /dev/null
+++ b/src/test/resources/examples/multiMediaType/client/OpenFeignClient.kt
@@ -0,0 +1,82 @@
+package examples.multiMediaType.client
+
+import com.fasterxml.jackson.databind.JsonNode
+import examples.multiMediaType.models.ContentType
+import examples.multiMediaType.models.QueryResult
+import examples.multiMediaType.models.SuccessResponse
+import feign.HeaderMap
+import feign.Headers
+import feign.Param
+import feign.RequestLine
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@Suppress("unused")
+interface ExamplePath1Client {
+    /**
+     * GET example path 1
+     *
+     * @param explodeListQueryParam
+     * @param queryParam2
+     * @param acceptHeader
+     */
+    @RequestLine("GET /example-path-1?explode_list_query_param={explodeListQueryParam}&query_param2={queryParam2}")
+    @Headers(value = ["Accept: {acceptHeader}", "Accept: application/vnd.custom.media+xml"])
+    fun getExamplePath1(
+        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
+        @Param("queryParam2") queryParam2: Int? = null,
+        @Param("acceptHeader") acceptHeader: String = "application/vnd.custom.media+xml",
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    ): QueryResult
+}
+
+@Suppress("unused")
+interface ExamplePath2Client {
+    /**
+     * GET example path 1
+     *
+     * @param explodeListQueryParam
+     * @param queryParam2
+     * @param accept the content type accepted by the client
+     */
+    @RequestLine("GET /example-path-2?explode_list_query_param={explodeListQueryParam}&query_param2={queryParam2}")
+    @Headers(value = ["Accept: {accept}", "Accept: application/vnd.custom.media+xml"])
+    fun getExamplePath2(
+        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
+        @Param("queryParam2") queryParam2: Int? = null,
+        @Param("accept") accept: ContentType? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    ): QueryResult
+}
+
+@Suppress("unused")
+interface MultipleResponseSchemasClient {
+    /**
+     * GET with multiple response content schemas
+     *
+     * @param accept the content type accepted by the client
+     */
+    @RequestLine("GET /multiple-response-schemas")
+    @Headers(value = ["Accept: {accept}", "Accept: application/json"])
+    fun getMultipleResponseSchemas(
+        @Param("accept") accept: ContentType? = null,
+        @HeaderMap
+        additionalHeaders: Map<String, String> = emptyMap()
+    ): JsonNode
+}
+
+@Suppress("unused")
+interface DifferentSuccessAndErrorResponseSchemaClient {
+    /**
+     *
+     */
+    @RequestLine("GET /different-success-and-error-response-schema")
+    @Headers("Accept: application/json")
+    fun getDifferentSuccessAndErrorResponseSchema(
+        @HeaderMap additionalHeaders: Map<String, String> =
+            emptyMap()
+    ): SuccessResponse
+}
diff --git a/src/test/resources/examples/okHttpClientMultiMediaType/models/Models.kt b/src/test/resources/examples/multiMediaType/models/Models.kt
similarity index 94%
rename from src/test/resources/examples/okHttpClientMultiMediaType/models/Models.kt
rename to src/test/resources/examples/multiMediaType/models/Models.kt
index aca4d17..2595659 100644
--- a/src/test/resources/examples/okHttpClientMultiMediaType/models/Models.kt
+++ b/src/test/resources/examples/multiMediaType/models/Models.kt
@@ -1,4 +1,4 @@
-package examples.okHttpClientMultiMediaType.models
+package examples.multiMediaType.models
 
 import com.fasterxml.jackson.annotation.JsonProperty
 import com.fasterxml.jackson.annotation.JsonSubTypes
@@ -101,6 +101,12 @@ enum class ContentType(
     }
 }
 
+data class ErrorResponse(
+    @param:JsonProperty("errorMessage")
+    @get:JsonProperty("errorMessage")
+    val errorMessage: String? = null
+)
+
 data class FirstModel(
     @param:JsonProperty("id")
     @get:JsonProperty("id")
@@ -172,6 +178,12 @@ data class SecondModel(
     override val modelType: ContentModelType = ContentModelType.SECOND_MODEL
 ) : Content(id, firstAttr, secondAttr, thirdAttr, etag)
 
+data class SuccessResponse(
+    @param:JsonProperty("successMessage")
+    @get:JsonProperty("successMessage")
+    val successMessage: String? = null
+)
+
 data class ThirdModel(
     @param:JsonProperty("id")
     @get:JsonProperty("id")
diff --git a/src/test/resources/examples/openFeignClient/api.yaml b/src/test/resources/examples/openFeignClient/api.yaml
new file mode 100644
index 0000000..cb3bff0
--- /dev/null
+++ b/src/test/resources/examples/openFeignClient/api.yaml
@@ -0,0 +1,310 @@
+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+json:
+              schema:
+                $ref: "#/components/schemas/QueryResult"
+
+    post:
+      summary: "POST example path 1"
+      parameters:
+        - $ref: "#/components/parameters/ListQueryParamExploded"
+      requestBody:
+        $ref: "#/components/requestBodies/PostBody"
+      responses:
+        201:
+          headers:
+            Location:
+              $ref: "#/components/headers/Location"
+          description: "Successful operation"
+
+  /example-path-2/{path_param}:
+    get:
+      summary: "GET example path 2"
+      parameters:
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/QueryParam2"
+        - $ref: "#/components/parameters/IfNoneMatch"
+        - $ref: "#/components/parameters/PaginationLimitParam"
+      responses:
+        200:
+          description: "successful operation"
+          headers:
+            Cache-Control:
+              $ref: "#/components/headers/CacheControl"
+          content:
+            application/json:
+              schema:
+                discriminator:
+                  propertyName: "model_type"
+                oneOf:
+                  - $ref: "#/components/schemas/FirstModel"
+                  - $ref: "#/components/schemas/SecondModel"
+                  - $ref: "#/components/schemas/ThirdModel"
+        304:
+          description: "Returned in conjunction with use of the If-None-Match header.\
+            \ Indicates that the resource as described by the passed etag value has\
+            \ not changed"
+    head:
+      operationId: headOperationIdExample
+      summary: "HEAD example path 2"
+      parameters:
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/QueryParam3"
+        - $ref: "#/components/parameters/IfNoneMatch"
+      responses:
+        200:
+          description: "Record Exists"
+        304:
+          description: "Returned in conjunction with use of the If-None-Match header.\
+            \ Indicates that the resource as described by the passed etag value has\
+            \ not changed"
+
+    put:
+      summary: "PUT example path 2"
+      parameters:
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/IfMatch"
+      requestBody:
+        $ref: "#/components/requestBodies/PutBody"
+      responses:
+        204:
+          description: "Operation successful"
+
+  /example-path-3/{path_param}/subresource:
+    put:
+      summary: "PUT example path 3"
+      parameters:
+        - $ref: "#/components/parameters/ListQueryParamCsv"
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/IfMatch"
+      requestBody:
+        $ref: "#/components/requestBodies/PutBody"
+      responses:
+        204:
+          description: "Operation successful"
+
+components:
+  parameters:
+    PathParam:
+      name: "path_param"
+      description: "The resource id"
+      in: "path"
+      required: true
+      schema:
+        type: "string"
+
+    IfMatch:
+      name: "If-Match"
+      description: "The RFC7232 If-Match header field"
+      in: "header"
+      schema:
+        type: "string"
+      required: true
+
+    IfNoneMatch:
+      name: "If-None-Match"
+      description: "The RFC7232 If-None-Match header field"
+      in: "header"
+      schema:
+        type: "string"
+      required: false
+
+    ListQueryParamExploded:
+      name: "explode_list_query_param"
+      in: "query"
+      schema:
+        type: "array"
+        items:
+          type: "string"
+      required: false
+      allowEmptyValue: false
+      
+    ListQueryParamCsv:
+      name: "csv_list_query_param"
+      in: "query"
+      explode: false
+      schema:
+        type: "array"
+        items:
+          type: "string"
+      required: false
+      allowEmptyValue: false
+      
+    PaginationLimitParam:
+      name: limit
+      in: query
+      schema:
+        type: integer
+        minimum: 1
+        maximum: 1000
+        default: 500
+      required: false
+
+    QueryParam2:
+      name: "query_param2"
+      in: "query"
+      schema:
+        type: integer
+      required: false
+
+    QueryParam3:
+      name: "query_param3"
+      in: "query"
+      schema:
+        type: boolean
+      required: false
+
+  headers:
+    Location:
+      description: "The Location header indicates the URL of a newly created resource"
+      schema:
+        type: "string"
+
+    CacheControl:
+      description: "The RFC7234 Cache Control header"
+      schema:
+        type: "string"
+      example: "must-revalidate, max-age=5"
+
+  requestBodies:
+    PostBody:
+      required: true
+      content:
+        application/json:
+          schema:
+            discriminator:
+              propertyName: "model_type"
+            oneOf:
+              - $ref: "#/components/schemas/FirstModel"
+              - $ref: "#/components/schemas/SecondModel"
+              - $ref: "#/components/schemas/ThirdModel"
+    PutBody:
+      required: true
+      content:
+        application/json:
+          schema:
+            $ref: "#/components/schemas/FirstModel"
+
+
+  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/openFeignClient/client/OpenFeignClient.kt b/src/test/resources/examples/openFeignClient/client/OpenFeignClient.kt
new file mode 100644
index 0000000..583b93c
--- /dev/null
+++ b/src/test/resources/examples/openFeignClient/client/OpenFeignClient.kt
@@ -0,0 +1,119 @@
+package examples.openFeignClient.client
+
+import examples.openFeignClient.models.Content
+import examples.openFeignClient.models.FirstModel
+import examples.openFeignClient.models.QueryResult
+import feign.HeaderMap
+import feign.Headers
+import feign.Param
+import feign.RequestLine
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@Suppress("unused")
+interface ExamplePath1Client {
+    /**
+     * GET example path 1
+     *
+     * @param explodeListQueryParam
+     * @param queryParam2
+     */
+    @RequestLine("GET /example-path-1?explode_list_query_param={explodeListQueryParam}&query_param2={queryParam2}")
+    @Headers("Accept: application/vnd.custom.media+json")
+    fun getExamplePath1(
+        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
+        @Param("queryParam2") queryParam2: Int? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    ): QueryResult
+
+    /**
+     * POST example path 1
+     *
+     * @param content
+     * @param explodeListQueryParam
+     */
+    @RequestLine("POST /example-path-1?explode_list_query_param={explodeListQueryParam}")
+    fun postExamplePath1(
+        content: Content,
+        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
+
+@Suppress("unused")
+interface ExamplePath2Client {
+    /**
+     * GET example path 2
+     *
+     * @param pathParam The resource id
+     * @param limit
+     * @param queryParam2
+     * @param ifNoneMatch The RFC7232 If-None-Match header field
+     */
+    @RequestLine("GET /example-path-2/{pathParam}?limit={limit}&query_param2={queryParam2}")
+    @Headers(value = ["If-None-Match: {ifNoneMatch}", "Accept: application/json"])
+    fun getExamplePath2PathParam(
+        @Param("pathParam") pathParam: String,
+        @Param("limit") limit: Int = 500,
+        @Param("queryParam2") queryParam2: Int? = null,
+        @Param("ifNoneMatch") ifNoneMatch: String? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    ): Content
+
+    /**
+     * HEAD example path 2
+     *
+     * @param pathParam The resource id
+     * @param queryParam3
+     * @param ifNoneMatch The RFC7232 If-None-Match header field
+     */
+    @RequestLine("HEAD /example-path-2/{pathParam}?query_param3={queryParam3}")
+    @Headers("If-None-Match: {ifNoneMatch}")
+    fun headOperationIdExample(
+        @Param("pathParam") pathParam: String,
+        @Param("queryParam3") queryParam3: Boolean? = null,
+        @Param("ifNoneMatch") ifNoneMatch: String? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+
+    /**
+     * PUT example path 2
+     *
+     * @param firstModel
+     * @param pathParam The resource id
+     * @param ifMatch The RFC7232 If-Match header field
+     */
+    @RequestLine("PUT /example-path-2/{pathParam}")
+    @Headers("If-Match: {ifMatch}")
+    fun putExamplePath2PathParam(
+        firstModel: FirstModel,
+        @Param("pathParam") pathParam: String,
+        @Param("ifMatch") ifMatch: String,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
+
+@Suppress("unused")
+interface ExamplePath3SubresourceClient {
+    /**
+     * PUT example path 3
+     *
+     * @param firstModel
+     * @param pathParam The resource id
+     * @param ifMatch The RFC7232 If-Match header field
+     * @param csvListQueryParam
+     */
+    @RequestLine("PUT /example-path-3/{pathParam}/subresource?csv_list_query_param={csvListQueryParam}")
+    @Headers("If-Match: {ifMatch}")
+    fun putExamplePath3PathParamSubresource(
+        firstModel: FirstModel,
+        @Param("pathParam") pathParam: String,
+        @Param("ifMatch") ifMatch: String,
+        @Param("csvListQueryParam") csvListQueryParam: List<String>? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
diff --git a/src/test/resources/examples/openFeignClient/client/SuspendableOpenFeignClient.kt b/src/test/resources/examples/openFeignClient/client/SuspendableOpenFeignClient.kt
new file mode 100644
index 0000000..f4146cc
--- /dev/null
+++ b/src/test/resources/examples/openFeignClient/client/SuspendableOpenFeignClient.kt
@@ -0,0 +1,119 @@
+package examples.openFeignClient.client
+
+import examples.openFeignClient.models.Content
+import examples.openFeignClient.models.FirstModel
+import examples.openFeignClient.models.QueryResult
+import feign.HeaderMap
+import feign.Headers
+import feign.Param
+import feign.RequestLine
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@Suppress("unused")
+interface ExamplePath1Client {
+    /**
+     * GET example path 1
+     *
+     * @param explodeListQueryParam
+     * @param queryParam2
+     */
+    @RequestLine("GET /example-path-1?explode_list_query_param={explodeListQueryParam}&query_param2={queryParam2}")
+    @Headers("Accept: application/vnd.custom.media+json")
+    suspend fun getExamplePath1(
+        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
+        @Param("queryParam2") queryParam2: Int? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    ): QueryResult
+
+    /**
+     * POST example path 1
+     *
+     * @param content
+     * @param explodeListQueryParam
+     */
+    @RequestLine("POST /example-path-1?explode_list_query_param={explodeListQueryParam}")
+    suspend fun postExamplePath1(
+        content: Content,
+        @Param("explodeListQueryParam") explodeListQueryParam: List<String>? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
+
+@Suppress("unused")
+interface ExamplePath2Client {
+    /**
+     * GET example path 2
+     *
+     * @param pathParam The resource id
+     * @param limit
+     * @param queryParam2
+     * @param ifNoneMatch The RFC7232 If-None-Match header field
+     */
+    @RequestLine("GET /example-path-2/{pathParam}?limit={limit}&query_param2={queryParam2}")
+    @Headers(value = ["If-None-Match: {ifNoneMatch}", "Accept: application/json"])
+    suspend fun getExamplePath2PathParam(
+        @Param("pathParam") pathParam: String,
+        @Param("limit") limit: Int = 500,
+        @Param("queryParam2") queryParam2: Int? = null,
+        @Param("ifNoneMatch") ifNoneMatch: String? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    ): Content
+
+    /**
+     * HEAD example path 2
+     *
+     * @param pathParam The resource id
+     * @param queryParam3
+     * @param ifNoneMatch The RFC7232 If-None-Match header field
+     */
+    @RequestLine("HEAD /example-path-2/{pathParam}?query_param3={queryParam3}")
+    @Headers("If-None-Match: {ifNoneMatch}")
+    suspend fun headOperationIdExample(
+        @Param("pathParam") pathParam: String,
+        @Param("queryParam3") queryParam3: Boolean? = null,
+        @Param("ifNoneMatch") ifNoneMatch: String? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+
+    /**
+     * PUT example path 2
+     *
+     * @param firstModel
+     * @param pathParam The resource id
+     * @param ifMatch The RFC7232 If-Match header field
+     */
+    @RequestLine("PUT /example-path-2/{pathParam}")
+    @Headers("If-Match: {ifMatch}")
+    suspend fun putExamplePath2PathParam(
+        firstModel: FirstModel,
+        @Param("pathParam") pathParam: String,
+        @Param("ifMatch") ifMatch: String,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
+
+@Suppress("unused")
+interface ExamplePath3SubresourceClient {
+    /**
+     * PUT example path 3
+     *
+     * @param firstModel
+     * @param pathParam The resource id
+     * @param ifMatch The RFC7232 If-Match header field
+     * @param csvListQueryParam
+     */
+    @RequestLine("PUT /example-path-3/{pathParam}/subresource?csv_list_query_param={csvListQueryParam}")
+    @Headers("If-Match: {ifMatch}")
+    suspend fun putExamplePath3PathParamSubresource(
+        firstModel: FirstModel,
+        @Param("pathParam") pathParam: String,
+        @Param("ifMatch") ifMatch: String,
+        @Param("csvListQueryParam") csvListQueryParam: List<String>? = null,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
diff --git a/src/test/resources/examples/openFeignClient/models/Models.kt b/src/test/resources/examples/openFeignClient/models/Models.kt
new file mode 100644
index 0000000..c0faf13
--- /dev/null
+++ b/src/test/resources/examples/openFeignClient/models/Models.kt
@@ -0,0 +1,168 @@
+package examples.openFeignClient.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
+import kotlin.collections.Map
+
+@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 ContentModelType(
+    @JsonValue
+    val value: String
+) {
+    FIRST_MODEL("first_model"),
+
+    SECOND_MODEL("second_model"),
+
+    THIRD_MODEL("third_model");
+
+    companion object {
+        private val mapping: Map<String, ContentModelType> =
+            values().associateBy(ContentModelType::value)
+
+        fun fromValue(value: String): ContentModelType? = mapping[value]
+    }
+}
+
+enum class ContentThirdAttr(
+    @JsonValue
+    val value: String
+) {
+    ENUM_TYPE_1("enum_type_1"),
+
+    ENUM_TYPE_2("enum_type_2");
+
+    companion object {
+        private val mapping: Map<String, ContentThirdAttr> =
+            values().associateBy(ContentThirdAttr::value)
+
+        fun fromValue(value: String): ContentThirdAttr? = mapping[value]
+    }
+}
+
+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,
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    @param:JsonProperty("model_type")
+    override val modelType: ContentModelType = ContentModelType.FIRST_MODEL
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag)
+
+data class QueryResult(
+    @param:JsonProperty("items")
+    @get:JsonProperty("items")
+    @get:NotNull
+    @get:Size(min = 0)
+    @get:Valid
+    val items: List<Content>
+)
+
+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,
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    @param:JsonProperty("model_type")
+    override val modelType: ContentModelType = ContentModelType.SECOND_MODEL
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag)
+
+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,
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    @param:JsonProperty("model_type")
+    override val modelType: ContentModelType = ContentModelType.THIRD_MODEL
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag)
diff --git a/src/test/resources/examples/parameterNameClash/client/OpenFeignClient.kt b/src/test/resources/examples/parameterNameClash/client/OpenFeignClient.kt
new file mode 100644
index 0000000..cb84e17
--- /dev/null
+++ b/src/test/resources/examples/parameterNameClash/client/OpenFeignClient.kt
@@ -0,0 +1,38 @@
+package examples.parameterNameClash.client
+
+import examples.parameterNameClash.models.SomeObject
+import feign.HeaderMap
+import feign.Param
+import feign.RequestLine
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.Map
+
+@Suppress("unused")
+interface ExampleClient {
+    /**
+     *
+     *
+     * @param pathB
+     * @param queryB
+     */
+    @RequestLine("GET /example/{pathB}?b={queryB}")
+    fun getExampleB(
+        @Param("pathB") pathB: String,
+        @Param("queryB") queryB: String,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+
+    /**
+     *
+     *
+     * @param bodySomeObject example
+     * @param querySomeObject
+     */
+    @RequestLine("POST /example?someObject={querySomeObject}")
+    fun postExample(
+        bodySomeObject: SomeObject,
+        @Param("querySomeObject") querySomeObject: String,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}
diff --git a/src/test/resources/examples/pathLevelParameters/client/OpenFeignClient.kt b/src/test/resources/examples/pathLevelParameters/client/OpenFeignClient.kt
new file mode 100644
index 0000000..b6b90e3
--- /dev/null
+++ b/src/test/resources/examples/pathLevelParameters/client/OpenFeignClient.kt
@@ -0,0 +1,24 @@
+package examples.pathLevelParameters.client
+
+import feign.HeaderMap
+import feign.Param
+import feign.RequestLine
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.Map
+
+@Suppress("unused")
+interface ExampleClient {
+    /**
+     *
+     *
+     * @param a
+     * @param b
+     */
+    @RequestLine("GET /example?a={a}&b={b}")
+    fun getExample(
+        @Param("a") a: String,
+        @Param("b") b: String,
+        @HeaderMap additionalHeaders: Map<String, String> = emptyMap()
+    )
+}