Repository: cjbooms/fabrikt
Pull request #164 (2023-02-27 17:55:43)

Support Micronaut controllers
Fixes https://github.com/cjbooms/fabrikt/issues/154

At the moment, this is yet a draft. I haven't done any tests (neither automated unit tests, nor manual tests in practice) so far and might need to hash out some details for code generation. However, I would appreciate some input already, whether this is a solution which you'd be okay with in general.

cjbooms: 👍 

Patch:
diff --git a/README.md b/README.md
index 6b6c4bf..70a0efa 100644
--- a/README.md
+++ b/README.md
@@ -123,34 +123,38 @@ data class ConcreteImplTwo(
 
 This section documents the available CLI parameters for controlling what gets generated. This documentation is generated using: `./gradlew printCodeGenUsage`
 
-| Parameter                  | Description
-| -------------------------- | --------------------------
-|   `--api-file`             | This must be a valid Open API v3 spec. All code generation will be based off this input.
-|   `--api-fragment`         | A partial Open API v3 fragment, to be combined with the primary API for code generation purposes.
-| * `--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:+"
-|   `--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
-|   `--http-model-opts`      | Select the options for the http models that you want to be generated.
-|                            | CHOOSE ANY OF:
-|                            |   `X_EXTENSIBLE_ENUMS` - This option treats x-extensible-enums as enums
-|                            |   `JAVA_SERIALIZATION` - This option adds Java Serializable interface to the generated models
-|                            |   `QUARKUS_REFLECTION` - This option adds @RegisterForReflection to the generated models. Requires dependency "'io.quarkus:quarkus-core:+"
-|                            |   `MICRONAUT_INTROSPECTION` - This option adds @Introspected to the generated models. Requires dependency "'io.micronaut:micronaut-core:+"
-|                            |   `MICRONAUT_REFLECTION` - This option adds @ReflectiveAccess to the generated models. Requires dependency "'io.micronaut:micronaut-core:+"
-|                            |   `INCLUDE_COMPANION_OBJECT` - This option adds a companion object to the generated models.
-|   `--output-directory`     | Allows the generation dir to be overridden. Defaults to current dir
-|   `--resources-path`       | Allows the path for generated resources to be overridden. Defaults to `src/main/resources`
-|   `--src-path`             | Allows the path for generated source files to be overridden. Defaults to `src/main/kotlin`
-|   `--targets`              | Targets are the parts of the application that you want to be generated.
-|                            | CHOOSE ANY OF:
-|                            |   `HTTP_MODELS` - Jackson annotated data classes to represent the schema objects defined in the input.
-|                            |   `CONTROLLERS` - Spring annotated HTTP controllers for each of the endpoints defined in the input.
-|                            |   `CLIENT` - Simple http rest client.
-|                            |   `QUARKUS_REFLECTION_CONFIG` - This options generates the reflection-config.json file for quarkus integration projects
+| Parameter                    | Description
+| ---------------------------- | ----------------------------
+|   `--api-file`               | This must be a valid Open API v3 spec. All code generation will be based off this input.
+|   `--api-fragment`           | A partial Open API v3 fragment, to be combined with the primary API for code generation purposes.
+| * `--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:+"
+|   `--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
+|   `--http-controller-target` | Optionally select the target framework for the controllers that you want to be generated. Defaults to Spring Controllers
+|                              | CHOOSE ONE OF:
+|                              |   `SPRING` - Generate for Spring framework.
+|                              |   `MICRONAUT` - Generate for Micronaut framework.
+|   `--http-model-opts`        | Select the options for the http models that you want to be generated.
+|                              | CHOOSE ANY OF:
+|                              |   `X_EXTENSIBLE_ENUMS` - This option treats x-extensible-enums as enums
+|                              |   `JAVA_SERIALIZATION` - This option adds Java Serializable interface to the generated models
+|                              |   `QUARKUS_REFLECTION` - This option adds @RegisterForReflection to the generated models. Requires dependency "'io.quarkus:quarkus-core:+"
+|                              |   `MICRONAUT_INTROSPECTION` - This option adds @Introspected to the generated models. Requires dependency "'io.micronaut:micronaut-core:+"
+|                              |   `MICRONAUT_REFLECTION` - This option adds @ReflectiveAccess to the generated models. Requires dependency "'io.micronaut:micronaut-core:+"
+|                              |   `INCLUDE_COMPANION_OBJECT` - This option adds a companion object to the generated models.
+|   `--output-directory`       | Allows the generation dir to be overridden. Defaults to current dir
+|   `--resources-path`         | Allows the path for generated resources to be overridden. Defaults to `src/main/resources`
+|   `--src-path`               | Allows the path for generated source files to be overridden. Defaults to `src/main/kotlin`
+|   `--targets`                | Targets are the parts of the application that you want to be generated.
+|                              | CHOOSE ANY OF:
+|                              |   `HTTP_MODELS` - Jackson annotated data classes to represent the schema objects defined in the input.
+|                              |   `CONTROLLERS` - Spring / Micronaut annotated HTTP controllers for each of the endpoints defined in the input.
+|                              |   `CLIENT` - Simple http rest client.
+|                              |   `QUARKUS_REFLECTION_CONFIG` - This options generates the reflection-config.json file for quarkus integration projects
 
 ### Command Line
 Fabrikt is packaged as an executable jar, allowing it to be integrated into any build tool. The CLI can be invoked as follows:
diff --git a/build.gradle.kts b/build.gradle.kts
index 478124d..4000c37 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -55,7 +55,9 @@ dependencies {
     testImplementation("org.assertj:assertj-core:3.14.0")
     // Below dependencies are solely present so code examples in the test resources dir compile
     testImplementation("javax.validation:validation-api:2.0.1.Final")
-    testImplementation("org.springframework:spring-webmvc:5.3.23")
+    testImplementation("org.springframework:spring-webmvc:5.1.9.RELEASE")
+    testImplementation("io.micronaut:micronaut-core:3.8.4")
+    testImplementation("io.micronaut:micronaut-http:3.8.4")
     testImplementation("com.squareup.okhttp3:okhttp:4.9.1")
     testImplementation("com.pinterest.ktlint:ktlint-core:0.41.0")
     testImplementation("com.pinterest:ktlint:0.41.0")
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
index 135302d..4f85dcf 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
@@ -26,6 +26,7 @@ object CodeGen {
             codeGenArgs.targets,
             codeGenArgs.apiFragments.map { it.toFile().readText() },
             codeGenArgs.controllerOptions,
+            codeGenArgs.controllerTarget,
             codeGenArgs.modelOptions,
             codeGenArgs.clientOptions,
             codeGenArgs.srcPath,
@@ -40,12 +41,13 @@ object CodeGen {
         codeGenTypes: Set<CodeGenerationType>,
         apiFragments: List<String> = emptyList(),
         controllerOptions: Set<ControllerCodeGenOptionType>,
+        controllerTarget: ControllerCodeGenTargetType,
         modelOptions: Set<ModelCodeGenOptionType>,
         clientOptions: Set<ClientCodeGenOptionType>,
         srcPath: Path,
         resourcesPath: Path,
     ) {
-        MutableSettings.updateSettings(codeGenTypes, controllerOptions, modelOptions, clientOptions)
+        MutableSettings.updateSettings(codeGenTypes, controllerOptions, controllerTarget, modelOptions, clientOptions)
 
         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 d85b29c..43bcb6f 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
@@ -85,6 +85,13 @@ class CodeGenArgs {
     )
     var controllerOptions: Set<ControllerCodeGenOptionType> = emptySet()
 
+    @Parameter(
+        names = ["--http-controller-target"],
+        description = "Optionally select the target framework for the controllers that you want to be generated. Defaults to Spring Controllers",
+        converter = ControllerCodeGenTargetConverter::class
+    )
+    var controllerTarget: ControllerCodeGenTargetType = ControllerCodeGenTargetType.SPRING
+
     @Parameter(
         names = ["--http-model-opts"],
         description = "Select the options for the http models that you want to be generated.",
@@ -123,6 +130,10 @@ class ControllerCodeGenOptionConverter : IStringConverter<ControllerCodeGenOptio
     override fun convert(value: String): ControllerCodeGenOptionType = convertToEnumValue(value)
 }
 
+class ControllerCodeGenTargetConverter : IStringConverter<ControllerCodeGenTargetType> {
+    override fun convert(value: String): ControllerCodeGenTargetType = convertToEnumValue(value)
+}
+
 class ModelCodeGenOptionConverter : IStringConverter<ModelCodeGenOptionType> {
     override fun convert(value: String): ModelCodeGenOptionType = 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 52cbc67..e09b0b2 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -5,7 +5,7 @@ enum class CodeGenerationType(val description: String) {
         "Jackson annotated data classes to represent the schema objects defined in the input."
     ),
     CONTROLLERS(
-        "Spring annotated HTTP controllers for each of the endpoints defined in the input."
+        "Spring / Micronaut annotated HTTP controllers for each of the endpoints defined in the input."
     ),
     CLIENT(
         "Simple http rest client."
@@ -37,5 +37,12 @@ enum class ModelCodeGenOptionType(val description: String) {
 enum class ControllerCodeGenOptionType(val description: String) {
     SUSPEND_MODIFIER("This option adds the suspend modifier to the generated controller functions");
 
+    override fun toString() = "`${super.toString()}` - $description"
+}
+
+enum class ControllerCodeGenTargetType(val description: String) {
+    SPRING("Generate for Spring framework."),
+    MICRONAUT("Generate for Micronaut framework.");
+
     override fun toString() = "`${super.toString()}` - $description"
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
index 3514ddb..c274e12 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
@@ -7,13 +7,14 @@ 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.controller.MicronautControllerInterfaceGenerator
 import com.cjbooms.fabrikt.generators.controller.SpringControllerInterfaceGenerator
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
 import com.cjbooms.fabrikt.generators.model.QuarkusReflectionModelGenerator
 import com.cjbooms.fabrikt.model.Clients
-import com.cjbooms.fabrikt.model.Controllers
 import com.cjbooms.fabrikt.model.GeneratedFile
 import com.cjbooms.fabrikt.model.KotlinSourceSet
+import com.cjbooms.fabrikt.model.KotlinTypes
 import com.cjbooms.fabrikt.model.Models
 import com.cjbooms.fabrikt.model.ResourceFile
 import com.cjbooms.fabrikt.model.ResourceSourceSet
@@ -60,8 +61,23 @@ class CodeGenerator(
     private fun resources(models: Models): List<ResourceFile> =
         listOfNotNull(QuarkusReflectionModelGenerator(models, MutableSettings.generationTypes()).generate())
 
-    private fun controllers(): Controllers =
-        SpringControllerInterfaceGenerator(packages, sourceApi, MutableSettings.controllerOptions()).generate()
+    private fun controllers(): KotlinTypes {
+        val generator =
+            when (MutableSettings.controllerTarget()) {
+                ControllerCodeGenTargetType.SPRING -> SpringControllerInterfaceGenerator(
+                    packages,
+                    sourceApi,
+                    MutableSettings.controllerOptions()
+                )
+
+                ControllerCodeGenTargetType.MICRONAUT -> MicronautControllerInterfaceGenerator(
+                    packages,
+                    sourceApi,
+                    MutableSettings.controllerOptions()
+                )
+            }
+        return generator.generate()
+    }
 
     private fun client(): Clients =
         OkHttpClientGenerator(packages, sourceApi, srcPath).generate(MutableSettings.clientOptions())
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
index cc3431e..c5b1d79 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/MutableSettings.kt
@@ -3,30 +3,35 @@ package com.cjbooms.fabrikt.generators
 import com.cjbooms.fabrikt.cli.ClientCodeGenOptionType
 import com.cjbooms.fabrikt.cli.CodeGenerationType
 import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
+import com.cjbooms.fabrikt.cli.ControllerCodeGenTargetType
 import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
 
 object MutableSettings {
     private lateinit var generationTypes: MutableSet<CodeGenerationType>
     private lateinit var controllerOptions: MutableSet<ControllerCodeGenOptionType>
+    private lateinit var controllerTarget: ControllerCodeGenTargetType
     private lateinit var modelOptions: MutableSet<ModelCodeGenOptionType>
     private lateinit var clientOptions: MutableSet<ClientCodeGenOptionType>
 
     fun updateSettings(
-        genTypes: Set<CodeGenerationType>,
-        controllerOptions: Set<ControllerCodeGenOptionType>,
-        modelOptions: Set<ModelCodeGenOptionType>,
-        clientOptions: Set<ClientCodeGenOptionType>
+        genTypes: Set<CodeGenerationType> = emptySet(),
+        controllerOptions: Set<ControllerCodeGenOptionType> = emptySet(),
+        controllerTarget: ControllerCodeGenTargetType = ControllerCodeGenTargetType.SPRING,
+        modelOptions: Set<ModelCodeGenOptionType> = emptySet(),
+        clientOptions: Set<ClientCodeGenOptionType> = emptySet()
     ) {
         this.generationTypes = genTypes.toMutableSet()
         this.controllerOptions = controllerOptions.toMutableSet()
+        this.controllerTarget = controllerTarget
         this.modelOptions = modelOptions.toMutableSet()
         this.clientOptions = clientOptions.toMutableSet()
     }
 
     fun addOption(option: ModelCodeGenOptionType) = modelOptions.add(option)
 
+    fun generationTypes() = this.generationTypes.toSet()
     fun controllerOptions() = this.controllerOptions.toSet()
+    fun controllerTarget() = this.controllerTarget
     fun modelOptions() = this.modelOptions.toSet()
-    fun generationTypes() = this.generationTypes.toSet()
     fun clientOptions() = this.clientOptions.toSet()
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerInterfaceGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerInterfaceGenerator.kt
new file mode 100644
index 0000000..92f7dfd
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/ControllerInterfaceGenerator.kt
@@ -0,0 +1,53 @@
+package com.cjbooms.fabrikt.generators.controller
+
+import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.controller.metadata.JavaXAnnotations
+import com.cjbooms.fabrikt.model.ControllerType
+import com.cjbooms.fabrikt.model.KotlinTypes
+import com.cjbooms.fabrikt.model.RequestParameter
+import com.cjbooms.fabrikt.model.SourceApi
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.basePath
+import com.cjbooms.fabrikt.util.toUpperCase
+import com.reprezen.kaizen.oasparser.model3.Operation
+import com.reprezen.kaizen.oasparser.model3.Path
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.TypeSpec
+
+abstract class ControllerInterfaceGenerator(
+    private val packages: Packages,
+    private val api: SourceApi
+) {
+    abstract fun generate(): KotlinTypes
+    abstract fun buildFunction(path: Path, op: Operation, verb: String): FunSpec
+    abstract fun controllerBuilder(className: String, basePath: String): TypeSpec.Builder
+    fun buildController(resourceName: String, paths: Collection<Path>): ControllerType {
+        val typeBuilder: TypeSpec.Builder = controllerBuilder(
+            className = ControllerGeneratorUtils.controllerName(resourceName),
+            basePath = api.openApi3.basePath()
+        )
+
+        paths.flatMap { path ->
+            path.operations
+                .filter { it.key.toUpperCase() != "HEAD" }
+                .map { op ->
+                    buildFunction(
+                        path,
+                        op.value,
+                        op.key,
+                    )
+                }
+        }.forEach { typeBuilder.addFunction(it) }
+
+        return ControllerType(
+            typeBuilder.build(),
+            packages.base
+        )
+    }
+    fun ParameterSpec.Builder.addValidationAnnotations(parameter: RequestParameter): ParameterSpec.Builder {
+        if (parameter.minimum != null) this.addAnnotation(JavaXAnnotations.min(parameter.minimum.toInt()))
+        if (parameter.maximum != null) this.addAnnotation(JavaXAnnotations.max(parameter.maximum.toInt()))
+        if (parameter.typeInfo.isComplexType) this.addAnnotation(JavaXAnnotations.validBuilder().build())
+        return this
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt
new file mode 100644
index 0000000..9b4549b
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt
@@ -0,0 +1,175 @@
+package com.cjbooms.fabrikt.generators.controller
+
+import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
+import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.GeneratorUtils.toIncomingParameters
+import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
+import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.happyPathResponse
+import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.methodName
+import com.cjbooms.fabrikt.generators.controller.metadata.JavaXAnnotations
+import com.cjbooms.fabrikt.generators.controller.metadata.MicronautImports
+import com.cjbooms.fabrikt.model.BodyParameter
+import com.cjbooms.fabrikt.model.ControllerType
+import com.cjbooms.fabrikt.model.HeaderParam
+import com.cjbooms.fabrikt.model.KotlinTypes
+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.isSingleResource
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.routeToPaths
+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.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.TypeSpec
+
+class MicronautControllerInterfaceGenerator(
+    private val packages: Packages,
+    private val api: SourceApi,
+    private val options: Set<ControllerCodeGenOptionType> = emptySet()
+) : ControllerInterfaceGenerator(packages, api) {
+
+    private val useSuspendModifier: Boolean
+        get() = options.any { it == ControllerCodeGenOptionType.SUSPEND_MODIFIER }
+
+    override fun generate(): MicronautControllers =
+        MicronautControllers(
+            api.openApi3.routeToPaths().map { (resourceName, paths) ->
+                buildController(resourceName, paths.values)
+            }.toSet()
+        )
+
+    override fun controllerBuilder(
+        className: String,
+        basePath: String
+    ) =
+        TypeSpec.interfaceBuilder(className)
+            .addAnnotation(
+                AnnotationSpec
+                    .builder(MicronautImports.CONTROLLER)
+                    .build()
+            )
+
+    override fun buildFunction(
+        path: Path,
+        op: Operation,
+        verb: String,
+    ): FunSpec {
+        val methodName = methodName(op, verb, path.pathString.isSingleResource())
+        val returnType = MicronautImports.RESPONSE.parameterizedBy(op.happyPathResponse(packages.base))
+        val parameters = op.toIncomingParameters(packages.base, path.parameters, emptyList())
+
+        // Main method builder
+        val funcSpec = FunSpec
+            .builder(methodName)
+            .addModifiers(KModifier.ABSTRACT)
+            .addKdoc(op.toKdoc(parameters))
+            .addMicronautFunAnnotation(op, verb, path.pathString)
+            .apply {
+                if (useSuspendModifier)
+                    addModifiers(KModifier.SUSPEND)
+            }
+            .returns(returnType)
+
+        // Function parameters
+        parameters
+            .map {
+                when (it) {
+                    is BodyParameter ->
+                        it
+                            .toParameterSpecBuilder()
+                            .addAnnotation(
+                                AnnotationSpec
+                                    .builder(MicronautImports.BODY).build()
+                            )
+                            .addAnnotation(JavaXAnnotations.validBuilder().build())
+                            .build()
+
+                    is RequestParameter ->
+                        it
+                            .toParameterSpecBuilder()
+                            .addValidationAnnotations(it)
+                            .addMicronautParamAnnotation(it)
+                            .build()
+                }
+            }
+            .forEach { funcSpec.addParameter(it) }
+
+        return funcSpec.build()
+    }
+
+    private fun FunSpec.Builder.addMicronautFunAnnotation(op: Operation, verb: String, path: String): FunSpec.Builder {
+        val produces = op.responses
+            .flatMap { it.value.contentMediaTypes.keys }
+            .toTypedArray()
+
+        val consumes = op.requestBody
+            .contentMediaTypes.keys
+            .toTypedArray()
+
+        this.addAnnotation(
+            AnnotationSpec
+                .builder(MicronautImports.HttpMethods.byName(verb))
+                .addMember("uri = %S", path).build()
+        )
+
+        if (consumes.isNotEmpty()) {
+            this.addAnnotation(
+                AnnotationSpec
+                    .builder(MicronautImports.CONSUMES)
+                    .addMember(
+                        "value = %L",
+                        consumes.joinToString(
+                            prefix = "[",
+                            postfix = "]",
+                            separator = ", ",
+                            transform = { "\"$it\"" })
+                    )
+                    .build()
+            )
+        }
+
+        if (produces.isNotEmpty()) {
+            this.addAnnotation(
+                AnnotationSpec
+                    .builder(MicronautImports.PRODUCES)
+                    .addMember(
+                        "value = %L",
+                        produces.joinToString(
+                            prefix = "[",
+                            postfix = "]",
+                            separator = ", ",
+                            transform = { "\"$it\"" })
+                    )
+                    .build()
+            )
+        }
+
+        return this
+    }
+
+    private fun ParameterSpec.Builder.addMicronautParamAnnotation(parameter: RequestParameter): ParameterSpec.Builder =
+        when (parameter.parameterLocation) {
+            QueryParam -> AnnotationSpec
+                .builder(MicronautImports.QUERY_VALUE)
+
+            HeaderParam -> AnnotationSpec
+                .builder(MicronautImports.HEADER)
+
+            PathParam -> AnnotationSpec
+                .builder(MicronautImports.PATH_VARIABLE)
+        }.let {
+            it.addMember("value = %S", parameter.oasName)
+
+            if (parameter.defaultValue != null)
+                it.addMember("defaultValue = %S", parameter.defaultValue)
+
+            this.addAnnotation(it.build())
+        }
+}
+
+data class MicronautControllers(val controllers: Collection<ControllerType>) : KotlinTypes(controllers)
\ No newline at end of file
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt
index 8189962..76b9cbd 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt
@@ -4,7 +4,6 @@ import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toIncomingParameters
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
-import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.controllerName
 import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.happyPathResponse
 import com.cjbooms.fabrikt.generators.controller.ControllerGeneratorUtils.methodName
 import com.cjbooms.fabrikt.generators.controller.metadata.JavaXAnnotations
@@ -12,66 +11,39 @@ import com.cjbooms.fabrikt.generators.controller.metadata.SpringAnnotations
 import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
 import com.cjbooms.fabrikt.model.BodyParameter
 import com.cjbooms.fabrikt.model.ControllerType
-import com.cjbooms.fabrikt.model.Controllers
 import com.cjbooms.fabrikt.model.HeaderParam
 import com.cjbooms.fabrikt.model.KotlinTypeInfo
+import com.cjbooms.fabrikt.model.KotlinTypes
 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.basePath
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSingleResource
 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.FileSpec
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterSpec
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.TypeSpec
-import com.squareup.kotlinpoet.asTypeName
-import java.time.LocalDate
-import java.time.OffsetDateTime
 
 class SpringControllerInterfaceGenerator(
     private val packages: Packages,
     private val api: SourceApi,
     private val options: Set<ControllerCodeGenOptionType> = emptySet()
-) {
+) : ControllerInterfaceGenerator(packages, api) {
 
-    fun generate(): Controllers =
-        Controllers(
+    override fun generate(): SpringControllers =
+        SpringControllers(
             api.openApi3.routeToPaths().map { (resourceName, paths) ->
                 buildController(resourceName, paths.values)
             }.toSet()
         )
 
-    private fun buildController(resourceName: String, paths: Collection<Path>): ControllerType {
-        val typeBuilder: TypeSpec.Builder = controllerBuilder(
-            className = controllerName(resourceName),
-            basePath = api.openApi3.basePath()
-        )
-
-        paths.flatMap { path ->
-            path.operations
-                .filter { it.key.toUpperCase() != "HEAD" }
-                .map { op ->
-                    buildFunction(
-                        path,
-                        op.value,
-                        op.key,
-                    )
-                }
-        }.forEach { typeBuilder.addFunction(it) }
-
-        return ControllerType(
-            typeBuilder.build(),
-            packages.base
-        )
-    }
-
-    private fun controllerBuilder(
+    override fun controllerBuilder(
         className: String,
         basePath: String
     ) =
@@ -80,7 +52,7 @@ class SpringControllerInterfaceGenerator(
             .addAnnotation(SpringAnnotations.VALIDATED)
             .addAnnotation(SpringAnnotations.requestMappingBuilder().addMember("%S", basePath).build())
 
-    private fun buildFunction(
+    override fun buildFunction(
         path: Path,
         op: Operation,
         verb: String,
@@ -150,13 +122,6 @@ class SpringControllerInterfaceGenerator(
         return this
     }
 
-    private fun ParameterSpec.Builder.addValidationAnnotations(parameter: RequestParameter): ParameterSpec.Builder {
-        if (parameter.minimum != null) this.addAnnotation(JavaXAnnotations.min(parameter.minimum.toInt()))
-        if (parameter.maximum != null) this.addAnnotation(JavaXAnnotations.max(parameter.maximum.toInt()))
-        if (parameter.typeInfo.isComplexType) this.addAnnotation(JavaXAnnotations.validBuilder().build())
-        return this
-    }
-
     private fun ParameterSpec.Builder.addSpringParamAnnotation(parameter: RequestParameter): ParameterSpec.Builder =
         when (parameter.parameterLocation) {
             QueryParam -> SpringAnnotations.requestParamBuilder()
@@ -183,3 +148,15 @@ class SpringControllerInterfaceGenerator(
         return this
     }
 }
+
+data class SpringControllers(val controllers: Collection<ControllerType>) : KotlinTypes(controllers) {
+    override val files: Collection<FileSpec> = super.files.map {
+        it.toBuilder()
+            .addImport(SpringImports.Static.REQUEST_METHOD.first, SpringImports.Static.REQUEST_METHOD.second)
+            .addImport(
+                SpringImports.Static.RESPONSE_STATUS.first,
+                SpringImports.Static.RESPONSE_STATUS.second
+            )
+            .build()
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/metadata/Micronaut.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/metadata/Micronaut.kt
new file mode 100644
index 0000000..5725bc9
--- /dev/null
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/metadata/Micronaut.kt
@@ -0,0 +1,47 @@
+package com.cjbooms.fabrikt.generators.controller.metadata
+
+import com.cjbooms.fabrikt.util.toUpperCase
+import com.squareup.kotlinpoet.ClassName
+
+object MicronautImports {
+
+    private object Packages {
+        const val MICRONAUT_BASE = "io.micronaut"
+
+        const val MICRONAUT_HTTP = "$MICRONAUT_BASE.http"
+
+        const val MICRONAUT_HTTP_ANNOTATION = "$MICRONAUT_HTTP.annotation"
+    }
+
+    val CONSUMES = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Consumes")
+    val PRODUCES = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Produces")
+    val RESPONSE = ClassName(Packages.MICRONAUT_HTTP, "HttpResponse")
+
+    val CONTROLLER = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Controller")
+    val BODY = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Body")
+    val HEADER = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Header")
+    val QUERY_VALUE = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "QueryValue")
+    val PATH_VARIABLE = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "PathVariable")
+
+    object HttpMethods {
+        val GET = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Get")
+        val POST = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Post")
+        val PUT = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Put")
+        val PATCH = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Patch")
+        val DELETE = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Delete")
+        val OPTIONS = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Options")
+        val HEAD = ClassName(Packages.MICRONAUT_HTTP_ANNOTATION, "Head")
+
+        fun byName(name: String) =
+            when (name.toUpperCase()) {
+                "GET" -> GET
+                "POST" -> POST
+                "PUT" -> PUT
+                "PATCH" -> PATCH
+                "DELETE" -> DELETE
+                "OPTIONS" -> OPTIONS
+                "HEAD" -> HEAD
+                else -> throw IllegalArgumentException(name)
+            }
+    }
+}
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
index abaf905..e1ca1bd 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
@@ -1,6 +1,5 @@
 package com.cjbooms.fabrikt.model
 
-import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
 import com.cjbooms.fabrikt.generators.model.JacksonMetadata
 import com.cjbooms.fabrikt.model.Destinations.clientPackage
 import com.cjbooms.fabrikt.model.Destinations.controllersPackage
@@ -54,18 +53,6 @@ data class Clients(val clients: Collection<ClientType>) : KotlinTypes(clients) {
     }
 }
 
-data class Controllers(val controllers: Collection<ControllerType>) : KotlinTypes(controllers) {
-    override val files: Collection<FileSpec> = super.files.map {
-        it.toBuilder()
-            .addImport(SpringImports.Static.REQUEST_METHOD.first, SpringImports.Static.REQUEST_METHOD.second)
-            .addImport(
-                SpringImports.Static.RESPONSE_STATUS.first,
-                SpringImports.Static.RESPONSE_STATUS.second
-            )
-            .build()
-    }
-}
-
 fun <T : GeneratedType> Collection<T>.toFileSpec(): Collection<FileSpec> = groupClasses(this)
     .map {
         val builder = FileSpec.builder(it.key.destinationPackage, it.key.className.simpleName)


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt
new file mode 100644
index 0000000..4a89176
--- /dev/null
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt
@@ -0,0 +1,172 @@
+package com.cjbooms.fabrikt.generators
+
+import com.cjbooms.fabrikt.cli.CodeGenerationType
+import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
+import com.cjbooms.fabrikt.cli.ControllerCodeGenTargetType
+import com.cjbooms.fabrikt.configurations.Packages
+import com.cjbooms.fabrikt.generators.controller.MicronautControllerInterfaceGenerator
+import com.cjbooms.fabrikt.generators.controller.MicronautControllers
+import com.cjbooms.fabrikt.model.Destinations.controllersPackage
+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 com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.TypeSpec
+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 MicronautControllerGeneratorTest {
+    private val basePackage = "ie.zalando"
+    private lateinit var generated: Collection<FileSpec>
+
+    @Suppress("unused")
+    private fun testCases(): Stream<String> = Stream.of(
+        "githubApi",
+        "singleAllOf",
+        "pathLevelParameters",
+        "parameterNameClash",
+    )
+
+    private fun setupGithubApiTestEnv() {
+        val api = SourceApi(readTextResource("/examples/githubApi/api.yaml"))
+        generated = MicronautControllerInterfaceGenerator(Packages(basePackage), api).generate().files
+    }
+
+    @BeforeEach
+    fun init() {
+        MutableSettings.updateSettings(
+            genTypes = setOf(CodeGenerationType.CONTROLLERS),
+            controllerTarget = ControllerCodeGenTargetType.MICRONAUT
+        )
+    }
+
+    // @Test
+    // fun `debug single test`() = `correct models are generated for different OpenApi Specifications`("singleAllOf")
+
+    @ParameterizedTest
+    @MethodSource("testCases")
+    fun `correct models are generated for different OpenApi Specifications`(testCaseName: String) {
+        val basePackage = "examples.$testCaseName"
+        val api = SourceApi(readTextResource("/examples/$testCaseName/api.yaml"))
+        val expectedControllers = readTextResource("/examples/$testCaseName/controllers/micronaut/Controllers.kt")
+
+        val controllers = MicronautControllerInterfaceGenerator(
+            Packages(basePackage),
+            api
+        ).generate().toSingleFile()
+
+        assertThat(controllers).isEqualTo(expectedControllers)
+    }
+
+    @Test
+    fun `should contain correct number of controller classes`() {
+        setupGithubApiTestEnv()
+        assertThat(generated.size).isEqualTo(6)
+    }
+
+    @Test
+    fun `the correct controllers are generated`() {
+        setupGithubApiTestEnv()
+        assertThat(generated.map { it.name })
+            .containsOnly(
+                "InternalEventsController",
+                "ContributorsController",
+                "OrganisationsController",
+                "OrganisationsContributorsController",
+                "RepositoriesController",
+                "RepositoriesPullRequestsController"
+            )
+    }
+
+    @Test
+    fun `ensure package is correct`() {
+        setupGithubApiTestEnv()
+        assertThat(generated.map { it.packageName }.distinct())
+            .containsOnly(controllersPackage(basePackage))
+    }
+
+    @Test
+    fun `ensure controller has correct annotations`() {
+        setupGithubApiTestEnv()
+        val controllerAnnotations =
+            generated.flatMap { it.members.flatMap { (it as TypeSpec).annotationSpecs.map { it.className.toString() } } }
+                .distinct()
+
+        assertThat(controllerAnnotations).containsOnly(
+            "io.micronaut.http.annotation.Controller",
+        )
+    }
+
+    @Test
+    fun `ensure that subresource specific controllers are created`() {
+        val api = SourceApi(readTextResource("/examples/githubApi/api.yaml"))
+        val controllers = MicronautControllerInterfaceGenerator(Packages(basePackage), api).generate()
+
+        assertThat(controllers.files).size().isEqualTo(6)
+        assertThat(controllers.files.map { it.name }).containsAll(
+            listOf(
+                "ContributorsController",
+                "OrganisationsController",
+                "OrganisationsContributorsController",
+                "RepositoriesController",
+                "RepositoriesPullRequestsController"
+            )
+        )
+
+        val linkedFunctionNames = controllers.files
+            .filter { it.name == "OrganisationsContributorsController" }
+            .flatMap { it.members }
+            .flatMap { (it as TypeSpec).funSpecs.map { it.name } }
+        val ownedFunctionNames = controllers.files
+            .filter { it.name == "RepositoriesPullRequestsController" }
+            .flatMap { it.members }
+            .flatMap { (it as TypeSpec).funSpecs.map { it.name } }
+        assertThat(linkedFunctionNames).containsExactlyInAnyOrder(
+            "get",
+            "getById",
+            "putById",
+            "deleteById"
+        )
+        assertThat(ownedFunctionNames).containsExactlyInAnyOrder(
+            "get",
+            "getById",
+            "post",
+            "putById"
+        )
+    }
+
+    @Test
+    fun `ensure controller methods has the correct modifiers`() {
+        val api = SourceApi(readTextResource("/examples/githubApi/api.yaml"))
+        val controllers = MicronautControllerInterfaceGenerator(
+            Packages(basePackage),
+            api,
+            setOf(ControllerCodeGenOptionType.SUSPEND_MODIFIER)
+        ).generate()
+
+        assertThat(controllers.files).size().isEqualTo(6)
+        assertThat(
+            controllers.files
+                .flatMap { file -> file.members }
+                .flatMap { (it as TypeSpec).funSpecs.map(FunSpec::modifiers) }
+                .all { it.contains(KModifier.SUSPEND) }
+        ).isTrue()
+    }
+
+    private fun MicronautControllers.toSingleFile(): String {
+        val destPackage = if (controllers.isNotEmpty()) controllers.first().destinationPackage else ""
+        val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
+        controllers.forEach {
+            singleFileBuilder.addType(it.spec).build()
+        }
+        return Linter.lintString(singleFileBuilder.build().toString())
+    }
+}
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
index c902e75..8387e8a 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -49,7 +49,9 @@ class ModelGeneratorTest {
 
     @BeforeEach
     fun init() {
-        MutableSettings.updateSettings(setOf(CodeGenerationType.HTTP_MODELS), emptySet(), emptySet(), emptySet())
+        MutableSettings.updateSettings(
+            genTypes = setOf(CodeGenerationType.HTTP_MODELS),
+        )
     }
 
     // @Test
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
index e2a4339..23dadc9 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
@@ -37,10 +37,8 @@ class OkHttpClientGeneratorTest {
     @BeforeEach
     fun init() {
         MutableSettings.updateSettings(
-            setOf(CodeGenerationType.CLIENT),
-            emptySet(),
-            setOf(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS),
-            emptySet()
+            genTypes = setOf(CodeGenerationType.CLIENT),
+            modelOptions = setOf(ModelCodeGenOptionType.X_EXTENSIBLE_ENUMS),
         )
     }
 
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt
index a3deff0..85ff7a3 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ResourceGeneratorTest.kt
@@ -23,7 +23,9 @@ class ResourceGeneratorTest {
 
     @BeforeEach
     fun init() {
-        MutableSettings.updateSettings(setOf(CodeGenerationType.HTTP_MODELS), emptySet(), emptySet(), emptySet())
+        MutableSettings.updateSettings(
+            genTypes = setOf(CodeGenerationType.HTTP_MODELS),
+        )
     }
 
     @ParameterizedTest
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt
index 328702a..f323890 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt
@@ -4,8 +4,8 @@ import com.cjbooms.fabrikt.cli.CodeGenerationType
 import com.cjbooms.fabrikt.cli.ControllerCodeGenOptionType
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.controller.SpringControllerInterfaceGenerator
+import com.cjbooms.fabrikt.generators.controller.SpringControllers
 import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
-import com.cjbooms.fabrikt.model.Controllers
 import com.cjbooms.fabrikt.model.Destinations.controllersPackage
 import com.cjbooms.fabrikt.model.SourceApi
 import com.cjbooms.fabrikt.util.Linter
@@ -42,7 +42,7 @@ class SpringControllerGeneratorTest {
 
     @BeforeEach
     fun init() {
-        MutableSettings.updateSettings(setOf(CodeGenerationType.CONTROLLERS), emptySet(), emptySet(), emptySet())
+        MutableSettings.updateSettings(genTypes = setOf(CodeGenerationType.CONTROLLERS))
     }
 
     // @Test
@@ -53,7 +53,7 @@ class SpringControllerGeneratorTest {
     fun `correct models are generated for different OpenApi Specifications`(testCaseName: String) {
         val basePackage = "examples.$testCaseName"
         val api = SourceApi(readTextResource("/examples/$testCaseName/api.yaml"))
-        val expectedControllers = readTextResource("/examples/$testCaseName/controllers/Controllers.kt")
+        val expectedControllers = readTextResource("/examples/$testCaseName/controllers/spring/Controllers.kt")
 
         val controllers = SpringControllerInterfaceGenerator(
             Packages(basePackage),
@@ -160,7 +160,7 @@ class SpringControllerGeneratorTest {
         ).isTrue()
     }
 
-    private fun Controllers.toSingleFile(): String {
+    private fun SpringControllers.toSingleFile(): String {
         val destPackage = if (controllers.isNotEmpty()) controllers.first().destinationPackage else ""
         val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
         controllers.forEach {
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt b/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt
index a4146ee..1d2d65c 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/util/ResourceHelper.kt
@@ -1,5 +1,8 @@
 package com.cjbooms.fabrikt.util
 
+import java.io.FileNotFoundException
+
 object ResourceHelper {
-    fun readTextResource(path: String): String = javaClass.getResource(path)!!.readText()
+    fun readTextResource(path: String): String =
+        (javaClass.getResource(path) ?: throw FileNotFoundException(path)).readText()
 }
diff --git a/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt b/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt
new file mode 100644
index 0000000..69f7871
--- /dev/null
+++ b/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt
@@ -0,0 +1,603 @@
+package examples.githubApi.controllers
+
+import examples.githubApi.models.BulkEntityDetails
+import examples.githubApi.models.Contributor
+import examples.githubApi.models.ContributorQueryResult
+import examples.githubApi.models.EventResults
+import examples.githubApi.models.Organisation
+import examples.githubApi.models.OrganisationQueryResult
+import examples.githubApi.models.PullRequest
+import examples.githubApi.models.PullRequestQueryResult
+import examples.githubApi.models.Repository
+import examples.githubApi.models.RepositoryQueryResult
+import examples.githubApi.models.StatusQueryParam
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.annotation.Body
+import io.micronaut.http.annotation.Consumes
+import io.micronaut.http.annotation.Controller
+import io.micronaut.http.annotation.Delete
+import io.micronaut.http.annotation.Get
+import io.micronaut.http.annotation.Header
+import io.micronaut.http.annotation.PathVariable
+import io.micronaut.http.annotation.Post
+import io.micronaut.http.annotation.Produces
+import io.micronaut.http.annotation.Put
+import io.micronaut.http.annotation.QueryValue
+import java.util.UUID
+import javax.validation.Valid
+import javax.validation.constraints.Max
+import javax.validation.constraints.Min
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.Unit
+import kotlin.collections.List
+
+@Controller
+interface InternalEventsController {
+    /**
+     * Generate change events for a list of entities
+     *
+     * @param bulkEntityDetails
+     */
+    @Post(uri = "/internal/events")
+    @Consumes(value = ["application/json"])
+    @Produces(value = ["application/json", "application/problem+json"])
+    fun post(
+        @Body @Valid
+        bulkEntityDetails: BulkEntityDetails
+    ): HttpResponse<EventResults>
+}
+
+@Controller
+interface ContributorsController {
+    /**
+     * Page through all the Contributor resources matching the query filters
+     *
+     * @param limit Upper bound for number of results to be returned from query api. A default limit
+     * will be applied if this is not present
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param includeInactive A query parameter to request both active and inactive entities
+     * @param cursor An encoded value which represents a point in the data from where pagination will
+     * commence. The pagination direction (either forward or backward) will also be encoded within the
+     * cursor value. The cursor value will always be generated server side
+     *
+     */
+    @Get(uri = "/contributors")
+    @Produces(value = ["application/json"])
+    fun searchContributors(
+        @Min(1) @Max(100) @QueryValue(value = "limit", defaultValue = "10")
+        limit: Int,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @QueryValue(value = "include_inactive") includeInactive: Boolean?,
+        @QueryValue(value = "cursor") cursor: String?
+    ): HttpResponse<ContributorQueryResult>
+
+    /**
+     * Create a new Contributor
+     *
+     * @param contributor
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Post(uri = "/contributors")
+    @Consumes(value = ["application/json"])
+    fun createContributor(
+        @Body @Valid
+        contributor: Contributor,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+
+    /**
+     * Get a Contributor by ID
+     *
+     * @param id The unique id for the resource
+     * @param status Changes the behavior of GET | HEAD based on the value of the status property.
+     * Will return a 404 if the status property does not match the query parameter."
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param ifNoneMatch The RFC7232 If-None-Match header field in a request requires the server to
+     * only operate on the resource if it does not match any of the provided entity-tags. If the provided
+     * entity-tag is `*`, it is required that the resource does not exist at all.
+     *
+     */
+    @Get(uri = "/contributors/{id}")
+    @Produces(value = ["application/json"])
+    fun getContributor(
+        @PathVariable(value = "id") id: String,
+        @QueryValue(value = "status", defaultValue = "all") status: StatusQueryParam,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "If-None-Match") ifNoneMatch: String?
+    ): HttpResponse<Contributor>
+
+    /**
+     * Update an existing Contributor
+     *
+     * @param contributor
+     * @param id The unique id for the resource
+     * @param ifMatch The RFC7232 If-Match header field in a request requires the server to only
+     * operate on the resource that matches at least one of the provided entity-tags. This allows clients
+     * express a precondition that prevent the method from being applied if there have been any changes
+     * to the resource.
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Put(uri = "/contributors/{id}")
+    @Consumes(value = ["application/json"])
+    fun putById(
+        @Body @Valid
+        contributor: Contributor,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "If-Match") ifMatch: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+}
+
+@Controller
+interface OrganisationsController {
+    /**
+     * Page through all the Organisation resources matching the query filters
+     *
+     * @param limit Upper bound for number of results to be returned from query api. A default limit
+     * will be applied if this is not present
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param includeInactive A query parameter to request both active and inactive entities
+     * @param cursor An encoded value which represents a point in the data from where pagination will
+     * commence. The pagination direction (either forward or backward) will also be encoded within the
+     * cursor value. The cursor value will always be generated server side
+     *
+     */
+    @Get(uri = "/organisations")
+    @Produces(value = ["application/json"])
+    fun get(
+        @Min(1) @Max(100) @QueryValue(value = "limit", defaultValue = "10")
+        limit: Int,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @QueryValue(value = "include_inactive") includeInactive: Boolean?,
+        @QueryValue(value = "cursor") cursor: String?
+    ): HttpResponse<OrganisationQueryResult>
+
+    /**
+     * Create a new Organisation
+     *
+     * @param organisation
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Post(uri = "/organisations")
+    @Consumes(value = ["application/json"])
+    fun post(
+        @Body @Valid
+        organisation: Organisation,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+
+    /**
+     * Get a Organisation by ID
+     *
+     * @param id The unique id for the resource
+     * @param status Changes the behavior of GET | HEAD based on the value of the status property.
+     * Will return a 404 if the status property does not match the query parameter."
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param ifNoneMatch The RFC7232 If-None-Match header field in a request requires the server to
+     * only operate on the resource if it does not match any of the provided entity-tags. If the provided
+     * entity-tag is `*`, it is required that the resource does not exist at all.
+     *
+     */
+    @Get(uri = "/organisations/{id}")
+    @Produces(value = ["application/json"])
+    fun getById(
+        @PathVariable(value = "id") id: String,
+        @QueryValue(value = "status", defaultValue = "all") status: StatusQueryParam,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "If-None-Match") ifNoneMatch: String?
+    ): HttpResponse<Organisation>
+
+    /**
+     * Update an existing Organisation
+     *
+     * @param organisation
+     * @param id The unique id for the resource
+     * @param ifMatch The RFC7232 If-Match header field in a request requires the server to only
+     * operate on the resource that matches at least one of the provided entity-tags. This allows clients
+     * express a precondition that prevent the method from being applied if there have been any changes
+     * to the resource.
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Put(uri = "/organisations/{id}")
+    @Consumes(value = ["application/json"])
+    fun putById(
+        @Body @Valid
+        organisation: Organisation,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "If-Match") ifMatch: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+}
+
+@Controller
+interface OrganisationsContributorsController {
+    /**
+     * Page through all the Contributor resources for this parent Organisation matching the query
+     * filters
+     *
+     * @param parentId The unique id for the parent resource
+     * @param limit Upper bound for number of results to be returned from query api. A default limit
+     * will be applied if this is not present
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param includeInactive A query parameter to request both active and inactive entities
+     * @param cursor An encoded value which represents a point in the data from where pagination will
+     * commence. The pagination direction (either forward or backward) will also be encoded within the
+     * cursor value. The cursor value will always be generated server side
+     *
+     */
+    @Get(uri = "/organisations/{parent-id}/contributors")
+    @Produces(value = ["application/json"])
+    fun get(
+        @PathVariable(value = "parent-id") parentId: String,
+        @Min(1) @Max(100) @QueryValue(value = "limit", defaultValue = "10")
+        limit: Int,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @QueryValue(value = "include_inactive") includeInactive: Boolean?,
+        @QueryValue(value = "cursor") cursor: String?
+    ): HttpResponse<ContributorQueryResult>
+
+    /**
+     * Get a Contributor for this Organisation by ID
+     *
+     * @param parentId The unique id for the parent resource
+     * @param id The unique id for the resource
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param ifNoneMatch The RFC7232 If-None-Match header field in a request requires the server to
+     * only operate on the resource if it does not match any of the provided entity-tags. If the provided
+     * entity-tag is `*`, it is required that the resource does not exist at all.
+     *
+     */
+    @Get(uri = "/organisations/{parent-id}/contributors/{id}")
+    @Produces(value = ["application/json"])
+    fun getById(
+        @PathVariable(value = "parent-id") parentId: String,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "If-None-Match") ifNoneMatch: String?
+    ): HttpResponse<Contributor>
+
+    /**
+     * Add an existing Contributor to this Organisation
+     *
+     * @param parentId The unique id for the parent resource
+     * @param id The unique id for the resource
+     * @param ifMatch The RFC7232 If-Match header field in a request requires the server to only
+     * operate on the resource that matches at least one of the provided entity-tags. This allows clients
+     * express a precondition that prevent the method from being applied if there have been any changes
+     * to the resource.
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Put(uri = "/organisations/{parent-id}/contributors/{id}")
+    fun putById(
+        @PathVariable(value = "parent-id") parentId: String,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "If-Match") ifMatch: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+
+    /**
+     * Remove Contributor from this Organisation. Does not delete the underlying Contributor.
+     *
+     * @param parentId The unique id for the parent resource
+     * @param id The unique id for the resource
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     */
+    @Delete(uri = "/organisations/{parent-id}/contributors/{id}")
+    fun deleteById(
+        @PathVariable(value = "parent-id") parentId: String,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?
+    ): HttpResponse<Unit>
+}
+
+@Controller
+interface RepositoriesController {
+    /**
+     * Page through all the Repository resources matching the query filters
+     *
+     * @param limit Upper bound for number of results to be returned from query api. A default limit
+     * will be applied if this is not present
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param slug Filters resources by the [slug] property. Accepts comma-delimited values
+     *
+     * @param name Filters resources by the [name] property. Accepts comma-delimited values
+     *
+     * @param includeInactive A query parameter to request both active and inactive entities
+     * @param cursor An encoded value which represents a point in the data from where pagination will
+     * commence. The pagination direction (either forward or backward) will also be encoded within the
+     * cursor value. The cursor value will always be generated server side
+     *
+     */
+    @Get(uri = "/repositories")
+    @Produces(value = ["application/json"])
+    fun get(
+        @Min(1) @Max(100) @QueryValue(value = "limit", defaultValue = "10")
+        limit: Int,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Valid @QueryValue(value = "slug")
+        slug: List<String>?,
+        @Valid @QueryValue(value = "name")
+        name: List<String>?,
+        @QueryValue(value = "include_inactive") includeInactive: Boolean?,
+        @QueryValue(value = "cursor") cursor: String?
+    ): HttpResponse<RepositoryQueryResult>
+
+    /**
+     * Create a new Repository
+     *
+     * @param repository
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Post(uri = "/repositories")
+    @Consumes(value = ["application/json"])
+    fun post(
+        @Body @Valid
+        repository: Repository,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+
+    /**
+     * Get a Repository by ID
+     *
+     * @param id The unique id for the resource
+     * @param status Changes the behavior of GET | HEAD based on the value of the status property.
+     * Will return a 404 if the status property does not match the query parameter."
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param ifNoneMatch The RFC7232 If-None-Match header field in a request requires the server to
+     * only operate on the resource if it does not match any of the provided entity-tags. If the provided
+     * entity-tag is `*`, it is required that the resource does not exist at all.
+     *
+     */
+    @Get(uri = "/repositories/{id}")
+    @Produces(value = ["application/json"])
+    fun getById(
+        @PathVariable(value = "id") id: String,
+        @QueryValue(value = "status", defaultValue = "all") status: StatusQueryParam,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "If-None-Match") ifNoneMatch: String?
+    ): HttpResponse<Repository>
+
+    /**
+     * Update an existing Repository
+     *
+     * @param repository
+     * @param id The unique id for the resource
+     * @param ifMatch The RFC7232 If-Match header field in a request requires the server to only
+     * operate on the resource that matches at least one of the provided entity-tags. This allows clients
+     * express a precondition that prevent the method from being applied if there have been any changes
+     * to the resource.
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Put(uri = "/repositories/{id}")
+    @Consumes(value = ["application/json"])
+    fun putById(
+        @Body @Valid
+        repository: Repository,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "If-Match") ifMatch: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+}
+
+@Controller
+interface RepositoriesPullRequestsController {
+    /**
+     * Page through all the PullRequest resources for this parent Repository matching the query
+     * filters
+     *
+     * @param parentId The unique id for the parent resource
+     * @param limit Upper bound for number of results to be returned from query api. A default limit
+     * will be applied if this is not present
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param includeInactive A query parameter to request both active and inactive entities
+     * @param cursor An encoded value which represents a point in the data from where pagination will
+     * commence. The pagination direction (either forward or backward) will also be encoded within the
+     * cursor value. The cursor value will always be generated server side
+     *
+     */
+    @Get(uri = "/repositories/{parent-id}/pull-requests")
+    @Produces(value = ["application/json"])
+    fun get(
+        @PathVariable(value = "parent-id") parentId: String,
+        @Min(1) @Max(100) @QueryValue(value = "limit", defaultValue = "10")
+        limit: Int,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @QueryValue(value = "include_inactive") includeInactive: Boolean?,
+        @QueryValue(value = "cursor") cursor: String?
+    ): HttpResponse<PullRequestQueryResult>
+
+    /**
+     * Create a new PullRequest for this parent Repository
+     *
+     * @param pullRequest
+     * @param parentId The unique id for the parent resource
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Post(uri = "/repositories/{parent-id}/pull-requests")
+    @Consumes(value = ["application/json"])
+    fun post(
+        @Body @Valid
+        pullRequest: PullRequest,
+        @PathVariable(value = "parent-id") parentId: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+
+    /**
+     * Get a PullRequest for this Repository by ID
+     *
+     * @param parentId The unique id for the parent resource
+     * @param id The unique id for the resource
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param ifNoneMatch The RFC7232 If-None-Match header field in a request requires the server to
+     * only operate on the resource if it does not match any of the provided entity-tags. If the provided
+     * entity-tag is `*`, it is required that the resource does not exist at all.
+     *
+     */
+    @Get(uri = "/repositories/{parent-id}/pull-requests/{id}")
+    @Produces(value = ["application/json"])
+    fun getById(
+        @PathVariable(value = "parent-id") parentId: String,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "If-None-Match") ifNoneMatch: String?
+    ): HttpResponse<PullRequest>
+
+    /**
+     * Update the PullRequest owned by this Repository
+     *
+     * @param pullRequest
+     * @param parentId The unique id for the parent resource
+     * @param id The unique id for the resource
+     * @param ifMatch The RFC7232 If-Match header field in a request requires the server to only
+     * operate on the resource that matches at least one of the provided entity-tags. This allows clients
+     * express a precondition that prevent the method from being applied if there have been any changes
+     * to the resource.
+     *
+     * @param xFlowId A custom header that will be passed onto any further requests and can be used
+     * for diagnosing.
+     *
+     * @param idempotencyKey This unique identifier can be used to allow for clients to safely retry
+     * requests without accidentally performing the same operation twice. This is useful in cases such as
+     * network failures. This Id should remain the same for a given operation, so that the server can use
+     * it to recognise subsequent retries of the same request. Clients should be careful in using this
+     * key as subsequent requests with the same key return the same result. How the key is generated is
+     * up to the client, but it is suggested to use UUID (v4), or any other random string with enough
+     * entropy to avoid collisions.
+     *
+     */
+    @Put(uri = "/repositories/{parent-id}/pull-requests/{id}")
+    @Consumes(value = ["application/json"])
+    fun putById(
+        @Body @Valid
+        pullRequest: PullRequest,
+        @PathVariable(value = "parent-id") parentId: String,
+        @PathVariable(value = "id") id: String,
+        @Header(value = "If-Match") ifMatch: String,
+        @Header(value = "X-Flow-Id") xFlowId: String?,
+        @Header(value = "Idempotency-Key") idempotencyKey: UUID?
+    ): HttpResponse<Unit>
+}
diff --git a/src/test/resources/examples/githubApi/controllers/Controllers.kt b/src/test/resources/examples/githubApi/controllers/spring/Controllers.kt
similarity index 100%
rename from src/test/resources/examples/githubApi/controllers/Controllers.kt
rename to src/test/resources/examples/githubApi/controllers/spring/Controllers.kt
diff --git a/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt b/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt
new file mode 100644
index 0000000..b9de8fc
--- /dev/null
+++ b/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt
@@ -0,0 +1,45 @@
+package examples.parameterNameClash.controllers
+
+import examples.parameterNameClash.models.SomeObject
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.annotation.Body
+import io.micronaut.http.annotation.Consumes
+import io.micronaut.http.annotation.Controller
+import io.micronaut.http.annotation.Get
+import io.micronaut.http.annotation.PathVariable
+import io.micronaut.http.annotation.Post
+import io.micronaut.http.annotation.QueryValue
+import javax.validation.Valid
+import kotlin.String
+import kotlin.Unit
+
+@Controller
+interface ExampleController {
+    /**
+     *
+     *
+     * @param pathB
+     * @param queryB
+     */
+    @Get(uri = "/example/{b}")
+    fun getById(
+        @PathVariable(value = "pathB") pathB: String,
+        @QueryValue(value = "queryB")
+        queryB: String
+    ): HttpResponse<Unit>
+
+    /**
+     *
+     *
+     * @param bodySomeObject example
+     * @param querySomeObject
+     */
+    @Post(uri = "/example")
+    @Consumes(value = ["application/json"])
+    fun post(
+        @Body @Valid
+        bodySomeObject: SomeObject,
+        @QueryValue(value = "querySomeObject")
+        querySomeObject: String
+    ): HttpResponse<Unit>
+}
diff --git a/src/test/resources/examples/parameterNameClash/controllers/Controllers.kt b/src/test/resources/examples/parameterNameClash/controllers/spring/Controllers.kt
similarity index 100%
rename from src/test/resources/examples/parameterNameClash/controllers/Controllers.kt
rename to src/test/resources/examples/parameterNameClash/controllers/spring/Controllers.kt
diff --git a/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt b/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt
new file mode 100644
index 0000000..cf52096
--- /dev/null
+++ b/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt
@@ -0,0 +1,21 @@
+package examples.pathLevelParameters.controllers
+
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.annotation.Controller
+import io.micronaut.http.annotation.Get
+import io.micronaut.http.annotation.QueryValue
+import kotlin.String
+import kotlin.Unit
+
+@Controller
+interface ExampleController {
+    /**
+     *
+     *
+     * @param a
+     * @param b
+     */
+    @Get(uri = "/example")
+    fun get(@QueryValue(value = "a") a: String, @QueryValue(value = "b") b: String):
+        HttpResponse<Unit>
+}
diff --git a/src/test/resources/examples/pathLevelParameters/controllers/Controllers.kt b/src/test/resources/examples/pathLevelParameters/controllers/spring/Controllers.kt
similarity index 100%
rename from src/test/resources/examples/pathLevelParameters/controllers/Controllers.kt
rename to src/test/resources/examples/pathLevelParameters/controllers/spring/Controllers.kt
diff --git a/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt b/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt
new file mode 100644
index 0000000..bdda948
--- /dev/null
+++ b/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt
@@ -0,0 +1,17 @@
+package examples.singleAllOf.controllers
+
+import examples.singleAllOf.models.Result
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.annotation.Controller
+import io.micronaut.http.annotation.Get
+import io.micronaut.http.annotation.Produces
+
+@Controller
+interface TestController {
+    /**
+     *
+     */
+    @Get(uri = "/test")
+    @Produces(value = ["application/json"])
+    fun test(): HttpResponse<Result>
+}