Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Configuring Scala Jackson and the addon-on "Enum" module for JSON support](#configuring-scala-jackson-and-the-addon-on-enum-module-for-json-support)
- [Scala DSL for rest-assured (similar to Kotlin DSL)](#scala-dsl-for-rest-assured-similar-to-kotlin-dsl)
- [Functional HTTP routes (Vert.x handlers)](#functional-http-routes-vertx-handlers)
- [Quarkus - Scala3 - ZIO](#quarkus---scala3---zio)

## Introduction

Expand Down Expand Up @@ -462,3 +463,61 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

# Quarkus - Scala3 - ZIO
Add these dependencies to your `pom.xml` (respectively gradle):

```xml
<dependency>
<groupId>io.quarkiverse.scala</groupId>
<artifactId>quarkus-scala3-zio</artifactId>
<version>999-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.scala</groupId>
<artifactId>quarkus-scala3-zio-deployment</artifactId>
<version>999-SNAPSHOT</version>
</dependency>
```

Now you're able to use a `ZIO[Any, E <: Throwable, A]` in your REST-Resources, e.g.

```scala
final case class AsyncGreetingResponse(message: String, ip: String, time: Long)

@GET
@Path("/greet/async")
@Produces(Array(APPLICATION_JSON))
def asyncGreeting(): Task[AsyncGreetingResponse] =
val numsAmount = 10
Log.debug(s"Generating $numsAmount numbers asynchronously...")
val startTime = System.currentTimeMillis()
// Get the IP address asynchronously
val IPFuture = ZIO.fromFuture(_ => getOwnIP().map(_.body).recover:
case e: Exception =>
Log.error("Failed to get the IP address.")
Left("Failed to get IP"))

val futureSum = ZIO.foreachPar((1 to numsAmount)){i => generateNum()}.map(_.sum)
for
sumF <- futureSum.fork
ipF <- IPFuture.map(_.merge).fork
result <- sumF.join zip ipF.join
(sum, ip) = result
yield
val endTime = System.currentTimeMillis() - startTime
Log.debug(s"My IP is: $ip")
Log.debug(s"Generated $numsAmount numbers asynchronously in ${endTime}ms")

AsyncGreetingResponse(
s"The sum of the $numsAmount generated numbers is $sum. Was generated asynchronously in ${endTime}ms.\nYour IP is: $ip.",
ip,
endTime)

end asyncGreeting

```

Please note that we currently don't support anything in the Environment `R`, as we don't have a way
to transfer this information from Java to Scala. Also, your error type needs to be either Nothing
or a (subtype of) Throwable.
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>io.quarkiverse</groupId>
<artifactId>quarkiverse-parent</artifactId>
<version>15</version>
<version>16</version>
</parent>
<groupId>io.quarkiverse.scala</groupId>
<artifactId>quarkus-scala3-parent</artifactId>
Expand All @@ -14,6 +14,7 @@
<modules>
<module>deployment</module>
<module>runtime</module>
<module>zio</module>
</modules>
<scm>
<connection>:git:[email protected]:quarkiverse/quarkus-scala3.git</connection>
Expand Down
10 changes: 10 additions & 0 deletions zio/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# ZIO TODOs / IDEAs

- ZIO-Config: Create implementation of ZIO ConfigProvider that is backed by the microprofile Config and
inject that into the environment / make it usable with ZIO.Config(..) [See ZIO-Configuration](https://zio.dev/reference/configuration/)
- ZIO-Logging: Bridge ZIO-Logging to Quarkus-Logger?
- Fill the environment with commonly required dependencies.
- Fill the environment with dependencies specified in the `R` type (we need to figure out how to parse that, as we loose type info in Java)
- Allow using `ZStream` where `Multi` is allowed atm (e.g. Kafka, SSE, etc.)
- Register the ZIO Runtime using a Provider, so it could be injected.
- ...
78 changes: 78 additions & 0 deletions zio/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.quarkiverse.scala</groupId>
<artifactId>quarkus-scala3-zio-parent</artifactId>
<version>999-SNAPSHOT</version>
</parent>
<artifactId>quarkus-scala3-zio-deployment</artifactId>
<name>Quarkus Scala3 Zio - Deployment</name>


<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.scala</groupId>
<artifactId>quarkus-scala3-zio</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-spi-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.resteasy.reactive</groupId>
<artifactId>resteasy-reactive-processor</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-server-spi-deployment</artifactId>
</dependency>

<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala3-library_3</artifactId>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>dev.zio</groupId>
<artifactId>zio_3</artifactId>
</dependency>

</dependencies>

<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkiverse.scala.scala3.zio.deployment;

import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;

public class Scala3ZIOJavaProcessor {

@BuildStep
public FeatureBuildItem feature() {
return new FeatureBuildItem("scala3-zio");
}

@BuildStep
public MethodScannerBuildItem registerZIORestReturnTypes() {
return new MethodScannerBuildItem(new Scala3ZIOReturnTypeMethodScanner());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkiverse.scala.scala3.zio.deployment

import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler
import zio.ZIO

import scala.jdk.CollectionConverters.*

class Scala3ZIOResponseHandler() extends ServerRestHandler {

override def handle(requestContext: ResteasyReactiveRequestContext): Unit = {
val result = requestContext.getResult

/*
TODO if we're able to read the environment from the effect, we might be able to hook into
Quarkus dependency injection mechanism to fill it here. For now, we can only assume its any.
*/
type R = Any

/* fixing the error type to Throwable. We can be sure its this type, as we've checked
it before in io.quarkiverse.scala.scala3.zio.deployment.Scala3ZIOReturnTypeMethodScanner.scan
There it can only be Nothing, or Throwable or subtypes of Throwable, so either way, we're
safe to assume it's Throwable here.
*/
type E = Throwable

/* We assume any as return type, as quarkus also accepts any object as return type.
*/
type A = Any

requestContext.suspend()
val r = result.asInstanceOf[ZIO[R, E, A]]

val r1 = r.fold(e => {
requestContext.handleException(e)
requestContext.resume()
}, a => {
requestContext.setResult(a)
requestContext.resume()
})

zio.Unsafe.unsafe(u => zio.Runtime.default.unsafe.runToFuture(r1)(zio.Trace.empty, u))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkiverse.scala.scala3.zio.deployment

import org.jboss.jandex.ClassInfo
import org.jboss.jandex.DotName
import org.jboss.jandex.MethodInfo
import org.jboss.jandex.Type
import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner

import java.util
import java.util.List as JList
import java.util.Collections as JCollections

class Scala3ZIOReturnTypeMethodScanner extends MethodScanner {
private val ZIO = DotName.createSimple("zio.ZIO")
private val nothing$ = DotName.createSimple("scala.Nothing$")
private val throwable = DotName.createSimple("java.lang.Throwable")


override def scan(method: MethodInfo,
actualEndpointClass: ClassInfo,
methodContext: util.Map[String, AnyRef]
): JList[HandlerChainCustomizer] = {
if(isMethodSignatureAsync(method)) {
ensuringFailureTypeIsNothingOrAThrowable(method)
JCollections.singletonList(
new FixedHandlerChainCustomizer(
new Scala3ZIOResponseHandler(),
HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE
)
)
} else {
JCollections.emptyList()
}
}

private def ensuringFailureTypeIsNothingOrAThrowable(info: MethodInfo): Unit = {
import scala.jdk.CollectionConverters._
val returnType = info.returnType()
val typeArguments: JList[Type] = returnType.asParameterizedType().arguments()
if (typeArguments.size() != 3) {
throw new RuntimeException("ZIO must have three type arguments")
}
val errorType = typeArguments.get(1)

if !(errorType.name() == nothing$) && !(errorType.name() == throwable) then
val realClazz = Class.forName(errorType.name().toString(), false, Thread.currentThread().getContextClassLoader)
if (!classOf[Throwable].isAssignableFrom(realClazz)) {
val returnType = info.returnType().toString.replaceAll("<","[").replaceAll(">","]")
val parameters = info.parameters().asScala.map(v => s"${v.name()}:${v.`type`().toString}").mkString(",")
val signature = s"${info.name()}(${parameters}):${returnType}"

throw new RuntimeException(s"The error type of def ${signature} in ${info.declaringClass()} needs to be either Nothing, a Throwable or subclass of Throwable")
}
}


override def isMethodSignatureAsync(info: MethodInfo): Boolean = {
info.returnType().name() == ZIO
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkiverse.scala.scala3.zio.test;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusDevModeTest;

class Scala3ZioDevModeTest {

// Start hot reload (DevMode) test with your extension loaded
@RegisterExtension
val devModeTest: QuarkusDevModeTest = new QuarkusDevModeTest()
.setArchiveProducer(() => ShrinkWrap.create(classOf[JavaArchive]))

@Test
def writeYourOwnDevModeTest(): Unit = {
// Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information
Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkiverse.scala.scala3.zio.test;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

class Scala3ZioTest {

// Start unit test with your extension loaded
@RegisterExtension
def unitTest: QuarkusUnitTest = new QuarkusUnitTest()
.setArchiveProducer(() => ShrinkWrap.create(classOf[JavaArchive]))

@Test
def writeYourOwnUnitTest(): Unit = {
// Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information
Assertions.assertTrue(true, "Add some assertions to " + getClass().getName());
}
}
Loading