Skip to main content
Version: 0.6.x

Generated Code Reference

Packages and code location

For each proto file that contains services definition, ZIO gRPC generates a Scala object that will contain service definitions for all services in that file. The object name would be the proto file name prefixed with Zio. It would reside in the same Scala package that ScalaPB will use for definitions in that file.

You can read more on how ScalaPB determines the Scala package name and how can this be customized in ScalaPB's documentation.

Service trait

Inside the object, for each service MyService that is defined in a .proto file, the following structure is generated:

trait MyService {
// methods for each RPC
def sayHello(request: HelloRequest):
ZIO[Any, StatusException, HelloReply]
}

The trait MyService is to be extended when implementing a server for this service.

object MyServiceImpl extends MyService {
def sayHello(request: HelloRequest): ZIO[Any, StatusException, HelloReply] = ???
}

It is common that services need to extract information from the request context, for example the caller's identity. To accomplish that, there is another trait ZMyService which takes one type parameter Context. The Context type parameter represents any domain object that you would like your RPC methods to receive. Later on, we will see how to convert between a RequestContext which represents the underlying context of the request with your domain model.

The most generic type is called GMyService. It takes two type parameters: Context and Error. The context is the same as before, and the error could be any type you would like your server implementatino to use an error. Before you use this service with the rest of zio-grpc API, you would need to convert the error type to StatusException by using either mapError or mapErrorZIO.

Learn more about using context and dependencies in the next section.

Running the server

The easiest way to run a service, is to create an object that extends scalapb.zio_grpc.ServerMain:

import scalapb.zio_grpc.{ServerMain, ServiceList}

object MyMain extends ServerMain {
def services = ServiceList.add(ServiceNameImpl)

// Default port is 9000
override def port: Int = 8980
}

You can also override def port: Int to set a port number (by default port 9000 is used).

ServiceList contains additional methods to add services to the service list that can be used when the service must be created effectfully, resourcefully (scoped), or provided through a layer.

Client trait

The generated client follows ZIO's module pattern:

type ServiceNameClient = ServiceNameClient.Service

object ServiceNameClient {
trait ZService[Context] {
// methods for use as a client
def sayHello(request: HelloRequest):
ZIO[Context, StatusException, HelloReply]
}
type Service = ZService[Any]

// accessor methods
def sayHello(request: HelloRequest):
ZIO[ServiceNameClient, StatusException, HelloReply]

def scoped[R](
managedChannel: ZManagedChannel,
options: CallOptions =
io.grpc.CallOptions.DEFAULT,
headers: zio.UIO[SafeMetadata] =
scalapb.zio_grpc.SafeMetadata.make
): zio.Managed[Throwable, ZService[R]]

def live[Context](
managedChannel: ZManagedChannel,
options: CallOptions =
io.grpc.CallOptions.DEFAULT,
headers: zio.UIO[scalapb.zio_grpc.SafeMetadata] =
scalapb.zio_grpc.SafeMetadata.make
): zio.ZLayer[Any, Throwable, ZService[Context]]
}

We have two ways to use a client: through a managed resource, or through a layer. In both cases, we start by creating a ZManagedChannel, which represents a communication channel to a gRPC server as a managed resource. Since it is scoped, proper shutdown of the channel is guaranteed:

type ZManagedChannel[R] = ZIO[Scope, Throwable, ZChannel[R]]

Creating a channel:

import scalapb.zio_grpc.ZManagedChannel
import io.grpc.ManagedChannelBuilder

val channel = ZManagedChannel(
ManagedChannelBuilder
.forAddress("localhost", 8980)
.usePlaintext()
)
// channel: ZManagedChannel = OnSuccess(
// trace = "scalapb.zio_grpc.ZManagedChannel.apply(ZManagedChannel.scala:13)",
// first = Sync(
// trace = "scalapb.zio_grpc.ZManagedChannel.apply(ZManagedChannel.scala:13)",
// eval = zio.ZIO$$$Lambda$15080/0x00000001040dd840@1f146475
// ),
// successK = zio.ZIO$$$Lambda$15000/0x000000010409e840@7744254c
// )

Using the client as a layer

A single ZManagedChannel represent a virtual connection to a conceptual endpoint to perform RPCs. A channel can have many actual connection to the endpoint. Therefore, it is very common to have a single service client for each RPC service you need to connect to. You can create a ZLayer to provide this service using the live method on the client companion object. Then simply write your logic using the accessor methods. Finally, inject the layer using provideLayer at the top of your app:

import myexample.testservice.ZioTestservice.ServiceNameClient
import myexample.testservice.{Request, Response}
import zio._
import zio.Console._

