Skip to content

Commit 0d6a441

Browse files
committed
Support for using Future[T] or Promise[T] as response type in Rest Resources
1 parent 6a3743f commit 0d6a441

File tree

17 files changed

+722
-3
lines changed

17 files changed

+722
-3
lines changed

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ VERSIONS = [
6161
6262
dependencies {
6363
implementation "io.quarkiverse.scala:quarkus-scala3:${VERSIONS.QUARKUS_SCALA3}"
64+
implementation "io.quarkiverse.scala:quarkus-scala3-deployment:${VERSIONS.QUARKUS_SCALA3}"
6465
implementation("org.scala-lang:scala3-compiler_3") {
6566
version {
6667
strictly VERSIONS.SCALA3
@@ -133,7 +134,12 @@ In your `pom.xml` file, add:
133134
<dependency>
134135
<groupId>io.quarkiverse.scala</groupId>
135136
<artifactId>quarkus-scala3</artifactId>
136-
<version>0.0.1<version>
137+
<version>1.0.0<version>
138+
</dependency>
139+
<dependency>
140+
<groupId>io.quarkiverse.scala</groupId>
141+
<artifactId>quarkus-scala3-deployment</artifactId>
142+
<version>1.0.0</version>
137143
</dependency>
138144
```
139145

@@ -440,6 +446,54 @@ def mkRoutes(router: Router) =
440446
})
441447
```
442448

449+
450+
### `Future[T]` and `Promise[T]` support in Rest-Endpoints
451+
452+
This extension allows you to return `Future[T]` and `Promise[T]` from your rest-endpoints.
453+
454+
```scala
455+
456+
@Path("/")
457+
class GreetingResource
458+
459+
@GET
460+
@Path("/greet/future")
461+
@Produces(Array(TEXT_PLAIN))
462+
def futureGreeting(): Future[String] =
463+
Future.successful("Hello from the future")
464+
end futureGreeting
465+
466+
@GET
467+
@Path("/greet/promise")
468+
def promiseGreeting(): Promise[String] =
469+
Promise.successful("Hello from the promise")
470+
end promiseGreeting
471+
472+
end GreetingResource
473+
```
474+
475+
If the `Future[T]` or `Promise[T]` fails, the normal exception handling is invoked.
476+
477+
Make sure to have the following dependencies in your `pom.xml` to make it work:
478+
479+
```xml
480+
<dependency>
481+
<groupId>io.quarkus</groupId>
482+
<artifactId>quarkus-rest</artifactId>
483+
</dependency>
484+
<dependency>
485+
<groupId>io.quarkiverse.scala</groupId>
486+
<artifactId>quarkus-scala3</artifactId>
487+
<version>${project.version}</version>
488+
</dependency>
489+
<dependency>
490+
<groupId>io.quarkiverse.scala</groupId>
491+
<artifactId>quarkus-scala3-deployment</artifactId>
492+
<version>${project.version}</version>
493+
</dependency>
494+
```
495+
496+
443497
## Contributors ✨
444498

445499
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -452,6 +506,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
452506
<tr>
453507
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GavinRay97"><img src="https://avatars.githubusercontent.com/u/26604994?v=4?s=100" width="100px;" alt="Gavin Ray"/><br /><sub><b>Gavin Ray</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-scala3/commits?author=GavinRay97" title="Code">💻</a> <a href="#maintenance-GavinRay97" title="Maintenance">🚧</a></td>
454508
<td align="center" valign="top" width="14.28%"><a href="https://lesincroyableslivres.fr/"><img src="https://avatars.githubusercontent.com/u/1279749?v=4?s=100" width="100px;" alt="Guillaume Smet"/><br /><sub><b>Guillaume Smet</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-scala3/commits?author=gsmet" title="Code">💻</a></td>
509+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/domdorn"><img src="https://avatars.githubusercontent.com/u/100349?v=4?s=100" width="100px;" alt="Dominik Dorn"/><br /><sub><b>Dominik Dorn</b></sub></a><br /><a href="https://github.com/quarkiverse/quarkus-scala3/commits?author=domdorn" title="Code">🚀🍺💻</a></td>
455510
</tr>
456511
</tbody>
457512
</table>
@@ -462,3 +517,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
462517
<!-- ALL-CONTRIBUTORS-LIST:END -->
463518

464519
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
520+
521+
## TODOs
522+
- correctly generate OpenAPI Spec for methods returning `Future[T]` or `Promise[T]`, e.g. similar to [Quarkus #8499](https://github.com/quarkusio/quarkus/issues/8499)
523+
- Quarkus-Arc has special handling for `CompletionStage[T]`, maybe we should add similar handling for `Future[T]` and `Promise[T]`, see [ActiveRequestContextInterceptor](https://github.com/quarkusio/quarkus/blob/24d3e5262d20fdaa8c056d59f012f8c7b5b1c5c8/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ActivateRequestContextInterceptor.java) ?

futures/deployment/pom.xml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>io.quarkiverse.scala</groupId>
7+
<artifactId>quarkus-scala3-futures-parent</artifactId>
8+
<version>999-SNAPSHOT</version>
9+
</parent>
10+
<artifactId>quarkus-scala3-futures-deployment</artifactId>
11+
<name>Quarkus Scala3 futures - Deployment</name>
12+
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>io.quarkus</groupId>
17+
<artifactId>quarkus-arc-deployment</artifactId>
18+
</dependency>
19+
<dependency>
20+
<groupId>io.quarkiverse.scala</groupId>
21+
<artifactId>quarkus-scala3-futures</artifactId>
22+
<version>${project.version}</version>
23+
</dependency>
24+
<dependency>
25+
<groupId>io.quarkus</groupId>
26+
<artifactId>quarkus-junit5-internal</artifactId>
27+
<scope>test</scope>
28+
</dependency>
29+
30+
<dependency>
31+
<groupId>io.quarkus</groupId>
32+
<artifactId>quarkus-rest-spi-deployment</artifactId>
33+
</dependency>
34+
<dependency>
35+
<groupId>io.quarkus.resteasy.reactive</groupId>
36+
<artifactId>resteasy-reactive-processor</artifactId>
37+
</dependency>
38+
<dependency>
39+
<groupId>io.quarkus</groupId>
40+
<artifactId>quarkus-rest-server-spi-deployment</artifactId>
41+
</dependency>
42+
43+
<dependency>
44+
<groupId>org.scala-lang</groupId>
45+
<artifactId>scala3-library_3</artifactId>
46+
<scope>compile</scope>
47+
</dependency>
48+
49+
</dependencies>
50+
51+
<build>
52+
<sourceDirectory>src/main/scala</sourceDirectory>
53+
<testSourceDirectory>src/test/scala</testSourceDirectory>
54+
<plugins>
55+
<plugin>
56+
<artifactId>maven-compiler-plugin</artifactId>
57+
<configuration>
58+
<annotationProcessorPaths>
59+
<path>
60+
<groupId>io.quarkus</groupId>
61+
<artifactId>quarkus-extension-processor</artifactId>
62+
<version>${quarkus.version}</version>
63+
</path>
64+
</annotationProcessorPaths>
65+
</configuration>
66+
</plugin>
67+
<plugin>
68+
<groupId>net.alchim31.maven</groupId>
69+
<artifactId>scala-maven-plugin</artifactId>
70+
</plugin>
71+
</plugins>
72+
</build>
73+
</project>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.quarkiverse.scala.scala3.deployment;
2+
3+
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
4+
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
5+
6+
public class Scala3FutureResponseHandler implements ServerRestHandler {
7+
8+
private void handleFuture(ResteasyReactiveRequestContext requestContext,
9+
scala.concurrent.Future<?> f) {
10+
11+
requestContext.suspend();
12+
13+
f.onComplete(tryValue -> {
14+
if (tryValue.isSuccess()) {
15+
requestContext.setResult(tryValue.get());
16+
} else {
17+
requestContext.handleException(tryValue.failed().get(), true);
18+
}
19+
requestContext.resume();
20+
return null;
21+
}, scala.concurrent.ExecutionContext.global());
22+
23+
}
24+
25+
@Override
26+
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
27+
if (requestContext.getResult() instanceof scala.concurrent.Future<?> future) {
28+
handleFuture(requestContext, future);
29+
} else if (requestContext.getResult() instanceof scala.concurrent.Promise<?> promise) {
30+
handleFuture(requestContext, promise.future());
31+
}
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.quarkiverse.scala.scala3.deployment;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
import org.jboss.jandex.ClassInfo;
8+
import org.jboss.jandex.DotName;
9+
import org.jboss.jandex.MethodInfo;
10+
import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer;
11+
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
12+
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer.Phase;
13+
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;
14+
15+
public class Scala3FutureReturnTypeMethodScanner implements MethodScanner {
16+
private static final DotName FUTURE = DotName.createSimple("scala.concurrent.Future");
17+
private static final DotName PROMISE = DotName.createSimple("scala.concurrent.Promise");
18+
private static final DotName BLOCKING_ANNOTATION = DotName.createSimple("io.smallrye.common.annotation.Blocking");
19+
20+
private void ensureNotBlocking(MethodInfo method) {
21+
if (method.annotation(BLOCKING_ANNOTATION) != null) {
22+
String format = String.format("Suspendable @Blocking methods are not supported yet: %s.%s",
23+
method.declaringClass().name(), method.name());
24+
throw new IllegalStateException(format);
25+
}
26+
}
27+
28+
public Scala3FutureReturnTypeMethodScanner() {
29+
}
30+
31+
@Override
32+
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
33+
Map<String, Object> methodContext) {
34+
35+
if (isMethodSignatureAsync(method)) {
36+
ensureNotBlocking(method);
37+
38+
return Collections.singletonList(new FixedHandlerChainCustomizer(
39+
new Scala3FutureResponseHandler(), Phase.AFTER_METHOD_INVOKE));
40+
}
41+
return Collections.emptyList();
42+
}
43+
44+
@Override
45+
public boolean isMethodSignatureAsync(MethodInfo info) {
46+
DotName name = info.returnType().name();
47+
return name.equals(FUTURE) || name.equals(PROMISE);
48+
}
49+
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.quarkiverse.scala.scala3.futures.deployment;
2+
3+
import io.quarkiverse.scala.scala3.deployment.Scala3FutureReturnTypeMethodScanner;
4+
import io.quarkus.deployment.annotations.BuildStep;
5+
import io.quarkus.deployment.builditem.FeatureBuildItem;
6+
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
7+
8+
public class Scala3FuturesJavaProcessor {
9+
10+
@BuildStep
11+
public FeatureBuildItem feature() {
12+
return new FeatureBuildItem("scala3-futures");
13+
}
14+
15+
@BuildStep
16+
public MethodScannerBuildItem registerFuturesRestReturnTypes() {
17+
return new MethodScannerBuildItem(new Scala3FutureReturnTypeMethodScanner());
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkiverse.scala.scala3.futures.test
2+
3+
import org.jboss.shrinkwrap.api.ShrinkWrap;
4+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.quarkus.test.QuarkusDevModeTest;
10+
11+
class Scala3FuturesDevModeTest {
12+
13+
// Start hot reload (DevMode) test with your extension loaded
14+
@RegisterExtension
15+
val devModeTest: QuarkusDevModeTest = new QuarkusDevModeTest()
16+
.setArchiveProducer(() => ShrinkWrap.create(classOf[JavaArchive]))
17+
18+
@Test
19+
def writeYourOwnDevModeTest(): Unit = {
20+
// Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information
21+
Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName());
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkiverse.scala.scala3.futures.test
2+
3+
import org.jboss.shrinkwrap.api.ShrinkWrap;
4+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.quarkus.test.QuarkusUnitTest;
10+
11+
class Scala3FuturesTest {
12+
13+
// Start unit test with your extension loaded
14+
@RegisterExtension
15+
def unitTest: QuarkusUnitTest = new QuarkusUnitTest()
16+
.setArchiveProducer(() => ShrinkWrap.create(classOf[JavaArchive]))
17+
18+
@Test
19+
def writeYourOwnUnitTest(): Unit = {
20+
// Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information
21+
Assertions.assertTrue(true, "Add some assertions to " + getClass().getName());
22+
}
23+
}

0 commit comments

Comments
 (0)