diff --git a/README.md b/README.md index a4061b78..06616fd4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This library was built to take advantage of the complex modeling features availa - GraalVM Native Reflection Registration - Json Merge Patch (via `JsonNullable`) (add `x-json-merge-patch: true` to schemas) - Override Jackson Include NonNull (via `JsonInclude`) (add `x-jackson-include-non-null: true` to schemas) + - Generate CompletionStage wrapped controllers (add `x-async-support: true` to path) More than just bootstrapping, this library can be permanently integrated into a gradle or maven build and will ensure contract and code always match, even as APIs evolve in complexity. 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 513e961a..93003061 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/SpringControllerInterfaceGenerator.kt @@ -39,6 +39,7 @@ class SpringControllerInterfaceGenerator( private val options: Set = emptySet(), ) : ControllerInterfaceGenerator, AnnotationBasedControllerInterfaceGenerator(packages, api, validationAnnotations) { + private val EXTENSION_ASYNC_SUPPORT = "x-async-support" private val addAuthenticationParameter: Boolean get() = options.any { it == ControllerCodeGenOptionType.AUTHENTICATION } @@ -78,7 +79,10 @@ class SpringControllerInterfaceGenerator( .addSpringFunAnnotation(op, verb, path.pathString) .addSuspendModifier() - val funcSpec = if (options.contains(ControllerCodeGenOptionType.COMPLETION_STAGE)) { + val explicitAsyncSupport = op.extensions[EXTENSION_ASYNC_SUPPORT] as? Boolean + val asyncSupport = explicitAsyncSupport ?: options.contains(ControllerCodeGenOptionType.COMPLETION_STAGE) + + val funcSpec = if (asyncSupport) { baseFunSpec.returns( SpringImports.COMPLETION_STAGE.parameterizedBy( SpringImports.RESPONSE_ENTITY.parameterizedBy(returnType) @@ -88,7 +92,7 @@ class SpringControllerInterfaceGenerator( baseFunSpec.returns(SpringImports.RESPONSE_ENTITY.parameterizedBy(returnType)) } - parameters + parameters .map { when (it) { is BodyParameter -> @@ -97,6 +101,7 @@ class SpringControllerInterfaceGenerator( .addAnnotation(SpringAnnotations.requestBodyBuilder().build()) .maybeAddAnnotation(validationAnnotations.parameterValid()) .build() + is RequestParameter -> it .toParameterSpecBuilder() diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt index 8929501f..db3fb460 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/SpringControllerGeneratorTest.kt @@ -152,7 +152,8 @@ class SpringControllerGeneratorTest { @Test fun `ensure that subresource specific controllers are created`() { val api = SourceApi(readTextResource("/examples/githubApi/api.yaml")) - val controllers = SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate() + val controllers = + SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate() assertThat(controllers.files).size().isEqualTo(6) assertThat(controllers.files.map { it.name }).containsAll( @@ -225,7 +226,9 @@ class SpringControllerGeneratorTest { @Test fun `controller parameters should have spring DateTimeFormat annotations`() { val api = SourceApi(readTextResource("/examples/springFormatDateAndDateTime/api.yaml")) - val controllers = SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate().toSingleFile() + val controllers = + SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate() + .toSingleFile() val expectedControllers = readTextResource("/examples/springFormatDateAndDateTime/controllers/Controllers.kt") assertThat(controllers.trim()).isEqualTo(expectedControllers.trim()) @@ -234,7 +237,9 @@ class SpringControllerGeneratorTest { @Test fun `ensure generates ByteArray body parameter and response for string with format binary`() { val api = SourceApi(readTextResource("/examples/binary/api.yaml")) - val controllers = SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate().toSingleFile() + val controllers = + SpringControllerInterfaceGenerator(Packages(basePackage), api, JavaxValidationAnnotations).generate() + .toSingleFile() val expectedControllers = readTextResource("/examples/binary/controllers/spring/Controllers.kt") assertThat(controllers.trim()).isEqualTo(expectedControllers.trim()) @@ -244,7 +249,25 @@ class SpringControllerGeneratorTest { fun `controller functions are wrapped by CompletionStage`() { val basePackage = "examples.completionStage" val api = SourceApi(readTextResource("/examples/githubApi/api.yaml")) - val expectedControllers = readTextResource("/examples/githubApi/controllers/spring-completion-stage/Controllers.kt") + val expectedControllers = + readTextResource("/examples/githubApi/controllers/spring-completion-stage/Controllers.kt") + + val controllers = SpringControllerInterfaceGenerator( + Packages(basePackage), + api, + JavaxValidationAnnotations, + setOf(ControllerCodeGenOptionType.COMPLETION_STAGE), + ).generate().toSingleFile() + + assertThat(controllers).isEqualTo(expectedControllers) + } + + @Test + fun `controller functions with x-async-support=false extension are NOT wrapped by CompletionStage`() { + val basePackage = "examples.completionStage" + val api = SourceApi(readTextResource("/examples/githubApi/api.yaml")) + val expectedControllers = + readTextResource("/examples/githubApi/controllers/spring-completion-stage/Controllers.kt") val controllers = SpringControllerInterfaceGenerator( Packages(basePackage), diff --git a/src/test/resources/examples/githubApi/api.yaml b/src/test/resources/examples/githubApi/api.yaml index c910cd0f..69096332 100644 --- a/src/test/resources/examples/githubApi/api.yaml +++ b/src/test/resources/examples/githubApi/api.yaml @@ -15,6 +15,7 @@ paths: post: tags: - events + x-async-support: false summary: Generate change events for a list of entities requestBody: $ref: '#/components/requestBodies/BulkEventGenerationBody' diff --git a/src/test/resources/examples/githubApi/controllers/spring-completion-stage/Controllers.kt b/src/test/resources/examples/githubApi/controllers/spring-completion-stage/Controllers.kt index 088cb9ac..51581525 100644 --- a/src/test/resources/examples/githubApi/controllers/spring-completion-stage/Controllers.kt +++ b/src/test/resources/examples/githubApi/controllers/spring-completion-stage/Controllers.kt @@ -46,7 +46,7 @@ public interface InternalEventsController { method = [RequestMethod.POST], consumes = ["application/json"], ) - public fun post(@RequestBody @Valid bulkEntityDetails: BulkEntityDetails): CompletionStage> + public fun post(@RequestBody @Valid bulkEntityDetails: BulkEntityDetails): ResponseEntity } @Controller