feat(rpc): add zio-blocks-rpc module with derives RPC type class (#1143)#1225
feat(rpc): add zio-blocks-rpc module with derives RPC type class (#1143)#1225
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new zio-blocks-rpc module that can derive an RPC[T] descriptor from Scala 3 service traits and provides a reference JSON-RPC 2.0 codec/deriver, plus wires the module into the build and test aliases.
Changes:
- Introduces
RPC[T], annotations, and protocol derivation abstractions (RpcDeriver,RpcFormat), with a Scala 3 macro (RPC.derived) to reify service traits. - Adds a JSON-RPC 2.0 proof-of-concept (
JsonRpcCodec,JsonRpcDeriver,JsonRpcFormat) and integration/macro tests. - Updates
build.sbtto include the newrpccrossProject in root aggregation and aliases; also adds OTelHistogram/Gaugeimplementations.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| rpc/shared/src/main/scala/zio/blocks/rpc/RPC.scala | Defines the RPC[T] descriptor data model and derive entry point. |
| rpc/shared/src/main/scala/zio/blocks/rpc/annotations.scala | Adds MetaAnnotation / ErrorAnnotation base annotations for RPC metadata. |
| rpc/shared/src/main/scala/zio/blocks/rpc/RpcDeriver.scala | Protocol-deriver abstraction for building protocol implementations from RPC[T]. |
| rpc/shared/src/main/scala/zio/blocks/rpc/RpcFormat.scala | Format wrapper tying a protocol type constructor to its deriver. |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcCodec.scala | JSON-RPC request handler and response encoder (reference protocol). |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcDeriver.scala | Derives a JsonRpcCodec[T] from an RPC[T] descriptor. |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcFormat.scala | RpcFormat instance for JSON-RPC. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCMacros.scala | Scala 3 macro that reflects on trait methods to build RPC[T]. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCCompanionVersionSpecific.scala | Exposes inline def derived[T] macro hook on Scala 3. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCVersionSpecific.scala | Scala 3 version-specific trait placeholder. |
| rpc/shared/src/test/scala/zio/blocks/rpc/fixtures/TestFixtures.scala | Test fixtures: service traits, schemas, and sample annotations. |
| rpc/shared/src/test/scala/zio/blocks/rpc/jsonrpc/JsonRpcIntegrationSpec.scala | Integration tests for JSON-RPC behavior and derivation wiring. |
| rpc/shared/src/test/scala-3/zio/blocks/rpc/RPCMacroSpec.scala | Macro derivation tests verifying operation metadata and schemas. |
| otel/src/main/scala/zio/blocks/otel/Histogram.scala | Adds an OTel histogram metric implementation. |
| otel/src/main/scala/zio/blocks/otel/Gauge.scala | Adds an OTel gauge metric implementation. |
| build.sbt | Registers rpc crossProject, adds coverage thresholds/exclusions, and updates test/doc aliases. |
| import zio.blocks.schema.Schema | ||
| import zio.blocks.rpc.{MetaAnnotation, ErrorAnnotation} | ||
|
|
||
| implicit val throwableSchema: Schema[Throwable] = Schema.string.asInstanceOf[Schema[Throwable]] |
| def handleRequest(request: String): ZIO[Any, Nothing, String] = | ||
| Json.parse(request) match { | ||
| case Left(_) => | ||
| ZIO.succeed(JsonRpcCodec.errorResponse(Json.Null, -32700, "Parse error")) | ||
| case Right(parsed) => | ||
| val idJson = parsed.get("id").values.flatMap(_.headOption).getOrElse(Json.Null) | ||
| parsed.get("method").values.flatMap(_.headOption) match { | ||
| case None => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32600, "Invalid Request: missing 'method'") | ||
| ) |
| case Left(_) => | ||
| ZIO.succeed(JsonRpcCodec.errorResponse(Json.Null, -32700, "Parse error")) | ||
| case Right(parsed) => | ||
| val idJson = parsed.get("id").values.flatMap(_.headOption).getOrElse(Json.Null) |
| final class Histogram private ( | ||
| val name: String, | ||
| val description: String, | ||
| val unit: String, | ||
| val boundaries: Array[Double] | ||
| ) { |
| import zio.blocks.rpc._ | ||
| import zio.blocks.rpc.fixtures._ | ||
| import zio.blocks.schema.json.Json | ||
| import zio.blocks.chunk.Chunk |
5e17c3d to
a13ed0a
Compare
| `scope-examples`, | ||
| schema.jvm, | ||
| schema.js, | ||
| rpc.jvm, |
There was a problem hiding this comment.
Module missing in alias
There was a problem hiding this comment.
The rpc module is Scala 3 only (crossScalaVersions := Seq(scala3)), so it can't be added to the testJVM/testJS aliases — under ++2.13, rpcJVM doesn't exist as a project and sbt errors. Tried adding a ci.yml step for Scala-3-only module tests, but the OAuth token lacks workflow scope. This needs to be done via a direct push or a separate PR that modifies the workflow file.
There was a problem hiding this comment.
Pull request overview
Adds a new zio-blocks-rpc module that can derive an RPC[T] descriptor from a Scala 3 service trait (derives RPC-style), plus a JSON-RPC 2.0 proof-of-concept codec/deriver and accompanying tests.
Changes:
- Introduces the core RPC descriptor model (
RPC[T],Operation,ServiceMetadata) along withRpcDeriver/RpcFormatabstractions and annotation types. - Implements Scala 3 macro-based derivation (
RPC.derived[T]) to introspect service traits and summonSchemafor inputs/outputs/errors. - Adds a JSON-RPC reference protocol (
JsonRpcCodec,JsonRpcDeriver,JsonRpcFormat) and test suites for macro derivation + JSON-RPC integration; wires the new module intobuild.sbt.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| rpc/shared/src/main/scala/zio/blocks/rpc/RPC.scala | Defines the RPC[T] descriptor, operation metadata, and service metadata. |
| rpc/shared/src/main/scala/zio/blocks/rpc/RpcDeriver.scala | Adds the protocol-derivation interface for turning RPC[T] into a protocol implementation. |
| rpc/shared/src/main/scala/zio/blocks/rpc/RpcFormat.scala | Adds a Format-style wrapper associating a protocol type constructor with its deriver. |
| rpc/shared/src/main/scala/zio/blocks/rpc/annotations.scala | Introduces MetaAnnotation / ErrorAnnotation[E] for trait/method metadata. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCCompanionVersionSpecific.scala | Scala 3 inline entrypoint for RPC.derived[T]. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCMacros.scala | Implements the Scala 3 macro that reifies trait operations into an RPC[T]. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCVersionSpecific.scala | Adds a Scala-3-specific extension point placeholder for RPC (currently empty). |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcCodec.scala | Implements JSON-RPC request parsing/dispatch and response rendering. |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcDeriver.scala | Derives a JsonRpcCodec[T] from an RPC[T] descriptor (stub handlers). |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcFormat.scala | Provides a RpcFormat instance for JSON-RPC. |
| rpc/shared/src/test/scala/zio/blocks/rpc/fixtures/TestFixtures.scala | Adds service/data fixtures and a test-only Schema[Throwable] stub for derivation. |
| rpc/shared/src/test/scala-3/zio/blocks/rpc/RPCMacroSpec.scala | Adds Scala 3 tests validating macro-derived operation structure and schemas. |
| rpc/shared/src/test/scala/zio/blocks/rpc/jsonrpc/JsonRpcIntegrationSpec.scala | Adds integration tests for JSON-RPC request handling and derived codec behavior. |
| build.sbt | Registers the new rpc crossProject (JVM/JS), dependencies, and coverage settings; adds it to the root aggregation. |
| val rawId = parsed.get("id").values.flatMap(_.headOption).getOrElse(Json.Null) | ||
| val idJson = rawId match { | ||
| case _: Json.String | _: Json.Number | Json.Null => rawId | ||
| case _ => Json.Null | ||
| } | ||
| parsed.get("method").values.flatMap(_.headOption) match { | ||
| case None => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32600, "Invalid Request: missing 'method'") | ||
| ) | ||
| case Some(methodJson) => | ||
| methodJson match { | ||
| case str: Json.String => | ||
| val methodName = str.value | ||
| operationHandlers.get(methodName) match { | ||
| case None => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32601, s"Method not found: $methodName") | ||
| ) | ||
| case Some(handler) => | ||
| val params = parsed.get("params").values.flatMap(_.headOption).getOrElse(Json.Null) | ||
| handler | ||
| .handle(params) | ||
| .map(result => JsonRpcCodec.successResponse(idJson, result)) | ||
| .catchAll(error => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse( | ||
| idJson, | ||
| -32603, | ||
| Option(error.getMessage).getOrElse("Internal error") | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
| case _ => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32600, "Invalid Request: 'method' must be a string") | ||
| ) | ||
| } |
| * Trait-level annotation that specifies the error type for all operations in a | ||
| * service that don't specify their own. | ||
| * | ||
| * @tparam E | ||
| * The error type |
| .catchAll(error => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse( | ||
| idJson, | ||
| -32603, | ||
| Option(error.getMessage).getOrElse("Internal error") | ||
| ) | ||
| ) | ||
| ) |
9b5def8 to
6579f5f
Compare
Adds a new zio-blocks-rpc module providing a derives RPC type class that captures service trait structure as a pure data representation, analogous to how derives Schema works for data types. Includes JSON-RPC 2.0 reference protocol. Cross-compiles for Scala 2.13 + 3.x (macro derivation Scala 3 only). Closes zio#1143
There was a problem hiding this comment.
Pull request overview
Adds a new zio-blocks-rpc crossProject module that can derive an RPC[T] descriptor from Scala 3 service traits (via macro) and includes a JSON-RPC 2.0 proof-of-concept codec/deriver plus tests.
Changes:
- Introduces the
RPC[T]descriptor model (operations + schemas + annotations) and protocol-derivation abstractions (RpcDeriver,RpcFormat). - Adds Scala 3 macro-based derivation (
RPC.derived[T]) to introspect service traits and auto-summonSchemafor inputs/outputs/errors. - Adds a JSON-RPC codec/deriver/format with integration tests; wires the new module into build/test aliases.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| rpc/shared/src/main/scala/zio/blocks/rpc/RPC.scala | Defines the core RPC[T] descriptor + operation/metadata model. |
| rpc/shared/src/main/scala/zio/blocks/rpc/RpcDeriver.scala | Protocol-derivation type class for turning RPC[T] into protocol implementations. |
| rpc/shared/src/main/scala/zio/blocks/rpc/RpcFormat.scala | Associates a protocol type constructor with its RpcDeriver. |
| rpc/shared/src/main/scala/zio/blocks/rpc/annotations.scala | Adds annotation base types (MetaAnnotation, ErrorAnnotation[E]) used by the macro. |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcCodec.scala | JSON-RPC request/response handling and error mapping. |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcDeriver.scala | Derives a JsonRpcCodec[T] from an RPC[T]. |
| rpc/shared/src/main/scala/zio/blocks/rpc/jsonrpc/JsonRpcFormat.scala | RpcFormat instance for JSON-RPC. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCMacros.scala | Scala 3 macro implementation for RPC.derived[T]. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCCompanionVersionSpecific.scala | Scala 3 inline entry point to the macro. |
| rpc/shared/src/main/scala-3/zio/blocks/rpc/RPCVersionSpecific.scala | Scala 3 version-specific trait placeholder. |
| rpc/shared/src/main/scala-2/zio/blocks/rpc/RPCCompanionVersionSpecific.scala | Scala 2 stub for companion version-specific wiring. |
| rpc/shared/src/main/scala-2/zio/blocks/rpc/RPCVersionSpecific.scala | Scala 2 stub for version-specific trait wiring. |
| rpc/shared/src/test/scala-3/zio/blocks/rpc/RPCMacroSpec.scala | Macro derivation and schema/typeId assertions. |
| rpc/shared/src/test/scala-3/zio/blocks/rpc/fixtures/TestFixtures.scala | Service/data fixtures and test-only Schema[Throwable] stub. |
| rpc/shared/src/test/scala-3/zio/blocks/rpc/jsonrpc/JsonRpcIntegrationSpec.scala | JSON-RPC integration tests for request/response and derivation wiring. |
| build.sbt | Adds the rpc module, includes it in root aggregation + test/doc aliases. |
| .catchAll(error => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse( | ||
| idJson, | ||
| -32603, | ||
| Option(error.getMessage).getOrElse("Internal error") |
| val params = parsed.get("params").values.flatMap(_.headOption).getOrElse(Json.Null) | ||
| handler | ||
| .handle(params) | ||
| .map(result => JsonRpcCodec.successResponse(idJson, result)) | ||
| .catchAll(error => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse( | ||
| idJson, | ||
| -32603, | ||
| Option(error.getMessage).getOrElse("Internal error") | ||
| ) | ||
| ) | ||
| ) | ||
| } |
|
|
||
| trait OperationHandler { |
| /** | ||
| * A `RPC` is a data type that contains reified information on the structure of | ||
| * a service trait, together with schemas for its operations' input and output | ||
| * types. | ||
| * | ||
| * @tparam T | ||
| * The service trait type | ||
| */ | ||
| final case class RPC[T]( |
| val rawId = parsed.get("id").values.flatMap(_.headOption).getOrElse(Json.Null) | ||
| val idJson = rawId match { | ||
| case _: Json.String | _: Json.Number | Json.Null => rawId | ||
| case _ => Json.Null | ||
| } | ||
| parsed.get("method").values.flatMap(_.headOption) match { | ||
| case None => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32600, "Invalid Request: missing 'method'") | ||
| ) | ||
| case Some(methodJson) => | ||
| methodJson match { | ||
| case str: Json.String => | ||
| val methodName = str.value | ||
| operationHandlers.get(methodName) match { | ||
| case None => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32601, s"Method not found: $methodName") | ||
| ) | ||
| case Some(handler) => | ||
| val params = parsed.get("params").values.flatMap(_.headOption).getOrElse(Json.Null) | ||
| handler | ||
| .handle(params) | ||
| .map(result => JsonRpcCodec.successResponse(idJson, result)) | ||
| .catchAll(error => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse( | ||
| idJson, | ||
| -32603, | ||
| Option(error.getMessage).getOrElse("Internal error") | ||
| ) | ||
| ) | ||
| ) | ||
| } | ||
| case _ => | ||
| ZIO.succeed( | ||
| JsonRpcCodec.errorResponse(idJson, -32600, "Invalid Request: 'method' must be a string") | ||
| ) | ||
| } |
|
|
||
| if (dealiasedSym.fullName != "zio.ZIO") | ||
| report.errorAndAbort( | ||
| s"Method '${methodName}' must return ZIO[R, E, A] or a type alias thereof, got: ${returnType.show}" |
| name: String, | ||
| inputSchema: Schema[Input], | ||
| outputSchema: Schema[Output], | ||
| errorSchema: Option[Schema[?]], |
There was a problem hiding this comment.
This implies we have a canonical error type that we support. I think we might want to rethink this:
Output just has a single schema, and you get prisms for success and failure?
Or we can just capture success and failure: success: Schema[Success], failure: Schema[Failure], and then have an As[Output, EIther[Success, Failure]] or something similar, which says, you can go from the either to the user type, and back again, if you need to.
Summary
Adds a new
zio-blocks-rpcmodule providing aderives RPCtype class that captures service trait structure as a "pure data" representation — analogous to howderives Schemaworks for data types.Closes #1143
Design Direction (per @jdegoes)
Then derive gRPC, JSON RPC, etc. from the "pure data" representation. ZIO Schema is used for input/output types.
What's Included
RPC[T]type class — runtime data structure capturing service operations (names, schemas, annotations, errors)RPC.Operation[Input, Output]— per-operation metadata with Schema-backed I/O typesMetaAnnotation/ErrorAnnotation[E]— annotation infrastructure for service traits and methodsinline def derived[T]) — introspects trait methods via quotes, auto-summonsSchemafor all parameter and return types, unwraps ZIO effect typesRpcDeriver[Protocol[_]]— trait for protocol-specific derivation (analogous to Schema'sDeriver[TC[_]])RpcFormat— associates a protocol with its deriver (analogous to Schema'sFormat)JsonRpcCodec[T]with request/response handling and JSON-RPC error codesKey Design Decisions
Schema[A]), not purely type-level (unlike ops-mirror) — enables runtime protocol derivationImplicits.searchto find Schema instances for all I/O types at compile timeZIO[R, E, A]; macro extracts output and error types via.dealiasModule Structure