Context and Dependencies
When implementing a server, ZIO gRPC allows you to specify that your service
methods depend depends on a context of type Context
which can be any Scala type.
For example, we can define a service with handlers that expect a context of type User
for each request:
import zio.ZIO
import zio.Console
import zio.Console.printLine
import scalapb.zio_grpc.RequestContext
import myexample.testservice.ZioTestservice.ZSimpleService
import myexample.testservice.{Request, Response}
import io.grpc.{Status, StatusException}
case class User(name: String)
object MyService extends ZSimpleService[User] {
def sayHello(req: Request, user: User): ZIO[Any, StatusException, Response] =
for {
_ <- printLine("I am here!").orDie
} yield Response(s"Hello, ${user.name}")
}
Context transformations
In order to be able to bind our service to a gRPC server, we need to have the service's Context type to be one of the supported types:
scalapb.zio_grpc.RequestContext
scalapb.zio_grpc.SafeMetadata
Any
The service MyService
as defined above expects User
as a context. In order to be able to bind it, we will transform it into a service that depends on a context of type RequestContext
. To do this, we need to provide the function to produce a User
out of a RequestContext
. This way, when a request comes in, ZIO gRPC can take the RequestContext
(which is request metadata such as headers and options), and use our function to construct a User
and provide it into the environment of our original service.
In many typical cases, we may need to retrieve the user from a database, and thus we are using an effectful function RequestContext => IO[Status, User]
to find the user.
For example, we can provide a function that returns an effect that always succeeds:
val fixedUserService =
MyService.transformContextZIO((rc: RequestContext) => ZIO.succeed(User("foo")))
// fixedUserService: myexample.testservice.ZioTestservice.GSimpleService[RequestContext, StatusException] = myexample.testservice.ZioTestservice$GSimpleService$$anon$5@2eaf4b72
and we got our service with context of type RequestContext
so it can be bound to a gRPC server.
Accessing metadata
Here is how we would extract a user from a metadata header:
import zio.IO
import scalapb.zio_grpc.{ServiceList, ServerMain}
val UserKey = io.grpc.Metadata.Key.of(
"user-key", io.grpc.Metadata.ASCII_STRING_MARSHALLER)
// UserKey: io.grpc.Metadata.Key[String] = Key{name='user-key'}
def findUser(rc: RequestContext): IO[StatusException, User] =
rc.metadata.get(UserKey).flatMap {
case Some(name) => ZIO.succeed(User(name))
case _ => ZIO.fail(
Status.UNAUTHENTICATED.withDescription("No access!").asException)
}
val rcService =
MyService.transformContextZIO(findUser)
// rcService: myexample.testservice.ZioTestservice.GSimpleService[RequestContext, StatusException] = myexample.testservice.ZioTestservice$GSimpleService$$anon$5@56589821
object MyServer extends ServerMain {
def services = ServiceList.add(rcService)
}
Context transformations that depends on a service
A context transformation may introduce a dependency on another service. For example, you
may want to organize your code such that there is a UserDatabase
service that provides
a fetchUser
effect that retrieves users from a database. Here is how you can do this:
trait UserDatabase {
def fetchUser(name: String): IO[StatusException, User]
}
object UserDatabase {
val layer = zio.ZLayer.succeed(
new UserDatabase {
def fetchUser(name: String): IO[StatusException, User] =
ZIO.succeed(User(name))
})
}
Now, The context transformation effect we apply may introduce an additional environmental dependency to our service. For example:
import zio.Clock._
import zio.Duration._
val myServiceAuthWithDatabase: ZIO[UserDatabase, Nothing, ZSimpleService[RequestContext]] =
ZIO.serviceWith[UserDatabase](
userDatabase =>
MyService.transformContextZIO {
(rc: RequestContext) =>
rc.metadata.get(UserKey)
.someOrFail(Status.UNAUTHENTICATED.asException)
.flatMap(userDatabase.fetchUser(_))
}
)
// myServiceAuthWithDatabase: ZIO[UserDatabase, Nothing, ZSimpleService[RequestContext]] = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myServiceAuthWithDatabase(context.md:104)",
// first = Sync(
// trace = "repl.MdocSession.MdocApp.myServiceAuthWithDatabase(context.md:104)",
// eval = zio.ZIO$ServiceWithZIOPartiallyApplied$$$Lambda$14975/0x00000001040ac840@458927e4
// ),
// successK = zio.ZIO$$$Lambda$14966/0x000000010409e840@e46e6ba
// )
Now our service can be built from an effect that depends on UserDatabase
. This effect can be
added to a ServiceList
using addZIO
:
object MyServer2 extends ServerMain {
def services = ServiceList
.addZIO(myServiceAuthWithDatabase)
.provide(UserDatabase.layer)
}
Using a service as ZLayer
If you require more flexibility than provided through ServerMain
, you can construct
the server directly.
We first turn our service into a ZLayer:
val myServiceLayer = zio.ZLayer(myServiceAuthWithDatabase)
// myServiceLayer: zio.ZLayer[UserDatabase, Nothing, ZSimpleService[RequestContext]] = Suspend(
// self = zio.ZLayer$$$Lambda$15006/0x00000001040c2440@69168e41
// )
Notice how the dependencies moved to the input side of the ZLayer
and the resulting layer is of
type ZSimpleService[RequestContext]
.
To use this layer in an app, we can wire it like so:
import scalapb.zio_grpc.ServerLayer
import scalapb.zio_grpc.Server
import zio.ZLayer
val serviceList = ServiceList
.addFromEnvironment[ZSimpleService[RequestContext]]
// serviceList: ServiceList[Any with ZSimpleService[RequestContext]] = scalapb.zio_grpc.ServiceList@244c13ca
val serverLayer =
ServerLayer.fromServiceList(
io.grpc.ServerBuilder.forPort(9000),
serviceList
)
// serverLayer: ZLayer[Any with ZSimpleService[RequestContext], Throwable, Server] = Suspend(
// self = zio.ZLayer$ScopedEnvironmentPartiallyApplied$$$Lambda$15027/0x00000001040cb040@1d828596
// )
val ourApp =
ZLayer.make[Server](
serverLayer,
myServiceLayer,
UserDatabase.layer
)
// ourApp: ZLayer[Any, Throwable, Server] = Suspend(
// self = zio.ZLayer$ZLayerProvideSomeOps$$$Lambda$15029/0x00000001040cf840@781b8ec3
// )
object LayeredApp extends zio.ZIOAppDefault {
def run = ourApp.launch.exitCode
}
serverLayer
creates a Server
from a ZSimpleService
layer and still depends on a UserDatabase
. Then, ourApp
feeds a UserDatabase.layer
into serverLayer
to produce
a Server
that doesn't depend on anything. In the run
method we launch the server layer.
Implementing a service with dependencies
In this scenario, your service depends on two additional services, DepA
and DepB
. Following ZIO's service pattern, we accept the (interaces of the ) dependencies as constructor parameters.
trait DepA {
def methodA(param: String): ZIO[Any, Nothing, Int]
}
object DepA {
val layer = ZLayer.succeed[DepA](new DepA {
def methodA(param: String) = ???
})
}
object DepB {
val layer = ZLayer.succeed[DepB](new DepB {
def methodB(param: Float) = ???
})
}
trait DepB {
def methodB(param: Float): ZIO[Any, Nothing, Double]
}
case class MyService2(depA: DepA, depB: DepB) extends ZSimpleService[User] {
def sayHello(req: Request, user: User): ZIO[Any, StatusException, Response] =
for {
num1 <- depA.methodA(user.name)
num2 <- depB.methodB(12.3f)
_ <- printLine("I am here $num1 $num2!").orDie
} yield Response(s"Hello, ${user.name}")
}
object MyService2 {
val layer: ZLayer[DepA with DepB, Nothing, ZSimpleService[RequestContext]] =
ZLayer.fromFunction {
(depA: DepA, depB: DepB) =>
MyService2(depA, depB).transformContextZIO(findUser(_))
}
}
Our service layer now depends on the DepA
and DepB
interfaces. A server can be created like this:
object MyServer3 extends zio.ZIOAppDefault {
val serverLayer =
ServerLayer.fromServiceList(
io.grpc.ServerBuilder.forPort(9000),
ServiceList.addFromEnvironment[ZSimpleService[RequestContext]]
)
val appLayer = ZLayer.make[Server](
serverLayer,
DepA.layer,
DepB.layer,
MyService2.layer
)
def run = ourApp.launch.exitCode
}