// create layer:
val clientLayer = ServiceNameClient.live(channel)
// clientLayer: ZLayer[Any, Throwable, ServiceNameClient] = Fold(
// self = Suspend(
// self = zio.ZLayer$ScopedEnvironmentPartiallyApplied$$$Lambda$15061/0x00000001040cb040@1b2bf34f
// ),
// failure = zio.ZLayer$$Lambda$15087/0x000000010411c040@5e296e4e,
// success = zio.ZLayer$$Lambda$15085/0x000000010411a840@53b607d4
// )

val myAppLogicNeedsEnv = for {
// use layer through accessor methods:
res <- ServiceNameClient.unary(Request())
_ <- printLine(res.toString)
} yield ()
// myAppLogicNeedsEnv: ZIO[ServiceNameClient, Exception, Unit] = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myAppLogicNeedsEnv(generated-code.md:41)",
// first = OnSuccess(
// trace = "myexample.testservice.ZioTestservice.ServiceNameAccessors.unary(ZioTestservice.scala:77)",
// first = Sync(
// trace = "myexample.testservice.ZioTestservice.ServiceNameAccessors.unary(ZioTestservice.scala:77)",
// eval = zio.ZIO$ServiceWithZIOPartiallyApplied$$$Lambda$15009/0x00000001040ac840@2bc8c128
// ),
// successK = zio.ZIO$$$Lambda$15000/0x000000010409e840@7744254c
// ),
// successK = <function1>
// )

// myAppLogicNeedsEnv needs access to a ServiceNameClient. We turn it into
// a self-contained effect (IO) by providing the layer to it:
val myAppLogic1 = myAppLogicNeedsEnv.provideLayer(clientLayer)
// myAppLogic1: ZIO[Any, Throwable, Unit] = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myAppLogic1(generated-code.md:46)",
// first = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myAppLogic1(generated-code.md:46)",
// first = Sync(
// trace = "repl.MdocSession.MdocApp.myAppLogic1(generated-code.md:46)",
// eval = zio.Scope$ReleaseMap$$$Lambda$15100/0x0000000104123840@7681e017
// ),
// successK = zio.ZIO$$Lambda$15057/0x00000001040c9840@30ccca3f
// ),
// successK = zio.ZIO$$$Lambda$15153/0x000000010414a040@3a48cbbe
// )

object LayeredApp extends zio.ZIOAppDefault {
def run: UIO[ExitCode] = myAppLogic1.exitCode
}

Here the application is broken to multiple value assignments so you can see the types. The first effect myAppLogicNeedsEnv uses accessor functions, which makes it depend on an environment of type ServiceNameClient. It chains the unary RPC with printing the result to the console, and hence the final inferred effect type is ServiceNameClient. Once we provide our custom layer, the effect type is ZEnv, which we can use with ZIO's exit method.

Using a Scoped client

As an alternative to using ZLayer, you can use the client as a scoped resource:

import myexample.testservice.ZioTestservice.ServiceNameClient
import myexample.testservice.{Request, Response}

val clientManaged = ServiceNameClient.scoped(channel)
// clientManaged: ZIO[Scope, Throwable, ServiceNameClient] = OnSuccess(
// trace = "myexample.testservice.ZioTestservice.ServiceNameClient.scoped(ZioTestservice.scala:109)",
// first = OnSuccess(
// trace = "myexample.testservice.ZioTestservice.ServiceNameClientWithResponseMetadata.scoped(ZioTestservice.scala:191)",
// first = OnSuccess(
// trace = "scalapb.zio_grpc.ZManagedChannel.apply(ZManagedChannel.scala:13)",
// first = Sync(
// trace = "scalapb.zio_grpc.ZManagedChannel.apply(ZManagedChannel.scala:13)",
// eval = zio.ZIO$$$Lambda$15080/0x00000001040dd840@1f146475
// ),
// successK = zio.ZIO$$$Lambda$15000/0x000000010409e840@7744254c
// ),
// successK = zio.ZIO$$Lambda$15057/0x00000001040c9840@a58556c
// ),
// successK = zio.ZIO$$Lambda$15057/0x00000001040c9840@53e86cdc
// )

val myAppLogic = ZIO.scoped {
clientManaged.flatMap { client =>
for {
res <- client.unary(Request())
} yield res
}
}
// myAppLogic: ZIO[Any, Throwable, Response] = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myAppLogic(generated-code.md:66)",
// first = OnSuccess(
// trace = "repl.MdocSession.MdocApp.myAppLogic(generated-code.md:66)",
// first = Sync(
// trace = "repl.MdocSession.MdocApp.myAppLogic(generated-code.md:66)",
// eval = zio.Scope$ReleaseMap$$$Lambda$15100/0x0000000104123840@7681e017
// ),
// successK = zio.ZIO$$Lambda$15057/0x00000001040c9840@794af79d
// ),
// successK = zio.ZIO$ScopedPartiallyApplied$$$Lambda$15102/0x0000000104125040@31713eed
// )