Skip to content

Commit

Permalink
ZIO: checking the error type is either Nothing or a Throwable
Browse files Browse the repository at this point in the history
  • Loading branch information
domdorn committed May 10, 2024
1 parent 992c22e commit df507ea
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,44 @@ 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]]

// we can experiment to update the requestContext directly from within the zio runtime.. for
// now working with a future seems good enough

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
@@ -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(),
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.quarkiverse.scala.scala3.zio.it;

public class CustomThrowable extends RuntimeException {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
// }

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class Scala3ZioResourceTest {

@Test
def `test zio-string Endpoint`(): Unit = {
println("running `test zio-string Endpoint`")
Given {
_.params("something", "value")
}.When {
Expand All @@ -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"))
}
}




}

0 comments on commit df507ea

Please sign in to comment.