Skip to content

feat(rpc): add zio-blocks-rpc module with derives RPC type class (#1143)#1225

Open
987Nabil wants to merge 1 commit intozio:mainfrom
987Nabil:feat/rpc
Open

feat(rpc): add zio-blocks-rpc module with derives RPC type class (#1143)#1225
987Nabil wants to merge 1 commit intozio:mainfrom
987Nabil:feat/rpc

Conversation

@987Nabil
Copy link
Contributor

Summary

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.

Closes #1143

Design Direction (per @jdegoes)

trait MyService { ... } derives RPC

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 types
  • MetaAnnotation / ErrorAnnotation[E] — annotation infrastructure for service traits and methods
  • Scala 3 derivation macro (inline def derived[T]) — introspects trait methods via quotes, auto-summons Schema for all parameter and return types, unwraps ZIO effect types
  • RpcDeriver[Protocol[_]] — trait for protocol-specific derivation (analogous to Schema's Deriver[TC[_]])
  • RpcFormat — associates a protocol with its deriver (analogous to Schema's Format)
  • JSON-RPC 2.0 reference protocol — proof-of-concept JsonRpcCodec[T] with request/response handling and JSON-RPC error codes
  • 33 tests — macro derivation (22) + JSON-RPC integration (11), passing on both JVM and JS
  • 100% coverage (stmt + branch, excluding macro files)

Key Design Decisions

  • Runtime data (like Schema[A]), not purely type-level (unlike ops-mirror) — enables runtime protocol derivation
  • Schema auto-summoning — macro uses Implicits.search to find Schema instances for all I/O types at compile time
  • ZIO effect unwrapping — methods must return ZIO[R, E, A]; macro extracts output and error types via .dealias
  • Compile-time validation — overloaded, generic, curried, and non-ZIO methods produce clear compile errors
  • Scala 3 only — no Scala 2 cross-building
  • Cross-platform — JVM + JS

Module Structure

rpc/shared/src/main/scala/zio/blocks/rpc/
├── RPC.scala                    # RPC[T], Operation, ServiceMetadata
├── annotations.scala            # MetaAnnotation, ErrorAnnotation[E]
├── RpcDeriver.scala             # Protocol derivation trait
├── RpcFormat.scala              # Format wrapper
└── jsonrpc/
    ├── JsonRpcCodec.scala       # JSON-RPC 2.0 handler
    ├── JsonRpcDeriver.scala     # RpcDeriver[JsonRpcCodec]
    └── JsonRpcFormat.scala      # RpcFormat wrapper

rpc/shared/src/main/scala-3/zio/blocks/rpc/
├── RPCMacros.scala              # Scala 3 derivation macro (295 lines)
├── RPCCompanionVersionSpecific.scala
└── RPCVersionSpecific.scala

Copilot AI review requested due to automatic review settings March 14, 2026 13:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sbt to include the new rpc crossProject in root aggregation and aliases; also adds OTel Histogram/Gauge implementations.

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]]
Comment on lines +34 to +44
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)
}
}

case n =>
Comment on lines +6 to +11
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
@987Nabil 987Nabil force-pushed the feat/rpc branch 2 times, most recently from 5e17c3d to a13ed0a Compare March 14, 2026 16:53
`scope-examples`,
schema.jvm,
schema.js,
rpc.jvm,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module missing in alias

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with RpcDeriver / RpcFormat abstractions and annotation types.
  • Implements Scala 3 macro-based derivation (RPC.derived[T]) to introspect service traits and summon Schema for 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 into build.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.

Comment on lines +41 to +79
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")
)
}
Comment on lines +28 to +32
* 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
Comment on lines +65 to +73
.catchAll(error =>
ZIO.succeed(
JsonRpcCodec.errorResponse(
idJson,
-32603,
Option(error.getMessage).getOrElse("Internal error")
)
)
)
@987Nabil 987Nabil force-pushed the feat/rpc branch 3 times, most recently from 9b5def8 to 6579f5f Compare March 15, 2026 15:33
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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-summon Schema for 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.

Comment on lines +65 to +70
.catchAll(error =>
ZIO.succeed(
JsonRpcCodec.errorResponse(
idJson,
-32603,
Option(error.getMessage).getOrElse("Internal error")
Comment on lines +61 to +74
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")
)
)
)
}
Comment on lines +85 to +86

trait OperationHandler {
Comment on lines +23 to +31
/**
* 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](
Comment on lines +41 to +79
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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZIO?

name: String,
inputSchema: Schema[Input],
outputSchema: Schema[Output],
errorSchema: Option[Schema[?]],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zio-blocks-rpc: RPC type class for service trait introspection (derives RPC)

3 participants