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
// )