From 992c22e1572d7f1108930c6d36abb9ebb044767a Mon Sep 17 00:00:00 2001 From: Dominik Dorn Date: Fri, 10 May 2024 00:55:47 +0200 Subject: [PATCH 1/4] zio WIP --- pom.xml | 3 +- zio/deployment/pom.xml | 78 +++++++++++ .../deployment/Scala3ZIOResponseHandler.scala | 51 +++++++ .../Scala3ZIOReturnTypeMethodScanner.scala | 56 ++++++++ .../deployment/Scala3ZioJavaProcessor.java | 18 +++ .../zio/test/Scala3ZioDevModeTest.scala | 23 +++ .../scala/scala3/zio/test/Scala3ZioTest.scala | 23 +++ zio/integration-tests/pom.xml | 131 ++++++++++++++++++ .../src/main/resources/application.properties | 0 .../scala3/zio/it/Scala3ZioResource.scala | 28 ++++ .../scala/scala3/zio/it/Given.scala | 27 ++++ .../scala3/zio/it/Scala3ZioResourceIT.scala | 7 + .../scala3/zio/it/Scala3ZioResourceTest.scala | 36 +++++ zio/pom.xml | 124 +++++++++++++++++ zio/runtime/pom.xml | 52 +++++++ .../resources/META-INF/quarkus-extension.yaml | 11 ++ 16 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 zio/deployment/pom.xml create mode 100644 zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala create mode 100644 zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala create mode 100644 zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java create mode 100644 zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioDevModeTest.scala create mode 100644 zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioTest.scala create mode 100644 zio/integration-tests/pom.xml create mode 100644 zio/integration-tests/src/main/resources/application.properties create mode 100644 zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala create mode 100644 zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Given.scala create mode 100644 zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceIT.scala create mode 100644 zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala create mode 100644 zio/pom.xml create mode 100644 zio/runtime/pom.xml create mode 100644 zio/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/pom.xml b/pom.xml index b80da21..69c1197 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.quarkiverse quarkiverse-parent - 15 + 16 io.quarkiverse.scala quarkus-scala3-parent @@ -14,6 +14,7 @@ deployment runtime + zio :git:git@github.com:quarkiverse/quarkus-scala3.git diff --git a/zio/deployment/pom.xml b/zio/deployment/pom.xml new file mode 100644 index 0000000..248a68d --- /dev/null +++ b/zio/deployment/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + io.quarkiverse.scala + quarkus-scala3-zio-parent + 999-SNAPSHOT + + quarkus-scala3-zio-deployment + Quarkus Scala3 Zio - Deployment + + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkiverse.scala + quarkus-scala3-zio + ${project.version} + + + io.quarkus + quarkus-junit5-internal + test + + + + io.quarkus + quarkus-rest-spi-deployment + + + io.quarkus.resteasy.reactive + resteasy-reactive-processor + + + io.quarkus + quarkus-rest-server-spi-deployment + + + + org.scala-lang + scala3-library_3 + compile + + + + dev.zio + zio_3 + + + + + + src/main/scala + src/test/scala + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + net.alchim31.maven + scala-maven-plugin + + + + diff --git a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala new file mode 100644 index 0000000..a68a6a6 --- /dev/null +++ b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala @@ -0,0 +1,51 @@ +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 java.lang.reflect.ParameterizedType +import scala.jdk.CollectionConverters.* +import scala.util.Failure +import scala.util.Success + +class Scala3ZIOResponseHandler() extends ServerRestHandler { + + override def handle(requestContext: ResteasyReactiveRequestContext): Unit = { + val result = requestContext.getResult + + type R = Any + type E = Throwable + type A = Any + /* + // TODO at the moment, we're just stupidly assume, the effect has no environment + // and the error type is a throwable. We need to figure out a way on how to access + // the type arguments of the ZIO effect in a more structured way. + // At the moment, a Environment of e.g. String with Int will be read as java.lang.Object, + // so we loose the type information which could be used for dependency injection + println("in handle of Scala3ZIOResponseHandler") + + val p = requestContext.getGenericReturnType.asInstanceOf[ParameterizedType] + val typeArgs = p.getActualTypeArguments.toSeq + val (environment, errorType, successType) = (typeArgs(0), typeArgs(1), typeArgs(2)) + + println(s"environment: $environment") + println(s"errorType: $errorType") + println(s"successType: $successType") + */ + result match + case r: ZIO[R, E, A] => + requestContext.suspend() + val f = zio.Unsafe.unsafe(u => zio.Runtime.default.unsafe.runToFuture(r)(zio.Trace.empty, u)) + f.onComplete { + case Success(value) => + requestContext.setResult(value) + requestContext.resume() + case Failure(exception) => + requestContext.handleException(exception, true) + requestContext.resume() + }(scala.concurrent.ExecutionContext.global) + + case _ => () + } +} diff --git a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala new file mode 100644 index 0000000..0db1c3e --- /dev/null +++ b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala @@ -0,0 +1,56 @@ +package io.quarkiverse.scala.scala3.zio.deployment + +import org.jboss.jandex.AnnotationInstance +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.core.parameters.ParameterExtractor +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 zio.RIO + +import java.util +import java.util.List as JList +import java.util.Collections as JCollections + +class Scala3ZIOReturnTypeMethodScanner extends MethodScanner { + val ZIO: DotName = DotName.createSimple("zio.ZIO") +// val TASK: DotName = DotName.createSimple("zio.Task") +// val UIO: DotName = DotName.createSimple("zio.UIO") +// val RIO: DotName = DotName.createSimple("zio.RIO") + + + + + override def scan(method: MethodInfo, + actualEndpointClass: ClassInfo, + methodContext: util.Map[String, AnyRef] + ): JList[HandlerChainCustomizer] = { + println("in Scala3ZIOReturnTypeMethodScanner scan") + if(isMethodSignatureAsync(method)) { + JCollections.singletonList( + new FixedHandlerChainCustomizer( + new Scala3ZIOResponseHandler(), + HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE + ) + ) + } else { + JCollections.emptyList() + } + } + + + + override def isMethodSignatureAsync(info: MethodInfo): Boolean = { + val name = info.returnType().name() + val isCorrect = name == ZIO + println(s"return type is $name, isCorrect: $isCorrect") + isCorrect +// name match { +// case ZIO | TASK | UIO => true +// case _ => false +// } + } +} diff --git a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java new file mode 100644 index 0000000..dd6210f --- /dev/null +++ b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java @@ -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()); + } +} diff --git a/zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioDevModeTest.scala b/zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioDevModeTest.scala new file mode 100644 index 0000000..fd8d727 --- /dev/null +++ b/zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioDevModeTest.scala @@ -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()); + } +} diff --git a/zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioTest.scala b/zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioTest.scala new file mode 100644 index 0000000..b5ef78d --- /dev/null +++ b/zio/deployment/src/test/scala/io/quarkiverse/scala/scala3/zio/test/Scala3ZioTest.scala @@ -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()); + } +} diff --git a/zio/integration-tests/pom.xml b/zio/integration-tests/pom.xml new file mode 100644 index 0000000..a0ec7e0 --- /dev/null +++ b/zio/integration-tests/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + + io.quarkiverse.scala + quarkus-scala3-zio-parent + 999-SNAPSHOT + + quarkus-scala3-zio-integration-tests + Quarkus Scala3 Zio - Integration Tests + + + false + + + + + io.quarkus + quarkus-rest + + + io.quarkiverse.scala + quarkus-scala3-zio + ${project.version} + + + io.quarkiverse.scala + quarkus-scala3-zio-deployment + ${project.version} + + + org.scala-lang + scala3-compiler_3 + ${scala.version} + test + + + org.scala-lang + scala3-library_3 + ${scala.version} + test + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + dev.zio + zio_3 + + + + + + src/main/scala + src/test/scala + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + net.alchim31.maven + scala-maven-plugin + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + true + + + + diff --git a/zio/integration-tests/src/main/resources/application.properties b/zio/integration-tests/src/main/resources/application.properties new file mode 100644 index 0000000..e69de29 diff --git a/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala b/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala new file mode 100644 index 0000000..fc9a48d --- /dev/null +++ b/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala @@ -0,0 +1,28 @@ + +package io.quarkiverse.scala.scala3.zio.it + +import jakarta.enterprise.context.ApplicationScoped +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import zio.ZIO + +@Path("/scala3-zio") +@ApplicationScoped +class Scala3ZioResource { + // add some rest methods here + + @GET + @Path("") + def hello(): String = "Hello scala3-zio simple string" + + @GET + @Path("/zio-string") + def zioString: ZIO[String with Int, Throwable, String] = { + import zio._ + for { + _ <- ZIO.sleep(2.seconds) + } yield "Hello ZIO" + } + + +} diff --git a/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Given.scala b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Given.scala new file mode 100644 index 0000000..6f9e5e7 --- /dev/null +++ b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Given.scala @@ -0,0 +1,27 @@ +package io.quarkiverse.scala.scala3.zio.it + +import io.restassured.RestAssured.* +import io.restassured.internal.{ResponseSpecificationImpl, ValidatableResponseImpl} +import io.restassured.response.{ExtractableResponse, Response, ValidatableResponse} +import io.restassured.specification.{RequestSender, RequestSpecification, ResponseSpecification} + +class GivenConstructor(givenBlock: RequestSpecification => RequestSpecification): + def When(whenBlock: RequestSpecification => Response): ExpectationConstructor = + ExpectationConstructor(givenBlock, whenBlock) + + class ExpectationConstructor( + givenBlock: RequestSpecification => RequestSpecification, + whenBlock: RequestSpecification => Response + ): + def Then(validatable: ValidatableResponse => Unit) = + val appliedGiven: RequestSpecification = givenBlock.apply(`given`()) + val appliedWhen: Response = whenBlock.apply(appliedGiven) + validatable.apply(appliedWhen.`then`()) + +object Given: + def apply(givenBlock: RequestSpecification => RequestSpecification): GivenConstructor = GivenConstructor(givenBlock) + +def When(whenBlock: RequestSpecification => Response) = + def blankGiven(givenBlock: RequestSpecification): RequestSpecification = `given`() + Given(blankGiven).When(whenBlock) + \ No newline at end of file diff --git a/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceIT.scala b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceIT.scala new file mode 100644 index 0000000..2b341c3 --- /dev/null +++ b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceIT.scala @@ -0,0 +1,7 @@ +package io.quarkiverse.scala.scala3.zio.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class Scala3ZioResourceIT extends Scala3ZioResourceTest { +} diff --git a/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala new file mode 100644 index 0000000..bc6dfa9 --- /dev/null +++ b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala @@ -0,0 +1,36 @@ +package io.quarkiverse.scala.scala3.zio.it; + + +import org.hamcrest.Matchers.is +import org.junit.jupiter.api.Test +import io.quarkus.test.junit.QuarkusTest +import org.hamcrest.Matchers + +@QuarkusTest +class Scala3ZioResourceTest { + + + @Test + def `test Hello Endpoint`(): Unit = { + Given { + _.params("something", "value") + }.When { + _.get("/scala3-zio").prettyPeek() + }.Then { + _.statusCode(200).body(is("Hello scala3-zio simple string")) + } + } + + @Test + def `test zio-string Endpoint`(): Unit = { + println("running `test zio-string Endpoint`") + Given { + _.params("something", "value") + }.When { + _.get("/scala3-zio/zio-string").prettyPeek() + }.Then { + _.statusCode(200).body(is("Hello ZIO")) + } + } + +} diff --git a/zio/pom.xml b/zio/pom.xml new file mode 100644 index 0000000..8f6c605 --- /dev/null +++ b/zio/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + + io.quarkiverse + quarkiverse-parent + 16 + + io.quarkiverse.scala + quarkus-scala3-zio-parent + 999-SNAPSHOT + pom + Quarkus Scala3 Zio - Parent + + + deployment + runtime + + + + 3.12.1 + 17 + UTF-8 + UTF-8 + 3.10.0 + 4.9.1 + 3.3.3 + 2.1.0 + + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + org.scala-lang + scala3-library_3 + ${scala.version} + + + dev.zio + zio_3 + ${zio.version} + + + + + + src/main/scala + src/test/scala + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin.version} + + + + scala-compile-first + process-resources + + add-source + compile + + + + + scala-test-compile + process-test-resources + + add-source + testCompile + + + + + + -Wunused:all + -feature + -deprecation + -Ysemanticdb + + + + + + + + + + it + + + performRelease + !true + + + + integration-tests + + + + diff --git a/zio/runtime/pom.xml b/zio/runtime/pom.xml new file mode 100644 index 0000000..91f01d8 --- /dev/null +++ b/zio/runtime/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.quarkiverse.scala + quarkus-scala3-zio-parent + 999-SNAPSHOT + + quarkus-scala3-zio + Quarkus Scala3 Zio - Runtime + + + + io.quarkus + quarkus-arc + + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/zio/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/zio/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..9ff048d --- /dev/null +++ b/zio/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +name: Scala3 Zio +#description: Do something useful. +metadata: + keywords: + - scala + - scala3 + - zio +# guide: https://quarkiverse.github.io/quarkiverse-docs/scala3-zio/dev/ # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension +# categories: +# - "miscellaneous" +# status: "preview" From a3b5aaf7da5916d8c7cb39607e63b61f0832cbb6 Mon Sep 17 00:00:00 2001 From: Dominik Dorn Date: Fri, 10 May 2024 13:32:43 +0200 Subject: [PATCH 2/4] ZIO: checking the error type is either Nothing or a Throwable --- ...essor.java => Scala3ZIOJavaProcessor.java} | 2 +- .../deployment/Scala3ZIOResponseHandler.scala | 59 ++++++++----------- .../Scala3ZIOReturnTypeMethodScanner.scala | 42 +++++++------ .../scala/scala3/zio/it/CustomThrowable.java | 5 ++ .../scala3/zio/it/Scala3ZioResource.scala | 43 +++++++++++++- .../scala3/zio/it/Scala3ZioResourceTest.scala | 37 +++++++++++- 6 files changed, 132 insertions(+), 56 deletions(-) rename zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/{Scala3ZioJavaProcessor.java => Scala3ZIOJavaProcessor.java} (93%) create mode 100644 zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/CustomThrowable.java diff --git a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOJavaProcessor.java similarity index 93% rename from zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java rename to zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOJavaProcessor.java index dd6210f..2f7676b 100644 --- a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZioJavaProcessor.java +++ b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOJavaProcessor.java @@ -4,7 +4,7 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; -public class Scala3ZioJavaProcessor { +public class Scala3ZIOJavaProcessor { @BuildStep public FeatureBuildItem feature() { diff --git a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala index a68a6a6..cb7c170 100644 --- a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala +++ b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOResponseHandler.scala @@ -4,48 +4,41 @@ import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext import org.jboss.resteasy.reactive.server.spi.ServerRestHandler import zio.ZIO -import java.lang.reflect.ParameterizedType import scala.jdk.CollectionConverters.* -import scala.util.Failure -import scala.util.Success 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 - /* - // TODO at the moment, we're just stupidly assume, the effect has no environment - // and the error type is a throwable. We need to figure out a way on how to access - // the type arguments of the ZIO effect in a more structured way. - // At the moment, a Environment of e.g. String with Int will be read as java.lang.Object, - // so we loose the type information which could be used for dependency injection - println("in handle of Scala3ZIOResponseHandler") - - val p = requestContext.getGenericReturnType.asInstanceOf[ParameterizedType] - val typeArgs = p.getActualTypeArguments.toSeq - val (environment, errorType, successType) = (typeArgs(0), typeArgs(1), typeArgs(2)) - - println(s"environment: $environment") - println(s"errorType: $errorType") - println(s"successType: $successType") - */ - result match - case r: ZIO[R, E, A] => - requestContext.suspend() - val f = zio.Unsafe.unsafe(u => zio.Runtime.default.unsafe.runToFuture(r)(zio.Trace.empty, u)) - f.onComplete { - case Success(value) => - requestContext.setResult(value) - requestContext.resume() - case Failure(exception) => - requestContext.handleException(exception, true) - requestContext.resume() - }(scala.concurrent.ExecutionContext.global) - - case _ => () + + 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)) } } diff --git a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala index 0db1c3e..f0718fe 100644 --- a/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala +++ b/zio/deployment/src/main/scala/io/quarkiverse/scala/scala3/zio/deployment/Scala3ZIOReturnTypeMethodScanner.scala @@ -1,35 +1,29 @@ package io.quarkiverse.scala.scala3.zio.deployment -import org.jboss.jandex.AnnotationInstance 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.core.parameters.ParameterExtractor 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 zio.RIO import java.util import java.util.List as JList import java.util.Collections as JCollections class Scala3ZIOReturnTypeMethodScanner extends MethodScanner { - val ZIO: DotName = DotName.createSimple("zio.ZIO") -// val TASK: DotName = DotName.createSimple("zio.Task") -// val UIO: DotName = DotName.createSimple("zio.UIO") -// val RIO: DotName = DotName.createSimple("zio.RIO") - - + 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] = { - println("in Scala3ZIOReturnTypeMethodScanner scan") if(isMethodSignatureAsync(method)) { + ensuringFailureTypeIsNothingOrAThrowable(method) JCollections.singletonList( new FixedHandlerChainCustomizer( new Scala3ZIOResponseHandler(), @@ -41,16 +35,28 @@ class Scala3ZIOReturnTypeMethodScanner extends MethodScanner { } } + 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 = { - val name = info.returnType().name() - val isCorrect = name == ZIO - println(s"return type is $name, isCorrect: $isCorrect") - isCorrect -// name match { -// case ZIO | TASK | UIO => true -// case _ => false -// } + info.returnType().name() == ZIO } } diff --git a/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/CustomThrowable.java b/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/CustomThrowable.java new file mode 100644 index 0000000..197934e --- /dev/null +++ b/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/CustomThrowable.java @@ -0,0 +1,5 @@ +package io.quarkiverse.scala.scala3.zio.it; + +public class CustomThrowable extends RuntimeException { + +} diff --git a/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala b/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala index fc9a48d..bfef1a3 100644 --- a/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala +++ b/zio/integration-tests/src/main/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResource.scala @@ -4,7 +4,8 @@ package io.quarkiverse.scala.scala3.zio.it import jakarta.enterprise.context.ApplicationScoped import jakarta.ws.rs.GET import jakarta.ws.rs.Path -import zio.ZIO +import jakarta.ws.rs.QueryParam +import zio.* @Path("/scala3-zio") @ApplicationScoped @@ -17,12 +18,48 @@ class Scala3ZioResource { @GET @Path("/zio-string") - def zioString: ZIO[String with Int, Throwable, String] = { + def zioString: ZIO[String with Int, CustomThrowable, String] = { import zio._ for { _ <- ZIO.sleep(2.seconds) } yield "Hello ZIO" } - + + + @GET + @Path("/zio-task") + def zioTask(@QueryParam("a") a: String): Task[String] = { + import zio._ + for { + _ <- ZIO.unit + } yield s"Hello ZIO Task: ${a}" + } + + @GET + @Path("/zio-uio") + def zioUIO(@QueryParam("a") a: String): UIO[String] = { + import zio._ + for { + _ <- ZIO.unit + } yield s"Hello ZIO UIO: ${a}" + } + + @GET + @Path("/zio-io") + def zioIO(@QueryParam("a") a: String): IO[CustomThrowable, String] = { + import zio._ + for { + _ <- ZIO.unit + } yield s"Hello ZIO IO: ${a}" + } + +// @GET +// @Path("/zio-wrong-error-type") +// def zioError(a: String): ZIO[Any, Nothing, String] = { +// import zio._ +// for { +// _ <- ZIO.unit +// } yield "Hello ZIO" +// } } diff --git a/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala index bc6dfa9..c81f91d 100644 --- a/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala +++ b/zio/integration-tests/src/test/scala/io/quarkiverse/scala/scala3/zio/it/Scala3ZioResourceTest.scala @@ -23,7 +23,6 @@ class Scala3ZioResourceTest { @Test def `test zio-string Endpoint`(): Unit = { - println("running `test zio-string Endpoint`") Given { _.params("something", "value") }.When { @@ -33,4 +32,40 @@ class Scala3ZioResourceTest { } } + @Test + def `test zio Task Endpoint`(): Unit = { + Given { + _.params("a", "value") + }.When { + _.get("/scala3-zio/zio-task") + }.Then { + _.statusCode(200).body(is("Hello ZIO Task: value")) + } + } + + @Test + def `test zio UIO Endpoint`(): Unit = { + Given { + _.params("a", "value") + }.When { + _.get("/scala3-zio/zio-uio") + }.Then { + _.statusCode(200).body(is("Hello ZIO UIO: value")) + } + } + + @Test + def `test zio IO Endpoint`(): Unit = { + Given { + _.params("a", "value") + }.When { + _.get("/scala3-zio/zio-io") + }.Then { + _.statusCode(200).body(is("Hello ZIO IO: value")) + } + } + + + + } From eb3702013f856e136d49ddd2b4523583056601fd Mon Sep 17 00:00:00 2001 From: Dominik Dorn Date: Fri, 10 May 2024 13:43:48 +0200 Subject: [PATCH 3/4] ZIO: TODOs + Ideas added --- zio/TODO.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 zio/TODO.md diff --git a/zio/TODO.md b/zio/TODO.md new file mode 100644 index 0000000..e02842a --- /dev/null +++ b/zio/TODO.md @@ -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. +- ... From fbb35ec7496d4bcda742a21c65d4760694b19b67 Mon Sep 17 00:00:00 2001 From: Dominik Dorn Date: Fri, 10 May 2024 13:51:10 +0200 Subject: [PATCH 4/4] ZIO: Adjusted README.md --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index 726098d..cf163e8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -462,3 +463,61 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d 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 + + io.quarkiverse.scala + quarkus-scala3-zio + 999-SNAPSHOT + + + io.quarkiverse.scala + quarkus-scala3-zio-deployment + 999-SNAPSHOT + +``` + +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.