Context and Dependencies

When implementing a server, ZIO gRPC allows you to specify that your service depends on an environment of type R and a context of type Context.

Context and R can be of any Scala type, however when they are not Any they have to be wrapped in an Has[]. This allows ZIO gRPC to combine two values (Context with R) when providing the values at effect execution time.

For example, we can define a service for which the effects depend on Console, and for each request we expect to get a context of type User. Note that Console is a type-alias to Has[Console.Service] so there is no need wrap it once more in an Has.

import zio.{Has, ZIO}
import zio.console._
import scalapb.zio_grpc.RequestContext
import myexample.testservice.ZioTestservice.ZSimpleService
import myexample.testservice.{Request, Response}
import io.grpc.Status
case class User(name: String)
object MyService extends ZSimpleService[Console, Has[User]] {
def sayHello(req: Request): ZIO[Console with Has[User], Status, Response] =
for {
user <- ZIO.service[User]
_ <- putStrLn("I am here!").orDie
} yield Response(s"Hello, ${user.name}")
}

As you can see above, we can access both the User and the Console in our effects. If one of the methods does not need to access the dependencies or context, the returned type from the method can be cleaned up to reflect that certain things are not needed.

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:

  • Has[scalapb.zio_grpc.RequestContext]
  • Has[scalapb.zio_grpc.SafeMetadata]
  • Any

The service MyService as defined above expects Has[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 Has[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.transformContextM((rc: RequestContext) => ZIO.succeed(User("foo")))
// fixedUserService: ZSimpleService[Console, Has[RequestContext]] = myexample.testservice.ZioTestservice$ZSimpleService$$anon$6$$anon$7@4b87155c

and we got our service, which still depends on an environment of type Console, however the context is now Has[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[Status, User] =
rc.metadata.get(UserKey).flatMap {
case Some(name) => IO.succeed(User(name))
case _ => IO.fail(Status.UNAUTHENTICATED.withDescription("No access!"))
}
val rcService =
MyService.transformContextM(findUser)
// rcService: ZSimpleService[Console, Has[RequestContext]] = myexample.testservice.ZioTestservice$ZSimpleService$$anon$6$$anon$7@23ea6576
object MyServer extends ServerMain {
def services = ServiceList.add(rcService)
}

Depending 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:

type UserDatabase = Has[UserDatabase.Service]
object UserDatabase {
trait Service {
def fetchUser(name: String): IO[Status, User]
}
// accessor
def fetchUser(name: String): ZIO[UserDatabase, Status, User] =
ZIO.accessM[UserDatabase](_.get.fetchUser(name))
val live = zio.ZLayer.succeed(
new Service {
def fetchUser(name: String): IO[Status, User] =
IO.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 =
MyService.transformContextM {
(rc: RequestContext) =>
rc.metadata.get(UserKey)
.someOrFail(Status.UNAUTHENTICATED)
.flatMap(UserDatabase.fetchUser)
}
// myServiceAuthWithDatabase: ZSimpleService[Console with UserDatabase, Has[RequestContext]] = myexample.testservice.ZioTestservice$ZSimpleService$$anon$6$$anon$7@16170263

And now our service not only depends on a Console, but also on a UserDatabase.

Using a service as ZLayer#

We can turn our service into a ZLayer:

val myServiceLive = myServiceAuthWithDatabase.toLayer
// myServiceLive: zio.ZLayer[Console with UserDatabase, Nothing, Has[ZSimpleService[Any, Has[RequestContext]]]] = Managed(
// self = zio.ZManaged$$anon$2@261dcfdc
// )

notice how the dependencies moved to the input side of the Layer and the resulting layer is of type ZSimpleService[Any, Has[RequestContext]]], which means no environment is expected, and it assumes a Has[RequestContext] context. To use this layer in an app, we can wire it like so:

import scalapb.zio_grpc.ServerLayer
val serverLayer =
ServerLayer.fromServiceLayer(
io.grpc.ServerBuilder.forPort(9000)
)(myServiceLive)
// serverLayer: zio.ZLayer[Console with UserDatabase, Throwable, Has[scalapb.zio_grpc.Server.Service]] = Fold(
// self = Managed(self = zio.ZManaged$$anon$2@261dcfdc),
// failure = Managed(self = zio.ZManaged$$anon$2@442bc6ef),
// success = Managed(self = zio.ZManaged$$anon$2@61e77860)
// )
val ourApp = (UserDatabase.live ++ Console.any) >>>
serverLayer
// ourApp: zio.ZLayer[Any with Console, Throwable, Has[scalapb.zio_grpc.Server.Service]] = Fold(
// self = ZipWithPar(
// self = Managed(self = zio.ZManaged$$anon$2@4dde3663),
// that = Managed(self = zio.ZManaged$$anon$2@780f7ef7),
// f = zio.ZLayer$$Lambda$14561/4550526@2f0988de
// ),
// failure = Managed(self = zio.ZManaged$$anon$2@1da0393d),
// success = Fold(
// self = Managed(self = zio.ZManaged$$anon$2@261dcfdc),
// failure = Managed(self = zio.ZManaged$$anon$2@442bc6ef),
// success = Managed(self = zio.ZManaged$$anon$2@61e77860)
// )
// )
object LayeredApp extends zio.App {
def run(args: List[String]) = ourApp.build.useForever.exitCode
}

serverLayer wraps around our service layer to produce a server. Then, ourApp layer is constructed such that it takes UserDatabase.live in conjuction to a passthrough layer for Console to satisfy the two input requirements of serverLayer. The outcome, ourApp, is a ZLayer that can produce a Server from a Console. In the run method we build the layer and run it. Note that we are directly using a zio.App rather than ServerMain which does not support this use case yet.