From cb58732a9aa75c7ac57d4ea60afd65fcab37bb80 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 11 Aug 2021 20:25:38 -0400 Subject: [PATCH] Support `kotlin.coroutines.Continuation` default response type --- .../api/constants/KotlinConstants.java | 17 +++++++++ .../scanner/spi/AnnotationScanner.java | 36 +++++++++++++++++- extension-jaxrs/pom.xml | 8 +++- .../runtime/scanner/ApiResponseTests.java | 38 +++++++++++++++++++ .../responses.kotlin-continuation.json | 37 ++++++++++++++++++ 5 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/io/smallrye/openapi/api/constants/KotlinConstants.java create mode 100644 extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/responses.kotlin-continuation.json diff --git a/core/src/main/java/io/smallrye/openapi/api/constants/KotlinConstants.java b/core/src/main/java/io/smallrye/openapi/api/constants/KotlinConstants.java new file mode 100644 index 000000000..4ee8ff07a --- /dev/null +++ b/core/src/main/java/io/smallrye/openapi/api/constants/KotlinConstants.java @@ -0,0 +1,17 @@ +package io.smallrye.openapi.api.constants; + +import org.jboss.jandex.DotName; + +/** + * Constants related to the Kotlin language + * + * @author Michael Edgar {@literal } + */ +public class KotlinConstants { + + public static final DotName CONTINUATION = DotName + .createSimple("kotlin.coroutines.Continuation"); + + private KotlinConstants() { + } +} diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java index 6124e4b3d..15e68aa1e 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java @@ -36,8 +36,10 @@ import org.jboss.jandex.MethodInfo; import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; import io.smallrye.openapi.api.OpenApiConfig.OperationIdStrategy; +import io.smallrye.openapi.api.constants.KotlinConstants; import io.smallrye.openapi.api.constants.OpenApiConstants; import io.smallrye.openapi.api.constants.SecurityConstants; import io.smallrye.openapi.api.models.OperationImpl; @@ -410,8 +412,9 @@ default void createResponseFromRestMethod(final AnnotationScannerContext context Schema schema; if (isMultipartOutput(returnType)) { - schema = new SchemaImpl(); - schema.setType(Schema.SchemaType.OBJECT); + schema = new SchemaImpl().type(Schema.SchemaType.OBJECT); + } else if (isKotlinContinuation(method)) { + schema = kotlinContinuationToSchema(context, method); } else { schema = SchemaFactory.typeToSchema(context, returnType, context.getExtensions()); } @@ -492,6 +495,35 @@ default boolean isVoidResponse(final MethodInfo method) { return false; } + default boolean isKotlinContinuation(MethodInfo method) { + if (method.parameters().size() != 1) { + return false; + } + + return KotlinConstants.CONTINUATION.equals(method.parameters().get(0).name()); + } + + default Schema kotlinContinuationToSchema(AnnotationScannerContext context, MethodInfo method) { + Schema schema; + Type type = context.getResourceTypeResolver().resolve(method.parameters().get(0)); + + if (type.kind() == Kind.PARAMETERIZED_TYPE) { + type = type.asParameterizedType().arguments().get(0); + + if (type.kind() == Kind.WILDCARD_TYPE) { + Type extendsBound = type.asWildcardType().extendsBound(); + Type superBound = type.asWildcardType().superBound(); + type = superBound != null ? superBound : extendsBound; + } + + schema = SchemaFactory.typeToSchema(context, type, context.getExtensions()); + } else { + schema = new SchemaImpl().type(Schema.SchemaType.OBJECT); + } + + return schema; + } + /** * Determine if the default response information should be generated. * It should be done when no responses have been declared or if the default diff --git a/extension-jaxrs/pom.xml b/extension-jaxrs/pom.xml index 92702cba8..759a171bd 100644 --- a/extension-jaxrs/pom.xml +++ b/extension-jaxrs/pom.xml @@ -88,7 +88,13 @@ test pom - + + io.quarkus + quarkus-kotlin + ${version.quarkus} + test + + ${project.groupId} diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ApiResponseTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ApiResponseTests.java index e379c427c..a2567bf59 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ApiResponseTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ApiResponseTests.java @@ -237,4 +237,42 @@ public jakarta.ws.rs.core.MultivaluedMap> ge NavigableMap.class, HashMap.class); } + + @Test + void testKotlinContinuationStringResponse() throws IOException, JSONException { + @jakarta.ws.rs.Path("/hello") + class Resource { + + /** + * Java equivalent of Kotlin function + * + * suspend fun hello(): String { + * return "Hello RESTEasy Reactive" + * } + * + * + * @param completion maps to Kotlin string return type when using `suspend` + * @return nothing + */ + @jakarta.ws.rs.GET + @jakarta.ws.rs.Path("/async") + @jakarta.ws.rs.Produces({ "text/plain" }) + public Object hello1(kotlin.coroutines.Continuation completion) { + return null; + } + + @jakarta.ws.rs.GET + @jakarta.ws.rs.Path("/sync") + @jakarta.ws.rs.Produces({ "text/plain" }) + public String hello2() { + return "Hello"; + } + } + + test("responses.kotlin-continuation.json", + Resource.class, + kotlin.coroutines.Continuation.class, + kotlin.coroutines.CoroutineContext.class); + } + } diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/responses.kotlin-continuation.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/responses.kotlin-continuation.json new file mode 100644 index 000000000..ef1283b6b --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/responses.kotlin-continuation.json @@ -0,0 +1,37 @@ +{ + "openapi": "3.0.3", + "paths": { + "/hello/async": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/hello/sync": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +}